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