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
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}