Detailed changes
@@ -59,6 +59,12 @@ type SessionAgent interface {
ClearQueue(sessionID string)
Summarize(context.Context, string, fantasy.ProviderOptions) error
Model() Model
+ // CancelCompletionNotification cancels any scheduled "turn ended"
+ // notification for the provided sessionID.
+ CancelCompletionNotification(sessionID string)
+ // HasPendingCompletionNotification returns true if a turn-end
+ // notification has been scheduled and not yet cancelled/shown.
+ HasPendingCompletionNotification(sessionID string) bool
}
const completionNotificationDelay = 5 * time.Second
@@ -546,6 +552,20 @@ func (a *sessionAgent) cancelCompletionNotification(sessionID string) {
}
}
+// CancelCompletionNotification implements SessionAgent.
+func (a *sessionAgent) CancelCompletionNotification(sessionID string) {
+ a.cancelCompletionNotification(sessionID)
+}
+
+// HasPendingCompletionNotification implements SessionAgent.
+func (a *sessionAgent) HasPendingCompletionNotification(sessionID string) bool {
+ if a.IsSessionBusy(sessionID) {
+ return false
+ }
+ _, ok := a.completionCancels.Get(sessionID)
+ return ok
+}
+
func (a *sessionAgent) Summarize(ctx context.Context, sessionID string, opts fantasy.ProviderOptions) error {
if a.IsSessionBusy(sessionID) {
return ErrSessionBusy
@@ -53,6 +53,12 @@ type Coordinator interface {
Summarize(context.Context, string) error
Model() Model
UpdateModels(ctx context.Context) error
+ // CancelCompletionNotification cancels any scheduled "turn ended"
+ // notification for the provided sessionID.
+ CancelCompletionNotification(sessionID string)
+ // HasPendingCompletionNotification reports whether there is a pending
+ // turn-end notification for the given session.
+ HasPendingCompletionNotification(sessionID string) bool
}
type coordinator struct {
@@ -153,6 +159,22 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
})
}
+// CancelCompletionNotification implements Coordinator.
+func (c *coordinator) CancelCompletionNotification(sessionID string) {
+ if c.currentAgent == nil || sessionID == "" {
+ return
+ }
+ c.currentAgent.CancelCompletionNotification(sessionID)
+}
+
+// HasPendingCompletionNotification implements Coordinator.
+func (c *coordinator) HasPendingCompletionNotification(sessionID string) bool {
+ if c.currentAgent == nil || sessionID == "" {
+ return false
+ }
+ return c.currentAgent.HasPendingCompletionNotification(sessionID)
+}
+
func getProviderOptions(model Model, providerCfg config.ProviderConfig) fantasy.ProviderOptions {
options := fantasy.ProviderOptions{}
@@ -129,6 +129,9 @@ func (m *messageListCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
return m, nil // Ignore clicks outside the component
}
if msg.Button == tea.MouseLeft {
+ if m.app.AgentCoordinator != nil && m.app.AgentCoordinator.HasPendingCompletionNotification(m.session.ID) {
+ m.app.AgentCoordinator.CancelCompletionNotification(m.session.ID)
+ }
cmds = append(cmds, m.handleMouseClick(x, y))
return m, tea.Batch(cmds...)
}
@@ -148,6 +151,9 @@ func (m *messageListCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
return m, nil // Ignore clicks outside the component
}
if msg.Button == tea.MouseLeft {
+ if m.app.AgentCoordinator != nil && m.app.AgentCoordinator.HasPendingCompletionNotification(m.session.ID) {
+ m.app.AgentCoordinator.CancelCompletionNotification(m.session.ID)
+ }
m.listCmp.EndSelection(x, y)
}
return m, tea.Batch(cmds...)
@@ -185,6 +191,9 @@ func (m *messageListCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
if msg.endSelection {
m.listCmp.EndSelection(msg.x, msg.y)
}
+ if m.app.AgentCoordinator != nil && m.app.AgentCoordinator.HasPendingCompletionNotification(m.session.ID) {
+ m.app.AgentCoordinator.CancelCompletionNotification(m.session.ID)
+ }
m.listCmp.SelectionStop()
cmds = append(cmds, m.CopySelectedText(true))
return m, tea.Batch(cmds...)
@@ -207,6 +216,9 @@ func (m *messageListCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
case tea.MouseWheelMsg:
+ if m.app != nil && m.app.AgentCoordinator != nil && m.session.ID != "" && m.app.AgentCoordinator.HasPendingCompletionNotification(m.session.ID) {
+ m.app.AgentCoordinator.CancelCompletionNotification(m.session.ID)
+ }
u, cmd := m.listCmp.Update(msg)
m.listCmp = u.(list.List[list.Item])
cmds = append(cmds, cmd)
@@ -215,11 +215,18 @@ func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
if m.app.AgentCoordinator.IsSessionBusy(m.session.ID) {
return m, util.ReportWarn("Agent is working, please wait...")
}
+ if m.app.AgentCoordinator != nil && m.app.AgentCoordinator.HasPendingCompletionNotification(m.session.ID) {
+ m.app.AgentCoordinator.CancelCompletionNotification(m.session.ID)
+ }
return m, m.openEditor(m.textarea.Value())
case OpenEditorMsg:
m.textarea.SetValue(msg.Text)
m.textarea.MoveToEnd()
case tea.PasteMsg:
+ // Interaction: cancel any pending turn-end notification for this session.
+ if m.app.AgentCoordinator != nil && m.app.AgentCoordinator.HasPendingCompletionNotification(m.session.ID) {
+ m.app.AgentCoordinator.CancelCompletionNotification(m.session.ID)
+ }
path := strings.ReplaceAll(string(msg), "\\ ", " ")
// try to get an image
path, err := filepath.Abs(strings.TrimSpace(path))
@@ -261,6 +268,10 @@ func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
m.setEditorPrompt()
return m, nil
case tea.KeyPressMsg:
+ // Interaction: cancel any pending turn-end notification for this session.
+ if m.app.AgentCoordinator != nil && m.app.AgentCoordinator.HasPendingCompletionNotification(m.session.ID) {
+ m.app.AgentCoordinator.CancelCompletionNotification(m.session.ID)
+ }
cur := m.textarea.Cursor()
curIdx := m.textarea.Width()*cur.Y + cur.X
switch {
@@ -174,6 +174,10 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
msg.Y -= 1
}
if p.isMouseOverChat(msg.X, msg.Y) {
+ // Interaction: cancel any pending turn-end notification for this session.
+ if p.app.AgentCoordinator != nil && p.app.AgentCoordinator.HasPendingCompletionNotification(p.session.ID) {
+ p.app.AgentCoordinator.CancelCompletionNotification(p.session.ID)
+ }
u, cmd := p.chat.Update(msg)
p.chat = u.(chat.MessageListCmp)
return p, cmd
@@ -246,6 +250,9 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
cmds = append(cmds, cmd)
return p, tea.Batch(cmds...)
case commands.ToggleCompactModeMsg:
+ if p.app.AgentCoordinator != nil && p.app.AgentCoordinator.HasPendingCompletionNotification(p.session.ID) {
+ p.app.AgentCoordinator.CancelCompletionNotification(p.session.ID)
+ }
p.forceCompact = !p.forceCompact
var cmd tea.Cmd
if p.forceCompact {
@@ -257,8 +264,14 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
}
return p, tea.Batch(p.SetSize(p.width, p.height), cmd)
case commands.ToggleThinkingMsg:
+ if p.app.AgentCoordinator != nil && p.app.AgentCoordinator.HasPendingCompletionNotification(p.session.ID) {
+ p.app.AgentCoordinator.CancelCompletionNotification(p.session.ID)
+ }
return p, p.toggleThinking()
case commands.OpenReasoningDialogMsg:
+ if p.app.AgentCoordinator != nil && p.app.AgentCoordinator.HasPendingCompletionNotification(p.session.ID) {
+ p.app.AgentCoordinator.CancelCompletionNotification(p.session.ID)
+ }
return p, p.openReasoningDialog()
case reasoning.ReasoningEffortSelectedMsg:
return p, p.handleReasoningEffortSelected(msg.Effort)
@@ -316,6 +329,9 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
return p, tea.Batch(cmds...)
case commands.ToggleYoloModeMsg:
// update the editor style
+ if p.app != nil && p.app.AgentCoordinator != nil && p.session.ID != "" && p.app.AgentCoordinator.HasPendingCompletionNotification(p.session.ID) {
+ p.app.AgentCoordinator.CancelCompletionNotification(p.session.ID)
+ }
u, cmd := p.editor.Update(msg)
p.editor = u.(editor.Editor)
return p, cmd
@@ -360,6 +376,11 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
}
return p, p.newSession()
case tea.KeyPressMsg:
+ // If the chat pane is focused, any key-based navigation in chat counts
+ // as interaction; cancel any pending turn-end notification.
+ if p.focusedPane == PanelTypeChat && p.app.AgentCoordinator != nil && p.app.AgentCoordinator.HasPendingCompletionNotification(p.session.ID) {
+ p.app.AgentCoordinator.CancelCompletionNotification(p.session.ID)
+ }
switch {
case key.Matches(msg, p.keyMap.NewSession):
// if we have no agent do nothing
@@ -374,6 +395,9 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
agentCfg := config.Get().Agents[config.AgentCoder]
model := config.Get().GetModelByType(agentCfg.Model)
if model.SupportsImages {
+ if p.app.AgentCoordinator != nil && p.app.AgentCoordinator.HasPendingCompletionNotification(p.session.ID) {
+ p.app.AgentCoordinator.CancelCompletionNotification(p.session.ID)
+ }
return p, util.CmdHandler(commands.OpenFilePickerMsg{})
} else {
return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Name)
@@ -384,6 +408,9 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
p.splash = u.(splash.Splash)
return p, cmd
}
+ if p.app.AgentCoordinator != nil && p.app.AgentCoordinator.HasPendingCompletionNotification(p.session.ID) {
+ p.app.AgentCoordinator.CancelCompletionNotification(p.session.ID)
+ }
p.changeFocus()
return p, nil
case key.Matches(msg, p.keyMap.Cancel):
@@ -391,6 +418,10 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
return p, p.cancel()
}
case key.Matches(msg, p.keyMap.Details):
+ // Opening/closing the sidebar counts as interaction; cancel pending notification.
+ if p.app.AgentCoordinator != nil && p.app.AgentCoordinator.HasPendingCompletionNotification(p.session.ID) {
+ p.app.AgentCoordinator.CancelCompletionNotification(p.session.ID)
+ }
p.toggleDetails()
return p, nil
}
@@ -191,6 +191,10 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
case commands.SwitchModelMsg:
+ // Opening model dialog is interaction; cancel pending turn-end notif.
+ if a.app.AgentCoordinator != nil && a.app.AgentCoordinator.HasPendingCompletionNotification(a.selectedSessionID) {
+ a.app.AgentCoordinator.CancelCompletionNotification(a.selectedSessionID)
+ }
return a, util.CmdHandler(
dialogs.OpenDialogMsg{
Model: models.NewModelDialogCmp(),
@@ -424,8 +428,13 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
a.status.ToggleFullHelp()
a.showingFullHelp = !a.showingFullHelp
return a.handleWindowResize(a.wWidth, a.wHeight)
- // dialogs
+ // dialogs
case key.Matches(msg, a.keyMap.Commands):
+ // Opening the command palette counts as interaction; cancel pending
+ // turn-end notification for the selected session if any.
+ if a.app.AgentCoordinator != nil && a.app.AgentCoordinator.HasPendingCompletionNotification(a.selectedSessionID) {
+ a.app.AgentCoordinator.CancelCompletionNotification(a.selectedSessionID)
+ }
// if the app is not configured show no commands
if !a.isConfigured {
return nil
@@ -444,6 +453,10 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
if !a.isConfigured {
return nil
}
+ // Opening sessions dialog is interaction; cancel pending turn-end notif.
+ if a.app.AgentCoordinator != nil && a.app.AgentCoordinator.HasPendingCompletionNotification(a.selectedSessionID) {
+ a.app.AgentCoordinator.CancelCompletionNotification(a.selectedSessionID)
+ }
if a.dialog.ActiveDialogID() == sessions.SessionsDialogID {
return util.CmdHandler(dialogs.CloseDialogMsg{})
}