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