1package tui
2
3import (
4 "github.com/charmbracelet/bubbles/v2/key"
5 tea "github.com/charmbracelet/bubbletea/v2"
6 "github.com/charmbracelet/lipgloss/v2"
7 "github.com/opencode-ai/opencode/internal/app"
8 "github.com/opencode-ai/opencode/internal/logging"
9 "github.com/opencode-ai/opencode/internal/pubsub"
10 "github.com/opencode-ai/opencode/internal/tui/components/core"
11 "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
12 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/commands"
13 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/quit"
14 "github.com/opencode-ai/opencode/internal/tui/layout"
15 "github.com/opencode-ai/opencode/internal/tui/page"
16 "github.com/opencode-ai/opencode/internal/tui/theme"
17 "github.com/opencode-ai/opencode/internal/tui/util"
18)
19
20// type startCompactSessionMsg struct{}
21
22type appModel struct {
23 width, height int
24 keyMap KeyMap
25
26 currentPage page.PageID
27 previousPage page.PageID
28 pages map[page.PageID]util.Model
29 loadedPages map[page.PageID]bool
30
31 status core.StatusCmp
32
33 app *app.App
34
35 // selectedSession session.Session
36 //
37 // showPermissions bool
38 // permissions dialog.PermissionDialogCmp
39 //
40 // showHelp bool
41 // help dialog.HelpCmp
42 //
43 // showSessionDialog bool
44 // sessionDialog dialog.SessionDialog
45 //
46 // showCommandDialog bool
47 // commandDialog dialog.CommandDialog
48 // commands []dialog.Command
49 //
50 // showModelDialog bool
51 // modelDialog dialog.ModelDialog
52 //
53 // showInitDialog bool
54 // initDialog dialog.InitDialogCmp
55 //
56 // showFilepicker bool
57 // filepicker dialog.FilepickerCmp
58 //
59 // showThemeDialog bool
60 // themeDialog dialog.ThemeDialog
61 //
62 // showMultiArgumentsDialog bool
63 // multiArgumentsDialog dialog.MultiArgumentsDialogCmp
64 //
65 // isCompacting bool
66 // compactingMessage string
67
68 // NEW DIALOG
69 dialog dialogs.DialogCmp
70}
71
72func (a appModel) Init() tea.Cmd {
73 var cmds []tea.Cmd
74 cmd := a.pages[a.currentPage].Init()
75 cmds = append(cmds, cmd)
76 a.loadedPages[a.currentPage] = true
77
78 cmd = a.status.Init()
79 cmds = append(cmds, cmd)
80 // cmd = a.help.Init()
81 // cmds = append(cmds, cmd)
82 // cmd = a.sessionDialog.Init()
83 // cmds = append(cmds, cmd)
84 // cmd = a.commandDialog.Init()
85 // cmds = append(cmds, cmd)
86 // cmd = a.modelDialog.Init()
87 // cmds = append(cmds, cmd)
88 // cmd = a.initDialog.Init()
89 // cmds = append(cmds, cmd)
90 // cmd = a.filepicker.Init()
91 // cmds = append(cmds, cmd)
92 // cmd = a.themeDialog.Init()
93 // cmds = append(cmds, cmd)
94
95 // Check if we should show the init dialog
96 // cmds = append(cmds, func() tea.Msg {
97 // shouldShow, err := config.ShouldShowInitDialog()
98 // if err != nil {
99 // return util.InfoMsg{
100 // Type: util.InfoTypeError,
101 // Msg: "Failed to check init status: " + err.Error(),
102 // }
103 // }
104 // return dialog.ShowInitDialogMsg{Show: shouldShow}
105 // })
106
107 t := theme.CurrentTheme()
108 cmds = append(cmds, tea.SetBackgroundColor(t.Background()))
109 return tea.Batch(cmds...)
110}
111
112func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
113 var cmds []tea.Cmd
114 var cmd tea.Cmd
115
116 switch msg := msg.(type) {
117 case tea.WindowSizeMsg:
118 return a, a.handleWindowResize(msg)
119 // TODO: remove when refactor is done
120 // msg.Height -= 1 // Make space for the status bar
121 // a.width, a.height = msg.Width, msg.Height
122 //
123 // s, _ := a.status.Update(msg)
124 // a.status = s.(core.StatusCmp)
125 // updated, cmd := a.pages[a.currentPage].Update(msg)
126 // a.pages[a.currentPage] = updated.(util.Model)
127 // cmds = append(cmds, cmd)
128 //
129 // prm, permCmd := a.permissions.Update(msg)
130 // a.permissions = prm.(dialog.PermissionDialogCmp)
131 // cmds = append(cmds, permCmd)
132 //
133 // help, helpCmd := a.help.Update(msg)
134 // a.help = help.(dialog.HelpCmp)
135 // cmds = append(cmds, helpCmd)
136 //
137 // session, sessionCmd := a.sessionDialog.Update(msg)
138 // a.sessionDialog = session.(dialog.SessionDialog)
139 // cmds = append(cmds, sessionCmd)
140 //
141 // command, commandCmd := a.commandDialog.Update(msg)
142 // a.commandDialog = command.(dialog.CommandDialog)
143 // cmds = append(cmds, commandCmd)
144 //
145 // filepicker, filepickerCmd := a.filepicker.Update(msg)
146 // a.filepicker = filepicker.(dialog.FilepickerCmp)
147 // cmds = append(cmds, filepickerCmd)
148 //
149 // a.initDialog.SetSize(msg.Width, msg.Height)
150 //
151 // if a.showMultiArgumentsDialog {
152 // a.multiArgumentsDialog.SetSize(msg.Width, msg.Height)
153 // args, argsCmd := a.multiArgumentsDialog.Update(msg)
154 // a.multiArgumentsDialog = args.(dialog.MultiArgumentsDialogCmp)
155 // cmds = append(cmds, argsCmd, a.multiArgumentsDialog.Init())
156 // }
157 //
158 // dialog, cmd := a.dialog.Update(msg)
159 // a.dialog = dialog.(dialogs.DialogCmp)
160 // cmds = append(cmds, cmd)
161 //
162 // return a, tea.Batch(cmds...)
163
164 // Dialog messages
165 case dialogs.OpenDialogMsg, dialogs.CloseDialogMsg:
166 u, dialogCmd := a.dialog.Update(msg)
167 a.dialog = u.(dialogs.DialogCmp)
168 return a, dialogCmd
169
170 // Page change messages
171 case page.PageChangeMsg:
172 return a, a.moveToPage(msg.ID)
173
174 // Status Messages
175 case util.InfoMsg, util.ClearStatusMsg:
176 s, cmd := a.status.Update(msg)
177 a.status = s.(core.StatusCmp)
178 cmds = append(cmds, cmd)
179 return a, tea.Batch(cmds...)
180 // Logs
181 case pubsub.Event[logging.LogMessage]:
182 // Send to the status component
183 s, cmd := a.status.Update(msg)
184 a.status = s.(core.StatusCmp)
185 cmds = append(cmds, cmd)
186
187 // If the current page is logs, update the logs view
188 if a.currentPage == page.LogsPage {
189 updated, cmd := a.pages[a.currentPage].Update(msg)
190 a.pages[a.currentPage] = updated.(util.Model)
191 cmds = append(cmds, cmd)
192 }
193 return a, tea.Batch(cmds...)
194
195 // // Permission
196 // case pubsub.Event[permission.PermissionRequest]:
197 // a.showPermissions = true
198 // return a, a.permissions.SetPermissions(msg.Payload)
199 // case dialog.PermissionResponseMsg:
200 // var cmd tea.Cmd
201 // switch msg.Action {
202 // case dialog.PermissionAllow:
203 // a.app.Permissions.Grant(msg.Permission)
204 // case dialog.PermissionAllowForSession:
205 // a.app.Permissions.GrantPersistant(msg.Permission)
206 // case dialog.PermissionDeny:
207 // a.app.Permissions.Deny(msg.Permission)
208 // }
209 // a.showPermissions = false
210 // return a, cmd
211 //
212 // // Theme changed
213 // case dialog.ThemeChangedMsg:
214 // updated, cmd := a.pages[a.currentPage].Update(msg)
215 // a.pages[a.currentPage] = updated.(util.Model)
216 // a.showThemeDialog = false
217 // return a, tea.Batch(cmd, util.ReportInfo("Theme changed to: "+msg.ThemeName))
218 //
219 // case dialog.CloseSessionDialogMsg:
220 // a.showSessionDialog = false
221 // return a, nil
222 //
223 // case dialog.CloseCommandDialogMsg:
224 // a.showCommandDialog = false
225 // return a, nil
226 //
227 // case startCompactSessionMsg:
228 // // Start compacting the current session
229 // a.isCompacting = true
230 // a.compactingMessage = "Starting summarization..."
231 //
232 // if a.selectedSession.ID == "" {
233 // a.isCompacting = false
234 // return a, util.ReportWarn("No active session to summarize")
235 // }
236 //
237 // // Start the summarization process
238 // return a, func() tea.Msg {
239 // ctx := context.Background()
240 // a.app.CoderAgent.Summarize(ctx, a.selectedSession.ID)
241 // return nil
242 // }
243 //
244 // case pubsub.Event[agent.AgentEvent]:
245 // payload := msg.Payload
246 // if payload.Error != nil {
247 // a.isCompacting = false
248 // return a, util.ReportError(payload.Error)
249 // }
250 //
251 // a.compactingMessage = payload.Progress
252 //
253 // if payload.Done && payload.Type == agent.AgentEventTypeSummarize {
254 // a.isCompacting = false
255 // return a, util.ReportInfo("Session summarization complete")
256 // } else if payload.Done && payload.Type == agent.AgentEventTypeResponse && a.selectedSession.ID != "" {
257 // model := a.app.CoderAgent.Model()
258 // contextWindow := model.ContextWindow
259 // tokens := a.selectedSession.CompletionTokens + a.selectedSession.PromptTokens
260 // if (tokens >= int64(float64(contextWindow)*0.95)) && config.Get().AutoCompact {
261 // return a, util.CmdHandler(startCompactSessionMsg{})
262 // }
263 // }
264 // // Continue listening for events
265 // return a, nil
266 //
267 // case dialog.CloseThemeDialogMsg:
268 // a.showThemeDialog = false
269 // return a, nil
270 //
271 // case dialog.CloseModelDialogMsg:
272 // a.showModelDialog = false
273 // return a, nil
274 //
275 // case dialog.ModelSelectedMsg:
276 // a.showModelDialog = false
277 //
278 // model, err := a.app.CoderAgent.Update(config.AgentCoder, msg.Model.ID)
279 // if err != nil {
280 // return a, util.ReportError(err)
281 // }
282 //
283 // return a, util.ReportInfo(fmt.Sprintf("Model changed to %s", model.Name))
284 //
285 // case dialog.ShowInitDialogMsg:
286 // a.showInitDialog = msg.Show
287 // return a, nil
288 //
289 // case dialog.CloseInitDialogMsg:
290 // a.showInitDialog = false
291 // if msg.Initialize {
292 // // Run the initialization command
293 // for _, cmd := range a.commands {
294 // if cmd.ID == "init" {
295 // // Mark the project as initialized
296 // if err := config.MarkProjectInitialized(); err != nil {
297 // return a, util.ReportError(err)
298 // }
299 // return a, cmd.Handler(cmd)
300 // }
301 // }
302 // } else {
303 // // Mark the project as initialized without running the command
304 // if err := config.MarkProjectInitialized(); err != nil {
305 // return a, util.ReportError(err)
306 // }
307 // }
308 // return a, nil
309 //
310 // case chat.SessionSelectedMsg:
311 // a.selectedSession = msg
312 // a.sessionDialog.SetSelectedSession(msg.ID)
313 //
314 // case pubsub.Event[session.Session]:
315 // if msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == a.selectedSession.ID {
316 // a.selectedSession = msg.Payload
317 // }
318 // case dialog.SessionSelectedMsg:
319 // a.showSessionDialog = false
320 // if a.currentPage == page.ChatPage {
321 // return a, util.CmdHandler(chat.SessionSelectedMsg(msg.Session))
322 // }
323 // return a, nil
324 //
325 // case dialog.CommandSelectedMsg:
326 // a.showCommandDialog = false
327 // // Execute the command handler if available
328 // if msg.Command.Handler != nil {
329 // return a, msg.Command.Handler(msg.Command)
330 // }
331 // return a, util.ReportInfo("Command selected: " + msg.Command.Title)
332 //
333 // case dialog.ShowMultiArgumentsDialogMsg:
334 // // Show multi-arguments dialog
335 // a.multiArgumentsDialog = dialog.NewMultiArgumentsDialogCmp(msg.CommandID, msg.Content, msg.ArgNames)
336 // a.showMultiArgumentsDialog = true
337 // return a, a.multiArgumentsDialog.Init()
338 //
339 // case dialog.CloseMultiArgumentsDialogMsg:
340 // // Close multi-arguments dialog
341 // a.showMultiArgumentsDialog = false
342 //
343 // // If submitted, replace all named arguments and run the command
344 // if msg.Submit {
345 // content := msg.Content
346 //
347 // // Replace each named argument with its value
348 // for name, value := range msg.Args {
349 // placeholder := "$" + name
350 // content = strings.ReplaceAll(content, placeholder, value)
351 // }
352 //
353 // // Execute the command with arguments
354 // return a, util.CmdHandler(dialog.CommandRunCustomMsg{
355 // Content: content,
356 // Args: msg.Args,
357 // })
358 // }
359 // return a, nil
360 //
361 case tea.KeyPressMsg:
362 return a, a.handleKeyPressMsg(msg)
363 // if a.dialog.HasDialogs() {
364 // u, dialogCmd := a.dialog.Update(msg)
365 // a.dialog = u.(dialogs.DialogCmp)
366 // return a, dialogCmd
367 // }
368 // // If multi-arguments dialog is open, let it handle the key press first
369 // if a.showMultiArgumentsDialog {
370 // args, cmd := a.multiArgumentsDialog.Update(msg)
371 // a.multiArgumentsDialog = args.(dialog.MultiArgumentsDialogCmp)
372 // return a, cmd
373 // }
374 //
375 // switch {
376 // case key.Matches(msg, keys.Quit):
377 // // TODO: fix this after testing
378 // // a.showQuit = !a.showQuit
379 // // if a.showHelp {
380 // // a.showHelp = false
381 // // }
382 // // if a.showSessionDialog {
383 // // a.showSessionDialog = false
384 // // }
385 // // if a.showCommandDialog {
386 // // a.showCommandDialog = false
387 // // }
388 // // if a.showFilepicker {
389 // // a.showFilepicker = false
390 // // a.filepicker.ToggleFilepicker(a.showFilepicker)
391 // // }
392 // // if a.showModelDialog {
393 // // a.showModelDialog = false
394 // // }
395 // // if a.showMultiArgumentsDialog {
396 // // a.showMultiArgumentsDialog = false
397 // // }
398 // return a, util.CmdHandler(dialogs.OpenDialogMsg{
399 // Model: quit.NewQuitDialog(),
400 // })
401 // case key.Matches(msg, keys.SwitchSession):
402 // if a.currentPage == page.ChatPage && !a.showPermissions && !a.showCommandDialog {
403 // // Load sessions and show the dialog
404 // sessions, err := a.app.Sessions.List(context.Background())
405 // if err != nil {
406 // return a, util.ReportError(err)
407 // }
408 // if len(sessions) == 0 {
409 // return a, util.ReportWarn("No sessions available")
410 // }
411 // a.sessionDialog.SetSessions(sessions)
412 // a.showSessionDialog = true
413 // return a, nil
414 // }
415 // return a, nil
416 // case key.Matches(msg, keys.Commands):
417 // if a.currentPage == page.ChatPage && !a.showPermissions && !a.showSessionDialog && !a.showThemeDialog && !a.showFilepicker {
418 // // Show commands dialog
419 // if len(a.commands) == 0 {
420 // return a, util.ReportWarn("No commands available")
421 // }
422 // a.commandDialog.SetCommands(a.commands)
423 // a.showCommandDialog = true
424 // return a, nil
425 // }
426 // return a, util.CmdHandler(dialogs.OpenDialogMsg{
427 // Model: commands.NewCommandDialog(),
428 // })
429 // case key.Matches(msg, keys.Models):
430 // if a.showModelDialog {
431 // a.showModelDialog = false
432 // return a, nil
433 // }
434 // if a.currentPage == page.ChatPage && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
435 // a.showModelDialog = true
436 // return a, nil
437 // }
438 // return a, nil
439 // case key.Matches(msg, keys.SwitchTheme):
440 // if !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
441 // // Show theme switcher dialog
442 // a.showThemeDialog = true
443 // // Theme list is dynamically loaded by the dialog component
444 // return a, a.themeDialog.Init()
445 // }
446 // return a, nil
447 // case key.Matches(msg, returnKey) || key.Matches(msg):
448 // if msg.String() == quitKey {
449 // if a.currentPage == page.LogsPage {
450 // return a, a.moveToPage(page.ChatPage)
451 // }
452 // } else if !a.filepicker.IsCWDFocused() {
453 // if a.showHelp {
454 // a.showHelp = !a.showHelp
455 // return a, nil
456 // }
457 // if a.showInitDialog {
458 // a.showInitDialog = false
459 // // Mark the project as initialized without running the command
460 // if err := config.MarkProjectInitialized(); err != nil {
461 // return a, util.ReportError(err)
462 // }
463 // return a, nil
464 // }
465 // if a.showFilepicker {
466 // a.showFilepicker = false
467 // a.filepicker.ToggleFilepicker(a.showFilepicker)
468 // return a, nil
469 // }
470 // if a.currentPage == page.LogsPage {
471 // return a, a.moveToPage(page.ChatPage)
472 // }
473 // }
474 // case key.Matches(msg, keys.Logs):
475 // return a, a.moveToPage(page.LogsPage)
476 // case key.Matches(msg, keys.Help):
477 // a.showHelp = !a.showHelp
478 // return a, nil
479 // case key.Matches(msg, helpEsc):
480 // if a.app.CoderAgent.IsBusy() {
481 // a.showHelp = !a.showHelp
482 // return a, nil
483 // }
484 // case key.Matches(msg, keys.Filepicker):
485 // a.showFilepicker = !a.showFilepicker
486 // a.filepicker.ToggleFilepicker(a.showFilepicker)
487 // return a, nil
488 // }
489 // default:
490 // u, dialogCmd := a.dialog.Update(msg)
491 // a.dialog = u.(dialogs.DialogCmp)
492 // cmds = append(cmds, dialogCmd)
493 // f, filepickerCmd := a.filepicker.Update(msg)
494 // a.filepicker = f.(dialog.FilepickerCmp)
495 // cmds = append(cmds, filepickerCmd)
496 // }
497
498 // if a.showFilepicker {
499 // f, filepickerCmd := a.filepicker.Update(msg)
500 // a.filepicker = f.(dialog.FilepickerCmp)
501 // cmds = append(cmds, filepickerCmd)
502 // // Only block key messages send all other messages down
503 // if _, ok := msg.(tea.KeyPressMsg); ok {
504 // return a, tea.Batch(cmds...)
505 // }
506 // }
507 //
508 // if a.showPermissions {
509 // d, permissionsCmd := a.permissions.Update(msg)
510 // a.permissions = d.(dialog.PermissionDialogCmp)
511 // cmds = append(cmds, permissionsCmd)
512 // // Only block key messages send all other messages down
513 // if _, ok := msg.(tea.KeyPressMsg); ok {
514 // return a, tea.Batch(cmds...)
515 // }
516 // }
517 //
518 // if a.showSessionDialog {
519 // d, sessionCmd := a.sessionDialog.Update(msg)
520 // a.sessionDialog = d.(dialog.SessionDialog)
521 // cmds = append(cmds, sessionCmd)
522 // // Only block key messages send all other messages down
523 // if _, ok := msg.(tea.KeyPressMsg); ok {
524 // return a, tea.Batch(cmds...)
525 // }
526 // }
527 //
528 // if a.showCommandDialog {
529 // d, commandCmd := a.commandDialog.Update(msg)
530 // a.commandDialog = d.(dialog.CommandDialog)
531 // cmds = append(cmds, commandCmd)
532 // // Only block key messages send all other messages down
533 // if _, ok := msg.(tea.KeyPressMsg); ok {
534 // return a, tea.Batch(cmds...)
535 // }
536 // }
537 //
538 // if a.showModelDialog {
539 // d, modelCmd := a.modelDialog.Update(msg)
540 // a.modelDialog = d.(dialog.ModelDialog)
541 // cmds = append(cmds, modelCmd)
542 // // Only block key messages send all other messages down
543 // if _, ok := msg.(tea.KeyPressMsg); ok {
544 // return a, tea.Batch(cmds...)
545 // }
546 // }
547 //
548 // if a.showInitDialog {
549 // d, initCmd := a.initDialog.Update(msg)
550 // a.initDialog = d.(dialog.InitDialogCmp)
551 // cmds = append(cmds, initCmd)
552 // // Only block key messages send all other messages down
553 // if _, ok := msg.(tea.KeyPressMsg); ok {
554 // return a, tea.Batch(cmds...)
555 // }
556 // }
557 //
558 // if a.showThemeDialog {
559 // d, themeCmd := a.themeDialog.Update(msg)
560 // a.themeDialog = d.(dialog.ThemeDialog)
561 // cmds = append(cmds, themeCmd)
562 // // Only block key messages send all other messages down
563 // if _, ok := msg.(tea.KeyPressMsg); ok {
564 // return a, tea.Batch(cmds...)
565 // }
566 }
567 //
568 s, _ := a.status.Update(msg)
569 a.status = s.(core.StatusCmp)
570 updated, cmd := a.pages[a.currentPage].Update(msg)
571 a.pages[a.currentPage] = updated.(util.Model)
572 cmds = append(cmds, cmd)
573 return a, tea.Batch(cmds...)
574}
575
576func (a *appModel) handleWindowResize(msg tea.WindowSizeMsg) tea.Cmd {
577 var cmds []tea.Cmd
578 msg.Height -= 1 // Make space for the status bar
579 a.width, a.height = msg.Width, msg.Height
580
581 // Update status bar
582 s, cmd := a.status.Update(msg)
583 a.status = s.(core.StatusCmp)
584 cmds = append(cmds, cmd)
585
586 // Update the current page
587 updated, cmd := a.pages[a.currentPage].Update(msg)
588 a.pages[a.currentPage] = updated.(util.Model)
589 cmds = append(cmds, cmd)
590
591 // Update the dialogs
592 dialog, cmd := a.dialog.Update(msg)
593 a.dialog = dialog.(dialogs.DialogCmp)
594 cmds = append(cmds, cmd)
595
596 return tea.Batch(cmds...)
597}
598
599func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
600 switch {
601 // dialogs
602 case key.Matches(msg, a.keyMap.Quit):
603 return util.CmdHandler(dialogs.OpenDialogMsg{
604 Model: quit.NewQuitDialog(),
605 })
606
607 case key.Matches(msg, a.keyMap.Commands):
608 return util.CmdHandler(dialogs.OpenDialogMsg{
609 Model: commands.NewCommandDialog(),
610 })
611
612 // Page navigation
613 case key.Matches(msg, a.keyMap.Logs):
614 return a.moveToPage(page.LogsPage)
615
616 default:
617 if a.dialog.HasDialogs() {
618 u, dialogCmd := a.dialog.Update(msg)
619 a.dialog = u.(dialogs.DialogCmp)
620 return dialogCmd
621 } else {
622 updated, cmd := a.pages[a.currentPage].Update(msg)
623 a.pages[a.currentPage] = updated.(util.Model)
624 return cmd
625 }
626 }
627}
628
629// RegisterCommand adds a command to the command dialog
630// func (a *appModel) RegisterCommand(cmd dialog.Command) {
631// a.commands = append(a.commands, cmd)
632// }
633
634func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
635 if a.app.CoderAgent.IsBusy() {
636 // For now we don't move to any page if the agent is busy
637 return util.ReportWarn("Agent is busy, please wait...")
638 }
639
640 var cmds []tea.Cmd
641 if _, ok := a.loadedPages[pageID]; !ok {
642 cmd := a.pages[pageID].Init()
643 cmds = append(cmds, cmd)
644 a.loadedPages[pageID] = true
645 }
646 a.previousPage = a.currentPage
647 a.currentPage = pageID
648 if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
649 cmd := sizable.SetSize(a.width, a.height)
650 cmds = append(cmds, cmd)
651 }
652
653 return tea.Batch(cmds...)
654}
655
656func (a *appModel) View() tea.View {
657 pageView := a.pages[a.currentPage].View()
658 components := []string{
659 pageView.String(),
660 }
661 components = append(components, a.status.View().String())
662
663 appView := lipgloss.JoinVertical(lipgloss.Top, components...)
664
665 // if a.showPermissions {
666 // overlay := a.permissions.View().String()
667 // row := lipgloss.Height(appView) / 2
668 // row -= lipgloss.Height(overlay) / 2
669 // col := lipgloss.Width(appView) / 2
670 // col -= lipgloss.Width(overlay) / 2
671 // appView = layout.PlaceOverlay(
672 // col,
673 // row,
674 // overlay,
675 // appView,
676 // true,
677 // )
678 // }
679 //
680 // if a.showFilepicker {
681 // overlay := a.filepicker.View().String()
682 // row := lipgloss.Height(appView) / 2
683 // row -= lipgloss.Height(overlay) / 2
684 // col := lipgloss.Width(appView) / 2
685 // col -= lipgloss.Width(overlay) / 2
686 // appView = layout.PlaceOverlay(
687 // col,
688 // row,
689 // overlay,
690 // appView,
691 // true,
692 // )
693 // }
694 //
695 // // Show compacting status overlay
696 // if a.isCompacting {
697 // t := theme.CurrentTheme()
698 // style := lipgloss.NewStyle().
699 // Border(lipgloss.RoundedBorder()).
700 // BorderForeground(t.BorderFocused()).
701 // BorderBackground(t.Background()).
702 // Padding(1, 2).
703 // Background(t.Background()).
704 // Foreground(t.Text())
705 //
706 // overlay := style.Render("Summarizing\n" + a.compactingMessage)
707 // row := lipgloss.Height(appView) / 2
708 // row -= lipgloss.Height(overlay) / 2
709 // col := lipgloss.Width(appView) / 2
710 // col -= lipgloss.Width(overlay) / 2
711 // appView = layout.PlaceOverlay(
712 // col,
713 // row,
714 // overlay,
715 // appView,
716 // true,
717 // )
718 // }
719 //
720 // if a.showHelp {
721 // bindings := layout.KeyMapToSlice(a.keymap)
722 // if p, ok := a.pages[a.currentPage].(layout.Bindings); ok {
723 // bindings = append(bindings, p.BindingKeys()...)
724 // }
725 // if a.showPermissions {
726 // bindings = append(bindings, a.permissions.BindingKeys()...)
727 // }
728 // if a.currentPage == page.LogsPage {
729 // // bindings = append(bindings, logsKeyReturnKey)
730 // }
731 // if !a.app.CoderAgent.IsBusy() {
732 // // bindings = append(bindings, helpEsc)
733 // }
734 //
735 // a.help.SetBindings(bindings)
736 //
737 // overlay := a.help.View().String()
738 // row := lipgloss.Height(appView) / 2
739 // row -= lipgloss.Height(overlay) / 2
740 // col := lipgloss.Width(appView) / 2
741 // col -= lipgloss.Width(overlay) / 2
742 // appView = layout.PlaceOverlay(
743 // col,
744 // row,
745 // overlay,
746 // appView,
747 // true,
748 // )
749 // }
750 //
751 // if a.showSessionDialog {
752 // overlay := a.sessionDialog.View().String()
753 // row := lipgloss.Height(appView) / 2
754 // row -= lipgloss.Height(overlay) / 2
755 // col := lipgloss.Width(appView) / 2
756 // col -= lipgloss.Width(overlay) / 2
757 // appView = layout.PlaceOverlay(
758 // col,
759 // row,
760 // overlay,
761 // appView,
762 // true,
763 // )
764 // }
765 //
766 // if a.showModelDialog {
767 // overlay := a.modelDialog.View().String()
768 // row := lipgloss.Height(appView) / 2
769 // row -= lipgloss.Height(overlay) / 2
770 // col := lipgloss.Width(appView) / 2
771 // col -= lipgloss.Width(overlay) / 2
772 // appView = layout.PlaceOverlay(
773 // col,
774 // row,
775 // overlay,
776 // appView,
777 // true,
778 // )
779 // }
780 //
781 // if a.showCommandDialog {
782 // overlay := a.commandDialog.View().String()
783 // row := lipgloss.Height(appView) / 2
784 // row -= lipgloss.Height(overlay) / 2
785 // col := lipgloss.Width(appView) / 2
786 // col -= lipgloss.Width(overlay) / 2
787 // appView = layout.PlaceOverlay(
788 // col,
789 // row,
790 // overlay,
791 // appView,
792 // true,
793 // )
794 // }
795 //
796 // if a.showInitDialog {
797 // overlay := a.initDialog.View()
798 // appView = layout.PlaceOverlay(
799 // a.width/2-lipgloss.Width(overlay)/2,
800 // a.height/2-lipgloss.Height(overlay)/2,
801 // overlay,
802 // appView,
803 // true,
804 // )
805 // }
806 //
807 // if a.showThemeDialog {
808 // overlay := a.themeDialog.View().String()
809 // row := lipgloss.Height(appView) / 2
810 // row -= lipgloss.Height(overlay) / 2
811 // col := lipgloss.Width(appView) / 2
812 // col -= lipgloss.Width(overlay) / 2
813 // appView = layout.PlaceOverlay(
814 // col,
815 // row,
816 // overlay,
817 // appView,
818 // true,
819 // )
820 // }
821 //
822 // if a.showMultiArgumentsDialog {
823 // overlay := a.multiArgumentsDialog.View()
824 // row := lipgloss.Height(appView) / 2
825 // row -= lipgloss.Height(overlay) / 2
826 // col := lipgloss.Width(appView) / 2
827 // col -= lipgloss.Width(overlay) / 2
828 // appView = layout.PlaceOverlay(
829 // col,
830 // row,
831 // overlay,
832 // appView,
833 // true,
834 // )
835 // }
836 t := theme.CurrentTheme()
837 if a.dialog.HasDialogs() {
838 layers := append(
839 []*lipgloss.Layer{
840 lipgloss.NewLayer(appView),
841 },
842 a.dialog.GetLayers()...,
843 )
844 canvas := lipgloss.NewCanvas(
845 layers...,
846 )
847 view := tea.NewView(canvas.Render())
848 activeView := a.dialog.ActiveView()
849 view.SetBackgroundColor(t.Background())
850 view.SetCursor(activeView.Cursor())
851 return view
852 }
853
854 view := tea.NewView(appView)
855 view.SetCursor(pageView.Cursor())
856 view.SetBackgroundColor(t.Background())
857 return view
858}
859
860func New(app *app.App) tea.Model {
861 startPage := page.ChatPage
862 model := &appModel{
863 currentPage: startPage,
864 app: app,
865 status: core.NewStatusCmp(app.LSPClients),
866 loadedPages: make(map[page.PageID]bool),
867 keyMap: DefaultKeyMap(),
868
869 // help: dialog.NewHelpCmp(),
870 // sessionDialog: dialog.NewSessionDialogCmp(),
871 // commandDialog: dialog.NewCommandDialogCmp(),
872 // modelDialog: dialog.NewModelDialogCmp(),
873 // permissions: dialog.NewPermissionDialogCmp(),
874 // initDialog: dialog.NewInitDialogCmp(),
875 // themeDialog: dialog.NewThemeDialogCmp(),
876 // commands: []dialog.Command{},
877 pages: map[page.PageID]util.Model{
878 page.ChatPage: page.NewChatPage(app),
879 page.LogsPage: page.NewLogsPage(),
880 },
881 // filepicker: dialog.NewFilepickerCmp(app),
882
883 // New dialog
884 dialog: dialogs.NewDialogCmp(),
885 }
886
887 // model.RegisterCommand(dialog.Command{
888 // ID: "init",
889 // Title: "Initialize Project",
890 // Description: "Create/Update the OpenCode.md memory file",
891 // Handler: func(cmd dialog.Command) tea.Cmd {
892 // prompt := `Please analyze this codebase and create a OpenCode.md file containing:
893 // 1. Build/lint/test commands - especially for running a single test
894 // 2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
895 //
896 // The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
897 // If there's already a opencode.md, improve it.
898 // If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.`
899 // return tea.Batch(
900 // util.CmdHandler(chat.SendMsg{
901 // Text: prompt,
902 // }),
903 // )
904 // },
905 // })
906 //
907 // model.RegisterCommand(dialog.Command{
908 // ID: "compact",
909 // Title: "Compact Session",
910 // Description: "Summarize the current session and create a new one with the summary",
911 // Handler: func(cmd dialog.Command) tea.Cmd {
912 // return func() tea.Msg {
913 // return startCompactSessionMsg{}
914 // }
915 // },
916 // })
917 // // Load custom commands
918 // customCommands, err := dialog.LoadCustomCommands()
919 // if err != nil {
920 // logging.Warn("Failed to load custom commands", "error", err)
921 // } else {
922 // for _, cmd := range customCommands {
923 // model.RegisterCommand(cmd)
924 // }
925 // }
926
927 return model
928}