tui.go

  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}