1package tui
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "math/rand"
8 "strings"
9 "time"
10
11 "github.com/charmbracelet/bubbles/v2/key"
12 tea "github.com/charmbracelet/bubbletea/v2"
13 "github.com/charmbracelet/crush/internal/app"
14 "github.com/charmbracelet/crush/internal/config"
15 "github.com/charmbracelet/crush/internal/event"
16 "github.com/charmbracelet/crush/internal/llm/agent"
17 "github.com/charmbracelet/crush/internal/permission"
18 "github.com/charmbracelet/crush/internal/pubsub"
19 cmpChat "github.com/charmbracelet/crush/internal/tui/components/chat"
20 "github.com/charmbracelet/crush/internal/tui/components/chat/splash"
21 "github.com/charmbracelet/crush/internal/tui/components/completions"
22 "github.com/charmbracelet/crush/internal/tui/components/core"
23 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
24 "github.com/charmbracelet/crush/internal/tui/components/core/status"
25 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
26 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
27 "github.com/charmbracelet/crush/internal/tui/components/dialogs/compact"
28 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
29 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
30 "github.com/charmbracelet/crush/internal/tui/components/dialogs/permissions"
31 "github.com/charmbracelet/crush/internal/tui/components/dialogs/quit"
32 "github.com/charmbracelet/crush/internal/tui/components/dialogs/sessions"
33 "github.com/charmbracelet/crush/internal/tui/page"
34 "github.com/charmbracelet/crush/internal/tui/page/chat"
35 "github.com/charmbracelet/crush/internal/tui/styles"
36 "github.com/charmbracelet/crush/internal/tui/util"
37 "github.com/charmbracelet/lipgloss/v2"
38 "github.com/modelcontextprotocol/go-sdk/mcp"
39 "golang.org/x/text/cases"
40 "golang.org/x/text/language"
41)
42
43var lastMouseEvent time.Time
44
45func MouseEventFilter(m tea.Model, msg tea.Msg) tea.Msg {
46 switch msg.(type) {
47 case tea.MouseWheelMsg, tea.MouseMotionMsg:
48 now := time.Now()
49 // trackpad is sending too many requests
50 if now.Sub(lastMouseEvent) < 15*time.Millisecond {
51 return nil
52 }
53 lastMouseEvent = now
54 }
55 return msg
56}
57
58// appModel represents the main application model that manages pages, dialogs, and UI state.
59type appModel struct {
60 wWidth, wHeight int // Window dimensions
61 width, height int
62 keyMap KeyMap
63
64 currentPage page.PageID
65 previousPage page.PageID
66 pages map[page.PageID]util.Model
67 loadedPages map[page.PageID]bool
68
69 // Status
70 status status.StatusCmp
71 showingFullHelp bool
72
73 app *app.App
74
75 dialog dialogs.DialogCmp
76 completions completions.Completions
77 isConfigured bool
78
79 // Chat Page Specific
80 selectedSessionID string // The ID of the currently selected session
81}
82
83// Init initializes the application model and returns initial commands.
84func (a appModel) Init() tea.Cmd {
85 item, ok := a.pages[a.currentPage]
86 if !ok {
87 return nil
88 }
89
90 var cmds []tea.Cmd
91 cmd := item.Init()
92 cmds = append(cmds, cmd)
93 a.loadedPages[a.currentPage] = true
94
95 cmd = a.status.Init()
96 cmds = append(cmds, cmd)
97
98 cmds = append(cmds, tea.EnableMouseAllMotion)
99
100 return tea.Batch(cmds...)
101}
102
103// Update handles incoming messages and updates the application state.
104func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
105 var cmds []tea.Cmd
106 var cmd tea.Cmd
107 a.isConfigured = config.HasInitialDataConfig()
108
109 switch msg := msg.(type) {
110 case tea.KeyboardEnhancementsMsg:
111 for id, page := range a.pages {
112 m, pageCmd := page.Update(msg)
113 if model, ok := m.(util.Model); ok {
114 a.pages[id] = model
115 }
116
117 if pageCmd != nil {
118 cmds = append(cmds, pageCmd)
119 }
120 }
121 return a, tea.Batch(cmds...)
122 case tea.WindowSizeMsg:
123 a.wWidth, a.wHeight = msg.Width, msg.Height
124 a.completions.Update(msg)
125 return a, a.handleWindowResize(msg.Width, msg.Height)
126
127 // Completions messages
128 case completions.OpenCompletionsMsg, completions.FilterCompletionsMsg,
129 completions.CloseCompletionsMsg, completions.RepositionCompletionsMsg:
130 u, completionCmd := a.completions.Update(msg)
131 if model, ok := u.(completions.Completions); ok {
132 a.completions = model
133 }
134
135 return a, completionCmd
136
137 // Dialog messages
138 case dialogs.OpenDialogMsg, dialogs.CloseDialogMsg:
139 u, completionCmd := a.completions.Update(completions.CloseCompletionsMsg{})
140 a.completions = u.(completions.Completions)
141 u, dialogCmd := a.dialog.Update(msg)
142 a.dialog = u.(dialogs.DialogCmp)
143 return a, tea.Batch(completionCmd, dialogCmd)
144 case commands.ShowArgumentsDialogMsg:
145 var args []commands.Argument
146 for _, arg := range msg.ArgNames {
147 args = append(args, commands.Argument{
148 Name: arg,
149 Title: cases.Title(language.English).String(arg),
150 Required: true,
151 })
152 }
153 return a, util.CmdHandler(
154 dialogs.OpenDialogMsg{
155 Model: commands.NewCommandArgumentsDialog(
156 msg.CommandID,
157 msg.CommandID,
158 msg.CommandID,
159 msg.Description,
160 args,
161 func(args map[string]string) tea.Cmd {
162 return func() tea.Msg {
163 content := msg.Content
164 for _, name := range msg.ArgNames {
165 value := args[name]
166 placeholder := "$" + name
167 content = strings.ReplaceAll(content, placeholder, value)
168 }
169 return commands.CommandRunCustomMsg{
170 Content: content,
171 }
172 }
173 },
174 ),
175 },
176 )
177 case commands.ShowMCPPromptArgumentsDialogMsg:
178 prompt, ok := agent.GetMCPPrompt(msg.PromptID)
179 clientName, _, ok := strings.Cut(msg.PromptID, ":")
180 if !ok {
181 slog.Warn("prompt not found", "prompt_id", msg.PromptID, "prompt_name", msg.PromptName)
182 util.ReportWarn(fmt.Sprintf("Prompt %s not found", msg.PromptName))
183 return a, nil
184 }
185 args := make([]commands.Argument, 0, len(prompt.Arguments))
186 for _, arg := range prompt.Arguments {
187 args = append(args, commands.Argument(*arg))
188 }
189 dialog := commands.NewCommandArgumentsDialog(
190 msg.PromptID,
191 prompt.Title,
192 prompt.Name,
193 prompt.Description,
194 args,
195 func(args map[string]string) tea.Cmd {
196 return func() tea.Msg {
197 ctx := context.Background()
198 result, err := agent.GetMCPPromptContent(ctx, clientName, prompt.Name, args)
199 if err != nil {
200 return util.ReportError(err)
201 }
202
203 var content strings.Builder
204 for _, msg := range result.Messages {
205 if msg.Role != "user" {
206 continue
207 }
208 textContent, ok := msg.Content.(*mcp.TextContent)
209 if !ok {
210 continue
211 }
212 _, _ = content.WriteString(textContent.Text)
213 _, _ = content.WriteString("\n")
214 }
215 return commands.CommandRunCustomMsg{
216 Content: content.String(),
217 }
218 }
219 },
220 )
221 return a, util.CmdHandler(
222 dialogs.OpenDialogMsg{
223 Model: dialog,
224 },
225 )
226 // Page change messages
227 case page.PageChangeMsg:
228 return a, a.moveToPage(msg.ID)
229
230 // Status Messages
231 case util.InfoMsg, util.ClearStatusMsg:
232 s, statusCmd := a.status.Update(msg)
233 a.status = s.(status.StatusCmp)
234 cmds = append(cmds, statusCmd)
235 return a, tea.Batch(cmds...)
236
237 // Session
238 case cmpChat.SessionSelectedMsg:
239 a.selectedSessionID = msg.ID
240 case cmpChat.SessionClearedMsg:
241 a.selectedSessionID = ""
242 // Commands
243 case commands.SwitchSessionsMsg:
244 return a, func() tea.Msg {
245 allSessions, _ := a.app.Sessions.List(context.Background())
246 return dialogs.OpenDialogMsg{
247 Model: sessions.NewSessionDialogCmp(allSessions, a.selectedSessionID),
248 }
249 }
250
251 case commands.SwitchModelMsg:
252 return a, util.CmdHandler(
253 dialogs.OpenDialogMsg{
254 Model: models.NewModelDialogCmp(),
255 },
256 )
257 // Compact
258 case commands.CompactMsg:
259 return a, util.CmdHandler(dialogs.OpenDialogMsg{
260 Model: compact.NewCompactDialogCmp(a.app.CoderAgent, msg.SessionID, true),
261 })
262 case commands.QuitMsg:
263 return a, util.CmdHandler(dialogs.OpenDialogMsg{
264 Model: quit.NewQuitDialog(),
265 })
266 case commands.ToggleYoloModeMsg:
267 a.app.Permissions.SetSkipRequests(!a.app.Permissions.SkipRequests())
268 case commands.ToggleHelpMsg:
269 a.status.ToggleFullHelp()
270 a.showingFullHelp = !a.showingFullHelp
271 return a, a.handleWindowResize(a.wWidth, a.wHeight)
272 // Model Switch
273 case models.ModelSelectedMsg:
274 if a.app.CoderAgent.IsBusy() {
275 return a, util.ReportWarn("Agent is busy, please wait...")
276 }
277
278 config.Get().UpdatePreferredModel(msg.ModelType, msg.Model)
279
280 // Update the agent with the new model/provider configuration
281 if err := a.app.UpdateAgentModel(); err != nil {
282 return a, util.ReportError(fmt.Errorf("model changed to %s but failed to update agent: %v", msg.Model.Model, err))
283 }
284
285 modelTypeName := "large"
286 if msg.ModelType == config.SelectedModelTypeSmall {
287 modelTypeName = "small"
288 }
289 return a, util.ReportInfo(fmt.Sprintf("%s model changed to %s", modelTypeName, msg.Model.Model))
290
291 // File Picker
292 case commands.OpenFilePickerMsg:
293 event.FilePickerOpened()
294
295 if a.dialog.ActiveDialogID() == filepicker.FilePickerID {
296 // If the commands dialog is already open, close it
297 return a, util.CmdHandler(dialogs.CloseDialogMsg{})
298 }
299 return a, util.CmdHandler(dialogs.OpenDialogMsg{
300 Model: filepicker.NewFilePickerCmp(a.app.Config().WorkingDir()),
301 })
302 // Permissions
303 case pubsub.Event[permission.PermissionNotification]:
304 item, ok := a.pages[a.currentPage]
305 if !ok {
306 return a, nil
307 }
308
309 // Forward to view.
310 updated, itemCmd := item.Update(msg)
311 if model, ok := updated.(util.Model); ok {
312 a.pages[a.currentPage] = model
313 }
314
315 return a, itemCmd
316 case pubsub.Event[permission.PermissionRequest]:
317 return a, util.CmdHandler(dialogs.OpenDialogMsg{
318 Model: permissions.NewPermissionDialogCmp(msg.Payload, &permissions.Options{
319 DiffMode: config.Get().Options.TUI.DiffMode,
320 }),
321 })
322 case permissions.PermissionResponseMsg:
323 switch msg.Action {
324 case permissions.PermissionAllow:
325 a.app.Permissions.Grant(msg.Permission)
326 case permissions.PermissionAllowForSession:
327 a.app.Permissions.GrantPersistent(msg.Permission)
328 case permissions.PermissionDeny:
329 a.app.Permissions.Deny(msg.Permission)
330 }
331 return a, nil
332 // Agent Events
333 case pubsub.Event[agent.AgentEvent]:
334 payload := msg.Payload
335
336 // Forward agent events to dialogs
337 if a.dialog.HasDialogs() && a.dialog.ActiveDialogID() == compact.CompactDialogID {
338 u, dialogCmd := a.dialog.Update(payload)
339 if model, ok := u.(dialogs.DialogCmp); ok {
340 a.dialog = model
341 }
342
343 cmds = append(cmds, dialogCmd)
344 }
345
346 // Handle auto-compact logic
347 if payload.Done && payload.Type == agent.AgentEventTypeResponse && a.selectedSessionID != "" {
348 // Get current session to check token usage
349 session, err := a.app.Sessions.Get(context.Background(), a.selectedSessionID)
350 if err == nil {
351 model := a.app.CoderAgent.Model()
352 contextWindow := model.ContextWindow
353 tokens := session.CompletionTokens + session.PromptTokens
354 if (tokens >= int64(float64(contextWindow)*0.95)) && !config.Get().Options.DisableAutoSummarize { // Show compact confirmation dialog
355 cmds = append(cmds, util.CmdHandler(dialogs.OpenDialogMsg{
356 Model: compact.NewCompactDialogCmp(a.app.CoderAgent, a.selectedSessionID, false),
357 }))
358 }
359 }
360 }
361
362 return a, tea.Batch(cmds...)
363 case splash.OnboardingCompleteMsg:
364 item, ok := a.pages[a.currentPage]
365 if !ok {
366 return a, nil
367 }
368
369 a.isConfigured = config.HasInitialDataConfig()
370 updated, pageCmd := item.Update(msg)
371 if model, ok := updated.(util.Model); ok {
372 a.pages[a.currentPage] = model
373 }
374
375 cmds = append(cmds, pageCmd)
376 return a, tea.Batch(cmds...)
377
378 case tea.KeyPressMsg:
379 return a, a.handleKeyPressMsg(msg)
380
381 case tea.MouseWheelMsg:
382 if a.dialog.HasDialogs() {
383 u, dialogCmd := a.dialog.Update(msg)
384 a.dialog = u.(dialogs.DialogCmp)
385 cmds = append(cmds, dialogCmd)
386 } else {
387 item, ok := a.pages[a.currentPage]
388 if !ok {
389 return a, nil
390 }
391
392 updated, pageCmd := item.Update(msg)
393 if model, ok := updated.(util.Model); ok {
394 a.pages[a.currentPage] = model
395 }
396
397 cmds = append(cmds, pageCmd)
398 }
399 return a, tea.Batch(cmds...)
400 case tea.PasteMsg:
401 if a.dialog.HasDialogs() {
402 u, dialogCmd := a.dialog.Update(msg)
403 if model, ok := u.(dialogs.DialogCmp); ok {
404 a.dialog = model
405 }
406
407 cmds = append(cmds, dialogCmd)
408 } else {
409 item, ok := a.pages[a.currentPage]
410 if !ok {
411 return a, nil
412 }
413
414 updated, pageCmd := item.Update(msg)
415 if model, ok := updated.(util.Model); ok {
416 a.pages[a.currentPage] = model
417 }
418
419 cmds = append(cmds, pageCmd)
420 }
421 return a, tea.Batch(cmds...)
422 }
423 s, _ := a.status.Update(msg)
424 a.status = s.(status.StatusCmp)
425
426 item, ok := a.pages[a.currentPage]
427 if !ok {
428 return a, nil
429 }
430
431 updated, cmd := item.Update(msg)
432 if model, ok := updated.(util.Model); ok {
433 a.pages[a.currentPage] = model
434 }
435
436 if a.dialog.HasDialogs() {
437 u, dialogCmd := a.dialog.Update(msg)
438 if model, ok := u.(dialogs.DialogCmp); ok {
439 a.dialog = model
440 }
441
442 cmds = append(cmds, dialogCmd)
443 }
444 cmds = append(cmds, cmd)
445 return a, tea.Batch(cmds...)
446}
447
448// handleWindowResize processes window resize events and updates all components.
449func (a *appModel) handleWindowResize(width, height int) tea.Cmd {
450 var cmds []tea.Cmd
451
452 // TODO: clean up these magic numbers.
453 if a.showingFullHelp {
454 height -= 5
455 } else {
456 height -= 2
457 }
458
459 a.width, a.height = width, height
460 // Update status bar
461 s, cmd := a.status.Update(tea.WindowSizeMsg{Width: width, Height: height})
462 if model, ok := s.(status.StatusCmp); ok {
463 a.status = model
464 }
465 cmds = append(cmds, cmd)
466
467 // Update the current view.
468 for p, page := range a.pages {
469 updated, pageCmd := page.Update(tea.WindowSizeMsg{Width: width, Height: height})
470 if model, ok := updated.(util.Model); ok {
471 a.pages[p] = model
472 }
473
474 cmds = append(cmds, pageCmd)
475 }
476
477 // Update the dialogs
478 dialog, cmd := a.dialog.Update(tea.WindowSizeMsg{Width: width, Height: height})
479 if model, ok := dialog.(dialogs.DialogCmp); ok {
480 a.dialog = model
481 }
482
483 cmds = append(cmds, cmd)
484
485 return tea.Batch(cmds...)
486}
487
488// handleKeyPressMsg processes keyboard input and routes to appropriate handlers.
489func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
490 // Check this first as the user should be able to quit no matter what.
491 if key.Matches(msg, a.keyMap.Quit) {
492 if a.dialog.ActiveDialogID() == quit.QuitDialogID {
493 return tea.Quit
494 }
495 return util.CmdHandler(dialogs.OpenDialogMsg{
496 Model: quit.NewQuitDialog(),
497 })
498 }
499
500 if a.completions.Open() {
501 // completions
502 keyMap := a.completions.KeyMap()
503 switch {
504 case key.Matches(msg, keyMap.Up), key.Matches(msg, keyMap.Down),
505 key.Matches(msg, keyMap.Select), key.Matches(msg, keyMap.Cancel),
506 key.Matches(msg, keyMap.UpInsert), key.Matches(msg, keyMap.DownInsert):
507 u, cmd := a.completions.Update(msg)
508 a.completions = u.(completions.Completions)
509 return cmd
510 }
511 }
512 if a.dialog.HasDialogs() {
513 u, dialogCmd := a.dialog.Update(msg)
514 a.dialog = u.(dialogs.DialogCmp)
515 return dialogCmd
516 }
517 switch {
518 // help
519 case key.Matches(msg, a.keyMap.Help):
520 a.status.ToggleFullHelp()
521 a.showingFullHelp = !a.showingFullHelp
522 return a.handleWindowResize(a.wWidth, a.wHeight)
523 // dialogs
524 case key.Matches(msg, a.keyMap.Commands):
525 // if the app is not configured show no commands
526 if !a.isConfigured {
527 return nil
528 }
529 if a.dialog.ActiveDialogID() == commands.CommandsDialogID {
530 return util.CmdHandler(dialogs.CloseDialogMsg{})
531 }
532 if a.dialog.HasDialogs() {
533 return nil
534 }
535 return util.CmdHandler(dialogs.OpenDialogMsg{
536 Model: commands.NewCommandDialog(a.selectedSessionID),
537 })
538 case key.Matches(msg, a.keyMap.Sessions):
539 // if the app is not configured show no sessions
540 if !a.isConfigured {
541 return nil
542 }
543 if a.dialog.ActiveDialogID() == sessions.SessionsDialogID {
544 return util.CmdHandler(dialogs.CloseDialogMsg{})
545 }
546 if a.dialog.HasDialogs() && a.dialog.ActiveDialogID() != commands.CommandsDialogID {
547 return nil
548 }
549 var cmds []tea.Cmd
550 if a.dialog.ActiveDialogID() == commands.CommandsDialogID {
551 // If the commands dialog is open, close it first
552 cmds = append(cmds, util.CmdHandler(dialogs.CloseDialogMsg{}))
553 }
554 cmds = append(cmds,
555 func() tea.Msg {
556 allSessions, _ := a.app.Sessions.List(context.Background())
557 return dialogs.OpenDialogMsg{
558 Model: sessions.NewSessionDialogCmp(allSessions, a.selectedSessionID),
559 }
560 },
561 )
562 return tea.Sequence(cmds...)
563 case key.Matches(msg, a.keyMap.Suspend):
564 if a.app.CoderAgent != nil && a.app.CoderAgent.IsBusy() {
565 return util.ReportWarn("Agent is busy, please wait...")
566 }
567 return tea.Suspend
568 default:
569 item, ok := a.pages[a.currentPage]
570 if !ok {
571 return nil
572 }
573
574 updated, cmd := item.Update(msg)
575 if model, ok := updated.(util.Model); ok {
576 a.pages[a.currentPage] = model
577 }
578 return cmd
579 }
580}
581
582// moveToPage handles navigation between different pages in the application.
583func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
584 if a.app.CoderAgent.IsBusy() {
585 // TODO: maybe remove this : For now we don't move to any page if the agent is busy
586 return util.ReportWarn("Agent is busy, please wait...")
587 }
588
589 var cmds []tea.Cmd
590 if _, ok := a.loadedPages[pageID]; !ok {
591 cmd := a.pages[pageID].Init()
592 cmds = append(cmds, cmd)
593 a.loadedPages[pageID] = true
594 }
595 a.previousPage = a.currentPage
596 a.currentPage = pageID
597 if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
598 cmd := sizable.SetSize(a.width, a.height)
599 cmds = append(cmds, cmd)
600 }
601
602 return tea.Batch(cmds...)
603}
604
605// View renders the complete application interface including pages, dialogs, and overlays.
606func (a *appModel) View() tea.View {
607 var view tea.View
608 t := styles.CurrentTheme()
609 view.BackgroundColor = t.BgBase
610 if a.wWidth < 25 || a.wHeight < 15 {
611 view.Layer = lipgloss.NewCanvas(
612 lipgloss.NewLayer(
613 t.S().Base.Width(a.wWidth).Height(a.wHeight).
614 Align(lipgloss.Center, lipgloss.Center).
615 Render(
616 t.S().Base.
617 Padding(1, 4).
618 Foreground(t.White).
619 BorderStyle(lipgloss.RoundedBorder()).
620 BorderForeground(t.Primary).
621 Render("Window too small!"),
622 ),
623 ),
624 )
625 return view
626 }
627
628 page := a.pages[a.currentPage]
629 if withHelp, ok := page.(core.KeyMapHelp); ok {
630 a.status.SetKeyMap(withHelp.Help())
631 }
632 pageView := page.View()
633 components := []string{
634 pageView,
635 }
636 components = append(components, a.status.View())
637
638 appView := lipgloss.JoinVertical(lipgloss.Top, components...)
639 layers := []*lipgloss.Layer{
640 lipgloss.NewLayer(appView),
641 }
642 if a.dialog.HasDialogs() {
643 layers = append(
644 layers,
645 a.dialog.GetLayers()...,
646 )
647 }
648
649 var cursor *tea.Cursor
650 if v, ok := page.(util.Cursor); ok {
651 cursor = v.Cursor()
652 // Hide the cursor if it's positioned outside the textarea
653 statusHeight := a.height - strings.Count(pageView, "\n") + 1
654 if cursor != nil && cursor.Y+statusHeight+chat.EditorHeight-2 <= a.height { // 2 for the top and bottom app padding
655 cursor = nil
656 }
657 }
658 activeView := a.dialog.ActiveModel()
659 if activeView != nil {
660 cursor = nil // Reset cursor if a dialog is active unless it implements util.Cursor
661 if v, ok := activeView.(util.Cursor); ok {
662 cursor = v.Cursor()
663 }
664 }
665
666 if a.completions.Open() && cursor != nil {
667 cmp := a.completions.View()
668 x, y := a.completions.Position()
669 layers = append(
670 layers,
671 lipgloss.NewLayer(cmp).X(x).Y(y),
672 )
673 }
674
675 canvas := lipgloss.NewCanvas(
676 layers...,
677 )
678
679 view.Layer = canvas
680 view.Cursor = cursor
681 view.ProgressBar = tea.NewProgressBar(tea.ProgressBarNone, 0)
682 if a.app.CoderAgent.IsBusy() {
683 // use a random percentage to prevent the ghostty from hiding it after
684 // a timeout.
685 view.ProgressBar = tea.NewProgressBar(tea.ProgressBarIndeterminate, rand.Intn(100))
686 }
687 return view
688}
689
690// New creates and initializes a new TUI application model.
691func New(app *app.App) tea.Model {
692 chatPage := chat.New(app)
693 keyMap := DefaultKeyMap()
694 keyMap.pageBindings = chatPage.Bindings()
695
696 model := &appModel{
697 currentPage: chat.ChatPageID,
698 app: app,
699 status: status.NewStatusCmp(),
700 loadedPages: make(map[page.PageID]bool),
701 keyMap: keyMap,
702
703 pages: map[page.PageID]util.Model{
704 chat.ChatPageID: chatPage,
705 },
706
707 dialog: dialogs.NewDialogCmp(),
708 completions: completions.New(),
709 }
710
711 return model
712}