@@ -404,6 +404,12 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
404404 if m.completionsOpen {
405405 m.completions.SetFiles(msg.Files)
406406 }
407+ default:
408+ if m.dialog.HasDialogs() {
409+ if cmd := m.handleDialogMsg(msg); cmd != nil {
410+ cmds = append(cmds, cmd)
411+ }
412+ }
407413 }
408414
409415 // This logic gets triggered on any message type, but should it?
@@ -690,6 +696,93 @@ func (m *UI) handleChildSessionMessage(event pubsub.Event[message.Message]) tea.
690696 return tea.Batch(cmds...)
691697}
692698
699+func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
700+ var cmds []tea.Cmd
701+ action := m.dialog.Update(msg)
702+ if action == nil {
703+ return tea.Batch(cmds...)
704+ }
705+
706+ switch msg := action.(type) {
707+ // Generic dialog messages
708+ case dialog.ActionClose:
709+ m.dialog.CloseFrontDialog()
710+ if m.focus == uiFocusEditor {
711+ cmds = append(cmds, m.textarea.Focus())
712+ }
713+ case dialog.ActionCmd:
714+ if msg.Cmd != nil {
715+ cmds = append(cmds, msg.Cmd)
716+ }
717+
718+ // Session dialog messages
719+ case dialog.ActionSelectSession:
720+ m.dialog.CloseDialog(dialog.SessionsID)
721+ cmds = append(cmds, m.loadSession(msg.Session.ID))
722+
723+ // Open dialog message
724+ case dialog.ActionOpenDialog:
725+ m.dialog.CloseDialog(dialog.CommandsID)
726+ if cmd := m.openDialog(msg.DialogID); cmd != nil {
727+ cmds = append(cmds, cmd)
728+ }
729+
730+ // Command dialog messages
731+ case dialog.ActionToggleYoloMode:
732+ yolo := !m.com.App.Permissions.SkipRequests()
733+ m.com.App.Permissions.SetSkipRequests(yolo)
734+ m.setEditorPrompt(yolo)
735+ m.dialog.CloseDialog(dialog.CommandsID)
736+ case dialog.ActionNewSession:
737+ if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
738+ cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before starting a new session..."))
739+ break
740+ }
741+ m.newSession()
742+ m.dialog.CloseDialog(dialog.CommandsID)
743+ case dialog.ActionSummarize:
744+ if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
745+ cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before summarizing session..."))
746+ break
747+ }
748+ err := m.com.App.AgentCoordinator.Summarize(context.Background(), msg.SessionID)
749+ if err != nil {
750+ cmds = append(cmds, uiutil.ReportError(err))
751+ }
752+ case dialog.ActionToggleHelp:
753+ m.status.ToggleHelp()
754+ m.dialog.CloseDialog(dialog.CommandsID)
755+ case dialog.ActionQuit:
756+ cmds = append(cmds, tea.Quit)
757+ case dialog.ActionSelectModel:
758+ if m.com.App.AgentCoordinator.IsBusy() {
759+ cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
760+ break
761+ }
762+
763+ // TODO: Validate model API and authentication here?
764+
765+ cfg := m.com.Config()
766+ if cfg == nil {
767+ cmds = append(cmds, uiutil.ReportError(errors.New("configuration not found")))
768+ break
769+ }
770+
771+ if err := cfg.UpdatePreferredModel(msg.ModelType, msg.Model); err != nil {
772+ cmds = append(cmds, uiutil.ReportError(err))
773+ }
774+
775+ // XXX: Should this be in a separate goroutine?
776+ go m.com.App.UpdateAgentModel(context.TODO())
777+
778+ modelMsg := fmt.Sprintf("%s model changed to %s", msg.ModelType, msg.Model.Model)
779+ cmds = append(cmds, uiutil.ReportInfo(modelMsg))
780+ m.dialog.CloseDialog(dialog.ModelsID)
781+ }
782+
783+ return tea.Batch(cmds...)
784+}
785+
693786func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
694787 var cmds []tea.Cmd
695788
@@ -729,96 +822,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
729822
730823 // Route all messages to dialog if one is open.
731824 if m.dialog.HasDialogs() {
732− msg := m.dialog.Update(msg)
733− if msg == nil {
734− return tea.Batch(cmds...)
735− }
736−
737− switch msg := msg.(type) {
738− // Generic dialog messages
739− case dialog.CloseMsg:
740− m.dialog.CloseFrontDialog()
741− if m.focus == uiFocusEditor {
742− cmds = append(cmds, m.textarea.Focus())
743− }
744−
745− // Session dialog messages
746− case dialog.SessionSelectedMsg:
747− m.dialog.CloseDialog(dialog.SessionsID)
748− cmds = append(cmds, m.loadSession(msg.Session.ID))
749−
750− // Open dialog message
751− case dialog.OpenDialogMsg:
752− switch msg.DialogID {
753− case dialog.SessionsID:
754− if cmd := m.openSessionsDialog(); cmd != nil {
755− cmds = append(cmds, cmd)
756− }
757− case dialog.ModelsID:
758− if cmd := m.openModelsDialog(); cmd != nil {
759− cmds = append(cmds, cmd)
760− }
761− default:
762− // Unknown dialog
763− break
764− }
765−
766− m.dialog.CloseDialog(dialog.CommandsID)
767−
768− // Command dialog messages
769− case dialog.ToggleYoloModeMsg:
770− yolo := !m.com.App.Permissions.SkipRequests()
771− m.com.App.Permissions.SetSkipRequests(yolo)
772− m.setEditorPrompt(yolo)
773− m.dialog.CloseDialog(dialog.CommandsID)
774− case dialog.NewSessionsMsg:
775− if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
776− cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before starting a new session..."))
777− break
778− }
779− m.newSession()
780− m.dialog.CloseDialog(dialog.CommandsID)
781− case dialog.CompactMsg:
782− if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
783− cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before summarizing session..."))
784− break
785− }
786− err := m.com.App.AgentCoordinator.Summarize(context.Background(), msg.SessionID)
787− if err != nil {
788− cmds = append(cmds, uiutil.ReportError(err))
789− }
790− case dialog.ToggleHelpMsg:
791− m.status.ToggleHelp()
792− m.dialog.CloseDialog(dialog.CommandsID)
793− case dialog.QuitMsg:
794− cmds = append(cmds, tea.Quit)
795− case dialog.ModelSelectedMsg:
796− if m.com.App.AgentCoordinator.IsBusy() {
797− cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
798− break
799− }
800−
801− // TODO: Validate model API and authentication here?
802−
803− cfg := m.com.Config()
804− if cfg == nil {
805− cmds = append(cmds, uiutil.ReportError(errors.New("configuration not found")))
806− break
807− }
808−
809− if err := cfg.UpdatePreferredModel(msg.ModelType, msg.Model); err != nil {
810− cmds = append(cmds, uiutil.ReportError(err))
811− }
812−
813− // XXX: Should this be in a separate goroutine?
814− go m.com.App.UpdateAgentModel(context.TODO())
815−
816− modelMsg := fmt.Sprintf("%s model changed to %s", msg.ModelType, msg.Model.Model)
817− cmds = append(cmds, uiutil.ReportInfo(modelMsg))
818− m.dialog.CloseDialog(dialog.ModelsID)
819− }
820−
821− return tea.Batch(cmds...)
825+ return m.handleDialogMsg(msg)
822826 }
823827
824828 switch m.state {
@@ -1035,7 +1039,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
10351039}
10361040
10371041// Draw implements [uv.Drawable] and draws the UI model.
1038−func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) {
1042+func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
10391043 layout := m.generateLayout(area.Dx(), area.Dy())
10401044
10411045 if m.layout != layout {
@@ -1132,36 +1136,20 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) {
11321136 })
11331137 }
11341138
1135− // This needs to come last to overlay on top of everything
1139+ // This needs to come last to overlay on top of everything. We always pass
1140+ // the full screen bounds because the dialogs will position themselves
1141+ // accordingly.
11361142 if m.dialog.HasDialogs() {
1137− m.dialog.Draw(scr, area)
1143+ return m.dialog.Draw(scr, scr.Bounds())
11381144 }
1139−}
11401145
1141−// Cursor returns the cursor position and properties for the UI model. It
1142−// returns nil if the cursor should not be shown.
1143−func (m *UI) Cursor() *tea.Cursor {
1144− if m.layout.editor.Dy() <= 0 {
1145− // Don't show cursor if editor is not visible
1146− return nil
1147− }
1148− if m.dialog.HasDialogs() {
1149− if front := m.dialog.DialogLast(); front != nil {
1150− c, ok := front.(uiutil.Cursor)
1151− if ok {
1152− cur := c.Cursor()
1153− if cur != nil {
1154− pos := m.dialog.CenterPosition(m.layout.area, front.ID())
1155− cur.X += pos.Min.X
1156− cur.Y += pos.Min.Y
1157− return cur
1158− }
1159− }
1160− }
1161− return nil
1162− }
11631146 switch m.focus {
11641147 case uiFocusEditor:
1148+ if m.layout.editor.Dy() <= 0 {
1149+ // Don't show cursor if editor is not visible
1150+ return nil
1151+ }
1152+
11651153 if m.textarea.Focused() {
11661154 cur := m.textarea.Cursor()
11671155 cur.X++ // Adjust for app margins
@@ -1181,11 +1169,10 @@ func (m *UI) View() tea.View {
11811169 var v tea.View
11821170 v.AltScreen = true
11831171 v.BackgroundColor = m.com.Styles.Background
1184− v.Cursor = m.Cursor()
11851172 v.MouseMode = tea.MouseModeCellMotion
11861173
11871174 canvas := uv.NewScreenBuffer(m.width, m.height)
1188− m.Draw(canvas, canvas.Bounds())
1175+ v.Cursor = m.Draw(canvas, canvas.Bounds())
11891176
11901177 content := strings.ReplaceAll(canvas.Render(), "\r\n", "\n") // normalize newlines
11911178 contentLines := strings.Split(content, "\n")
@@ -1813,6 +1800,33 @@ func (m *UI) sendMessage(content string, attachments []message.Attachment) tea.C
18131800 return tea.Batch(cmds...)
18141801}
18151802
1803+// openDialog opens a dialog by its ID.
1804+func (m *UI) openDialog(id string) tea.Cmd {
1805+ var cmds []tea.Cmd
1806+ switch id {
1807+ case dialog.SessionsID:
1808+ if cmd := m.openSessionsDialog(); cmd != nil {
1809+ cmds = append(cmds, cmd)
1810+ }
1811+ case dialog.ModelsID:
1812+ if cmd := m.openModelsDialog(); cmd != nil {
1813+ cmds = append(cmds, cmd)
1814+ }
1815+ case dialog.CommandsID:
1816+ if cmd := m.openCommandsDialog(); cmd != nil {
1817+ cmds = append(cmds, cmd)
1818+ }
1819+ case dialog.QuitID:
1820+ if cmd := m.openQuitDialog(); cmd != nil {
1821+ cmds = append(cmds, cmd)
1822+ }
1823+ default:
1824+ // Unknown dialog
1825+ break
1826+ }
1827+ return tea.Batch(cmds...)
1828+}
1829+
18161830// openQuitDialog opens the quit confirmation dialog.
18171831func (m *UI) openQuitDialog() tea.Cmd {
18181832 if m.dialog.ContainsDialog(dialog.QuitID) {
@@ -1839,7 +1853,6 @@ func (m *UI) openModelsDialog() tea.Cmd {
18391853 return uiutil.ReportError(err)
18401854 }
18411855
1842− modelsDialog.SetSize(min(60, m.width-8), 30)
18431856 m.dialog.OpenDialog(modelsDialog)
18441857
18451858 return nil
@@ -1863,8 +1876,6 @@ func (m *UI) openCommandsDialog() tea.Cmd {
18631876 return uiutil.ReportError(err)
18641877 }
18651878
1866− // TODO: Get. Rid. Of. Magic numbers!
1867− commands.SetSize(min(120, m.width-8), 30)
18681879 m.dialog.OpenDialog(commands)
18691880
18701881 return nil
@@ -1890,8 +1901,6 @@ func (m *UI) openSessionsDialog() tea.Cmd {
18901901 return uiutil.ReportError(err)
18911902 }
18921903
1893− // TODO: Get. Rid. Of. Magic numbers!
1894− dialog.SetSize(min(120, m.width-8), 30)
18951904 m.dialog.OpenDialog(dialog)
18961905
18971906 return nil
@@ -1915,6 +1924,10 @@ func (m *UI) newSession() {
19151924
19161925// handlePasteMsg handles a paste message.
19171926func (m *UI) handlePasteMsg(msg tea.PasteMsg) tea.Cmd {
1927+ if m.dialog.HasDialogs() {
1928+ return m.handleDialogMsg(msg)
1929+ }
1930+
19181931 if m.focus != uiFocusEditor {
19191932 return nil
19201933 }