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