@@ -189,6 +189,16 @@ type UI struct {
190 // detailsOpen tracks whether the details panel is open (in compact mode)
190 // detailsOpen tracks whether the details panel is open (in compact mode)
191 detailsOpen bool
191 detailsOpen bool
195+ focusedPillSection pillSection
200+ todoSpinner spinner.Model
194// New creates a new instance of the [UI] model.
204// New creates a new instance of the [UI] model.
@@ -212,6 +222,11 @@ func New(com *common.Common) *UI {
212 com.Styles.Completions.Match,
222 com.Styles.Completions.Match,
225+ todoSpinner := spinner.New(
226+ spinner.WithSpinner(spinner.MiniDot),
227+ spinner.WithStyle(com.Styles.Pills.TodoSpinner),
215 // Attachments component
230 // Attachments component
216 attachments := attachments.New(
231 attachments := attachments.New(
217 attachments.NewRenderer(
232 attachments.NewRenderer(
@@ -237,6 +252,7 @@ func New(com *common.Common) *UI {
237 chat: ch,
252 chat: ch,
238 completions: comp,
253 completions: comp,
239 attachments: attachments,
254 attachments: attachments,
255+ todoSpinner: todoSpinner,
242 status := NewStatus(com, ui)
258 status := NewStatus(com, ui)
@@ -312,6 +328,13 @@ func (m *UI) loadMCPrompts() tea.Cmd {
312// Update handles updates to the UI model.
328// Update handles updates to the UI model.
313func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
329func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
314 var cmds []tea.Cmd
330 var cmds []tea.Cmd
331+ if m.hasSession() && m.isAgentBusy() {
332+ queueSize := m.com.App.AgentCoordinator.QueuedPrompts(m.session.ID)
333+ if queueSize != m.promptQueue {
334+ m.promptQueue = queueSize
335+ m.updateLayoutAndSize()
315 switch msg := msg.(type) {
338 switch msg := msg.(type) {
316 case tea.EnvMsg:
339 case tea.EnvMsg:
317 // Is this Windows Terminal?
340 // Is this Windows Terminal?
@@ -333,6 +356,14 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
333 if cmd := m.setSessionMessages(msgs); cmd != nil {
356 if cmd := m.setSessionMessages(msgs); cmd != nil {
334 cmds = append(cmds, cmd)
357 cmds = append(cmds, cmd)
359+ if hasInProgressTodo(m.session.Todos) {
360+ // only start spinner if there is an in-progress todo
361+ if m.isAgentBusy() {
362+ m.todoIsSpinning = true
363+ cmds = append(cmds, m.todoSpinner.Tick)
365+ m.updateLayoutAndSize()
337 case sendMessageMsg:
368 case sendMessageMsg:
338 cmds = append(cmds, m.sendMessage(msg.Content, msg.Attachments...))
369 cmds = append(cmds, m.sendMessage(msg.Content, msg.Attachments...))
@@ -363,6 +394,16 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
363 case closeDialogMsg:
394 case closeDialogMsg:
364 m.dialog.CloseFrontDialog()
395 m.dialog.CloseFrontDialog()
397+ case pubsub.Event[session.Session]:
398+ if m.session != nil && msg.Payload.ID == m.session.ID {
399+ prevHasInProgress := hasInProgressTodo(m.session.Todos)
400+ m.session = &msg.Payload
401+ if !prevHasInProgress && hasInProgressTodo(m.session.Todos) {
402+ m.todoIsSpinning = true
403+ cmds = append(cmds, m.todoSpinner.Tick)
404+ m.updateLayoutAndSize()
366 case pubsub.Event[message.Message]:
407 case pubsub.Event[message.Message]:
367 // Check if this is a child session message for an agent tool.
408 // Check if this is a child session message for an agent tool.
368 if m.session == nil {
409 if m.session == nil {
@@ -383,6 +424,17 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
383 case pubsub.DeletedEvent:
424 case pubsub.DeletedEvent:
384 m.chat.RemoveMessage(msg.Payload.ID)
425 m.chat.RemoveMessage(msg.Payload.ID)
427+ // start the spinner if there is a new message
428+ if hasInProgressTodo(m.session.Todos) && m.isAgentBusy() && !m.todoIsSpinning {
429+ m.todoIsSpinning = true
430+ cmds = append(cmds, m.todoSpinner.Tick)
432+ // stop the spinner if the agent is not busy anymore
433+ if m.todoIsSpinning && !m.isAgentBusy() {
434+ m.todoIsSpinning = false
436+ // there is a number of things that could change the pills here so we want to re-render
386 case pubsub.Event[history.File]:
438 case pubsub.Event[history.File]:
387 cmds = append(cmds, m.handleFileEvent(msg.Payload))
439 cmds = append(cmds, m.handleFileEvent(msg.Payload))
388 case pubsub.Event[app.LSPEvent]:
440 case pubsub.Event[app.LSPEvent]:
@@ -524,6 +576,14 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
524 cmds = append(cmds, cmd)
576 cmds = append(cmds, cmd)
579+ if m.state == uiChat && m.hasSession() && hasInProgressTodo(m.session.Todos) && m.todoIsSpinning {
581+ m.todoSpinner, cmd = m.todoSpinner.Update(msg)
584+ cmds = append(cmds, cmd)
528 case tea.KeyPressMsg:
588 case tea.KeyPressMsg:
529 if cmd := m.handleKeyPressMsg(msg); cmd != nil {
589 if cmd := m.handleKeyPressMsg(msg); cmd != nil {
@@ -573,7 +633,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
573 case uiFocusMain:
633 case uiFocusMain:
574 case uiFocusEditor:
634 case uiFocusEditor:
575 // Textarea placeholder logic
635 // Textarea placeholder logic
576− if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
636+ if m.isAgentBusy() {
577 m.textarea.Placeholder = m.workingPlaceholder
637 m.textarea.Placeholder = m.workingPlaceholder
579 m.textarea.Placeholder = m.readyPlaceholder
639 m.textarea.Placeholder = m.readyPlaceholder
@@ -945,14 +1005,14 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
945 m.setEditorPrompt(yolo)
1005 m.setEditorPrompt(yolo)
946 m.dialog.CloseDialog(dialog.CommandsID)
1006 m.dialog.CloseDialog(dialog.CommandsID)
947 case dialog.ActionNewSession:
1007 case dialog.ActionNewSession:
948− if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
1008+ if m.isAgentBusy() {
949 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before starting a new session..."))
1009 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before starting a new session..."))
952 m.newSession()
1012 m.newSession()
953 m.dialog.CloseDialog(dialog.CommandsID)
1013 m.dialog.CloseDialog(dialog.CommandsID)
954 case dialog.ActionSummarize:
1014 case dialog.ActionSummarize:
955− if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
1015+ if m.isAgentBusy() {
956 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before summarizing session..."))
1016 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before summarizing session..."))
@@ -968,7 +1028,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
968 m.status.ToggleHelp()
1028 m.status.ToggleHelp()
969 m.dialog.CloseDialog(dialog.CommandsID)
1029 m.dialog.CloseDialog(dialog.CommandsID)
970 case dialog.ActionExternalEditor:
1030 case dialog.ActionExternalEditor:
971− if m.session != nil && m.com.App.AgentCoordinator.IsSessionBusy(m.session.ID) {
1031+ if m.isAgentBusy() {
972 cmds = append(cmds, uiutil.ReportWarn("Agent is working, please wait..."))
1032 cmds = append(cmds, uiutil.ReportWarn("Agent is working, please wait..."))
@@ -978,7 +1038,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
978 cmds = append(cmds, m.toggleCompactMode())
1038 cmds = append(cmds, m.toggleCompactMode())
979 m.dialog.CloseDialog(dialog.CommandsID)
1039 m.dialog.CloseDialog(dialog.CommandsID)
980 case dialog.ActionToggleThinking:
1040 case dialog.ActionToggleThinking:
981− if m.com.App.AgentCoordinator.IsBusy() {
1041+ if m.isAgentBusy() {
982 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
1042 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
@@ -1010,14 +1070,14 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
1010 case dialog.ActionQuit:
1070 case dialog.ActionQuit:
1011 cmds = append(cmds, tea.Quit)
1071 cmds = append(cmds, tea.Quit)
1012 case dialog.ActionInitializeProject:
1072 case dialog.ActionInitializeProject:
1013− if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
1073+ if m.isAgentBusy() {
1014 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before summarizing session..."))
1074 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before summarizing session..."))
1017 cmds = append(cmds, m.initializeProject())
1077 cmds = append(cmds, m.initializeProject())
1019 case dialog.ActionSelectModel:
1079 case dialog.ActionSelectModel:
1020− if m.com.App.AgentCoordinator.IsBusy() {
1080+ if m.isAgentBusy() {
1021 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
1081 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
@@ -1063,7 +1123,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
1063 m.dialog.CloseDialog(dialog.OAuthID)
1123 m.dialog.CloseDialog(dialog.OAuthID)
1064 m.dialog.CloseDialog(dialog.ModelsID)
1124 m.dialog.CloseDialog(dialog.ModelsID)
1065 case dialog.ActionSelectReasoningEffort:
1125 case dialog.ActionSelectReasoningEffort:
1066− if m.com.App.AgentCoordinator.IsBusy() {
1126+ if m.isAgentBusy() {
1067 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
1127 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
@@ -1217,6 +1277,27 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
1217 m.detailsOpen = !m.detailsOpen
1277 m.detailsOpen = !m.detailsOpen
1218 m.updateLayoutAndSize()
1278 m.updateLayoutAndSize()
1219 return true
1279 return true
1280+ case key.Matches(msg, m.keyMap.Chat.TogglePills):
1281+ if m.state == uiChat && m.hasSession() {
1282+ if cmd := m.togglePillsExpanded(); cmd != nil {
1283+ cmds = append(cmds, cmd)
1287+ case key.Matches(msg, m.keyMap.Chat.PillLeft):
1288+ if m.state == uiChat && m.hasSession() && m.pillsExpanded {
1289+ if cmd := m.switchPillSection(-1); cmd != nil {
1290+ cmds = append(cmds, cmd)
1294+ case key.Matches(msg, m.keyMap.Chat.PillRight):
1295+ if m.state == uiChat && m.hasSession() && m.pillsExpanded {
1296+ if cmd := m.switchPillSection(1); cmd != nil {
1297+ cmds = append(cmds, cmd)
1221 return false
1302 return false
@@ -1237,7 +1318,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
1238 // Handle cancel key when agent is busy.
1319 // Handle cancel key when agent is busy.
1239 if key.Matches(msg, m.keyMap.Chat.Cancel) {
1320 if key.Matches(msg, m.keyMap.Chat.Cancel) {
1240− if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
1321+ if m.isAgentBusy() {
1241 if cmd := m.cancelAgent(); cmd != nil {
1322 if cmd := m.cancelAgent(); cmd != nil {
1242 cmds = append(cmds, cmd)
1323 cmds = append(cmds, cmd)
@@ -1309,10 +1390,10 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
1310 return m.sendMessage(value, attachments...)
1391 return m.sendMessage(value, attachments...)
1311 case key.Matches(msg, m.keyMap.Chat.NewSession):
1392 case key.Matches(msg, m.keyMap.Chat.NewSession):
1312− if m.session == nil || m.session.ID == "" {
1393+ if !m.hasSession() {
1315− if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
1396+ if m.isAgentBusy() {
1316 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before starting a new session..."))
1397 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before starting a new session..."))
@@ -1323,7 +1404,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
1323 m.chat.Focus()
1404 m.chat.Focus()
1324 m.chat.SetSelected(m.chat.Len() - 1)
1405 m.chat.SetSelected(m.chat.Len() - 1)
1325 case key.Matches(msg, m.keyMap.Editor.OpenEditor):
1406 case key.Matches(msg, m.keyMap.Editor.OpenEditor):
1326− if m.session != nil && m.com.App.AgentCoordinator.IsSessionBusy(m.session.ID) {
1407+ if m.isAgentBusy() {
1327 cmds = append(cmds, uiutil.ReportWarn("Agent is working, please wait..."))
1408 cmds = append(cmds, uiutil.ReportWarn("Agent is working, please wait..."))
@@ -1518,6 +1599,9 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
1520 m.chat.Draw(scr, layout.main)
1601 m.chat.Draw(scr, layout.main)
1602+ if layout.pills.Dy() > 0 && m.pillsView != "" {
1603+ uv.NewStyledString(m.pillsView).Draw(scr, layout.pills)
1522 editorWidth := scr.Bounds().Dx()
1606 editorWidth := scr.Bounds().Dx()
1523 if !m.isCompact {
1607 if !m.isCompact {
@@ -1617,7 +1701,7 @@ func (m *UI) View() tea.View {
1617 content = strings.Join(contentLines, "\n")
1701 content = strings.Join(contentLines, "\n")
1619 v.Content = content
1703 v.Content = content
1620− if m.sendProgressBar && m.com.App != nil && m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
1704+ if m.sendProgressBar && m.isAgentBusy() {
1621 // HACK: use a random percentage to prevent ghostty from hiding it
1705 // HACK: use a random percentage to prevent ghostty from hiding it
1622 // after a timeout.
1706 // after a timeout.
1623 v.ProgressBar = tea.NewProgressBar(tea.ProgressBarIndeterminate, rand.Intn(100))
1707 v.ProgressBar = tea.NewProgressBar(tea.ProgressBarIndeterminate, rand.Intn(100))
@@ -1641,7 +1725,7 @@ func (m *UI) ShortHelp() []key.Binding {
1641 binds = append(binds, k.Quit)
1725 binds = append(binds, k.Quit)
1642 case uiChat:
1726 case uiChat:
1643 // Show cancel binding if agent is busy.
1727 // Show cancel binding if agent is busy.
1644− if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
1728+ if m.isAgentBusy() {
1645 cancelBinding := k.Chat.Cancel
1729 cancelBinding := k.Chat.Cancel
1646 if m.isCanceling {
1730 if m.isCanceling {
1647 cancelBinding.SetHelp("esc", "press again to cancel")
1731 cancelBinding.SetHelp("esc", "press again to cancel")
@@ -1676,6 +1760,9 @@ func (m *UI) ShortHelp() []key.Binding {
1676 k.Chat.PageDown,
1760 k.Chat.PageDown,
1677 k.Chat.Copy,
1761 k.Chat.Copy,
1763+ if m.pillsExpanded && hasIncompleteTodos(m.session.Todos) && m.promptQueue > 0 {
1764+ binds = append(binds, k.Chat.PillLeft)
1680 default:
1767 default:
1681 // TODO: other states
1768 // TODO: other states
@@ -1703,7 +1790,7 @@ func (m *UI) FullHelp() [][]key.Binding {
1703 help := k.Help
1790 help := k.Help
1704 help.SetHelp("ctrl+g", "less")
1791 help.SetHelp("ctrl+g", "less")
1705 hasAttachments := len(m.attachments.List()) > 0
1792 hasAttachments := len(m.attachments.List()) > 0
1706− hasSession := m.session != nil && m.session.ID != ""
1793+ hasSession := m.hasSession()
1707 commands := k.Commands
1794 commands := k.Commands
1708 if m.focus == uiFocusEditor && m.textarea.LineCount() == 0 {
1795 if m.focus == uiFocusEditor && m.textarea.LineCount() == 0 {
1709 commands.SetHelp("/ or ctrl+p", "commands")
1796 commands.SetHelp("/ or ctrl+p", "commands")
@@ -1717,7 +1804,7 @@ func (m *UI) FullHelp() [][]key.Binding {
1718 case uiChat:
1805 case uiChat:
1719 // Show cancel binding if agent is busy.
1806 // Show cancel binding if agent is busy.
1720− if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
1807+ if m.isAgentBusy() {
1721 cancelBinding := k.Chat.Cancel
1808 cancelBinding := k.Chat.Cancel
1722 if m.isCanceling {
1809 if m.isCanceling {
1723 cancelBinding.SetHelp("esc", "press again to cancel")
1810 cancelBinding.SetHelp("esc", "press again to cancel")
@@ -1785,6 +1872,9 @@ func (m *UI) FullHelp() [][]key.Binding {
1785 k.Chat.ClearHighlight,
1872 k.Chat.ClearHighlight,
1875+ if m.pillsExpanded && hasIncompleteTodos(m.session.Todos) && m.promptQueue > 0 {
1876+ binds = append(binds, []key.Binding{k.Chat.PillLeft})
1789 default:
1879 default:
1790 if m.session == nil {
1880 if m.session == nil {
@@ -1873,6 +1963,7 @@ func (m *UI) updateSize() {
1873 m.chat.SetSize(m.layout.main.Dx(), m.layout.main.Dy())
1963 m.chat.SetSize(m.layout.main.Dx(), m.layout.main.Dy())
1874 m.textarea.SetWidth(m.layout.editor.Dx())
1964 m.textarea.SetWidth(m.layout.editor.Dx())
1875 m.textarea.SetHeight(m.layout.editor.Dy())
1965 m.textarea.SetHeight(m.layout.editor.Dy())
1877 // Handle different app states
1968 // Handle different app states
1878 switch m.state {
1969 switch m.state {
@@ -1984,10 +2075,18 @@ func (m *UI) generateLayout(w, h int) layout {
1984 mainRect.Min.Y += 1
2075 mainRect.Min.Y += 1
1985 mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
2076 mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
1986 mainRect.Max.X -= 1 // Add padding right
2077 mainRect.Max.X -= 1 // Add padding right
1987− // Add bottom margin to main
1988− mainRect.Max.Y -= 1
1989 layout.header = headerRect
2078 layout.header = headerRect
2079+ pillsHeight := m.pillsAreaHeight()
2080+ if pillsHeight > 0 {
2081+ pillsHeight = min(pillsHeight, mainRect.Dy())
2082+ chatRect, pillsRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-pillsHeight))
1990− layout.main = mainRect
2083+ layout.main = chatRect
2084+ layout.pills = pillsRect
2086+ layout.main = mainRect
2088+ // Add bottom margin to main
2089+ layout.main.Max.Y -= 1
1991 layout.editor = editorRect
2090 layout.editor = editorRect
1992 } else {
2091 } else {
1993 // Layout
2092 // Layout
@@ -2004,10 +2103,18 @@ func (m *UI) generateLayout(w, h int) layout {
2004 sideRect.Min.X += 1
2103 sideRect.Min.X += 1
2005 mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
2104 mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
2006 mainRect.Max.X -= 1 // Add padding right
2105 mainRect.Max.X -= 1 // Add padding right
2007− // Add bottom margin to main
2008− mainRect.Max.Y -= 1
2009 layout.sidebar = sideRect
2106 layout.sidebar = sideRect
2107+ pillsHeight := m.pillsAreaHeight()
2108+ if pillsHeight > 0 {
2109+ pillsHeight = min(pillsHeight, mainRect.Dy())
2110+ chatRect, pillsRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-pillsHeight))
2010− layout.main = mainRect
2111+ layout.main = chatRect
2112+ layout.pills = pillsRect
2114+ layout.main = mainRect
2116+ // Add bottom margin to main
2117+ layout.main.Max.Y -= 1
2011 layout.editor = editorRect
2118 layout.editor = editorRect
@@ -2035,6 +2142,9 @@ type layout struct {
2035 // main is the area for the main pane. (e.x chat, configure, landing)
2142 // main is the area for the main pane. (e.x chat, configure, landing)
2036 main uv.Rectangle
2143 main uv.Rectangle
2145+ // pills is the area for the pills panel.
2038 // editor is the area for the editor pane.
2148 // editor is the area for the editor pane.
2039 editor uv.Rectangle
2149 editor uv.Rectangle
@@ -2208,6 +2318,19 @@ func isWhitespace(b byte) bool {
2208 return b == ' ' || b == '\t' || b == '\n' || b == '\r'
2318 return b == ' ' || b == '\t' || b == '\n' || b == '\r'
2321+// isAgentBusy returns true if the agent coordinator exists and is currently
2322+// busy processing a request.
2323+func (m *UI) isAgentBusy() bool {
2324+ return m.com.App != nil &&
2325+ m.com.App.AgentCoordinator != nil &&
2326+ m.com.App.AgentCoordinator.IsBusy()
2329+// hasSession returns true if there is an active session with a valid ID.
2330+func (m *UI) hasSession() bool {
2331+ return m.session != nil && m.session.ID != ""
2211// mimeOf detects the MIME type of the given content.
2334// mimeOf detects the MIME type of the given content.
2212func mimeOf(content []byte) string {
2335func mimeOf(content []byte) string {
2213 mimeBufferSize := min(512, len(content))
2336 mimeBufferSize := min(512, len(content))
@@ -2271,7 +2394,7 @@ func (m *UI) sendMessage(content string, attachments ...message.Attachment) tea.
2273 var cmds []tea.Cmd
2396 var cmds []tea.Cmd
2274− if m.session == nil || m.session.ID == "" {
2397+ if !m.hasSession() {
2275 newSession, err := m.com.App.Sessions.Create(context.Background(), "New Session")
2398 newSession, err := m.com.App.Sessions.Create(context.Background(), "New Session")
2276 if err != nil {
2399 if err != nil {
2277 return uiutil.ReportError(err)
2400 return uiutil.ReportError(err)
@@ -2319,7 +2442,7 @@ func cancelTimerCmd() tea.Cmd {
2319// and starts a timer. The second press (before the timer expires) actually
2442// and starts a timer. The second press (before the timer expires) actually
2320// cancels the agent.
2443// cancels the agent.
2321func (m *UI) cancelAgent() tea.Cmd {
2444func (m *UI) cancelAgent() tea.Cmd {
2322− if m.session == nil || m.session.ID == "" {
2445+ if !m.hasSession() {
2323 return nil
2446 return nil
@@ -2332,6 +2455,9 @@ func (m *UI) cancelAgent() tea.Cmd {
2332 // Second escape press - actually cancel the agent.
2455 // Second escape press - actually cancel the agent.
2333 m.isCanceling = false
2456 m.isCanceling = false
2334 coordinator.Cancel(m.session.ID)
2457 coordinator.Cancel(m.session.ID)
2458+ // Stop the spinning todo indicator.
2459+ m.todoIsSpinning = false
2335 return nil
2461 return nil
@@ -2521,7 +2647,7 @@ func (m *UI) handlePermissionNotification(notification permission.PermissionNoti
2521// newSession clears the current session state and prepares for a new session.
2647// newSession clears the current session state and prepares for a new session.
2522// The actual session creation happens when the user sends their first message.
2648// The actual session creation happens when the user sends their first message.
2523func (m *UI) newSession() {
2649func (m *UI) newSession() {
2524− if m.session == nil || m.session.ID == "" {
2650+ if !m.hasSession() {
@@ -2532,6 +2658,9 @@ func (m *UI) newSession() {
2532 m.textarea.Focus()
2658 m.textarea.Focus()
2533 m.chat.Blur()
2659 m.chat.Blur()
2534 m.chat.ClearMessages()
2660 m.chat.ClearMessages()
2661+ m.pillsExpanded = false
2537// handlePasteMsg handles a paste message.
2666// handlePasteMsg handles a paste message.