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