on main help sign in
feat(bangmode): cancel command execution
47ed0f3Kieran Klukas
internal/ui/model/ui.go+32−4
@@ -212,6 +212,12 @@ type UI struct {
212 bangWasEmpty bool // true when bang prompt became empty on last keystroke
212 bangWasEmpty bool // true when bang prompt became empty on last keystroke
213 pendingShellItem *chat.ShellItem
213 pendingShellItem *chat.ShellItem
214
214
215+ // bangCancel cancels a running bang-mode shell command. Nil when no
216+ // bang command is in progress. Set by runShellCommand, cleared by
217+ // shellResultMsg. Checked by isAgentBusy and cancelAgent so that
218+ // Escape works for bang commands the same way it does for agent runs.
219+ bangCancel context.CancelFunc
220+
215 header *header
221 header *header
216
222
217 // sendProgressBar instructs the TUI to send progress bar updates to the
223 // sendProgressBar instructs the TUI to send progress bar updates to the
@@ -982,6 +988,11 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
982 })
988 })
983 }
989 }
984 case shellResultMsg:
990 case shellResultMsg:
991+ // Clear the bang cancel func — command is done.
992+ if m.bangCancel != nil {
993+ m.bangCancel()
994+ m.bangCancel = nil
995+ }
985 // Complete the pending shell item if it exists, otherwise create a new one.
996 // Complete the pending shell item if it exists, otherwise create a new one.
986 if msg.PendingID != "" {
997 if msg.PendingID != "" {
987 if item := m.chat.MessageItem(msg.PendingID); item != nil {
998 if item := m.chat.MessageItem(msg.PendingID); item != nil {
@@ -3275,6 +3286,9 @@ func isWhitespace(b byte) bool {
3275// isAgentBusy returns true if the agent coordinator exists and is currently
3286// isAgentBusy returns true if the agent coordinator exists and is currently
3276// busy processing a request.
3287// busy processing a request.
3277func (m *UI) isAgentBusy() bool {
3288func (m *UI) isAgentBusy() bool {
3289+ if m.bangCancel != nil {
3290+ return true
3291+ }
3278 return m.com.Workspace.AgentIsReady() &&
3292 return m.com.Workspace.AgentIsReady() &&
3279 m.com.Workspace.AgentIsBusy()
3293 m.com.Workspace.AgentIsBusy()
3280}
3294}
@@ -3497,20 +3511,27 @@ func (m *UI) runShellCommand(command string) tea.Cmd {
3497 return shellStreamMsg{PendingID: pendingID, Chunk: chunk, streamCh: streamCh}
3511 return shellStreamMsg{PendingID: pendingID, Chunk: chunk, streamCh: streamCh}
3498 })
3512 })
3499
3513
3514+ ctx, cancel := context.WithCancel(context.Background())
3515+ m.bangCancel = cancel
3516+
3500 cmds = append(cmds, func() tea.Msg {
3517 cmds = append(cmds, func() tea.Msg {
3501− resp, err := m.com.Workspace.AgentRunShellCommand(context.Background(), sessionID, command, contentWidth, onProgress)
3518+ resp, err := m.com.Workspace.AgentRunShellCommand(ctx, sessionID, command, contentWidth, onProgress)
3502 close(streamCh)
3519 close(streamCh)
3503− if err != nil {
3520+ if err != nil && !errors.Is(err, context.Canceled) {
3504 return util.InfoMsg{
3521 return util.InfoMsg{
3505 Type: util.InfoTypeError,
3522 Type: util.InfoTypeError,
3506 Msg: fmt.Sprintf("shell: %v", err),
3523 Msg: fmt.Sprintf("shell: %v", err),
3507 }
3524 }
3508 }
3525 }
3526+ exitCode := resp.ExitCode
3527+ if errors.Is(err, context.Canceled) {
3528+ exitCode = 130 // conventional SIGINT exit code
3529+ }
3509 return shellResultMsg{
3530 return shellResultMsg{
3510 PendingID: pendingID,
3531 PendingID: pendingID,
3511 Command: command,
3532 Command: command,
3512 Output: resp.Output,
3533 Output: resp.Output,
3513− ExitCode: resp.ExitCode,
3534+ ExitCode: exitCode,
3514 }
3535 }
3515 })
3536 })
3516 return tea.Batch(cmds...)
3537 return tea.Batch(cmds...)
@@ -3538,8 +3559,15 @@ func (m *UI) cancelAgent() tea.Cmd {
3538 }
3559 }
3539
3560
3540 if m.isCanceling {
3561 if m.isCanceling {
3541− // Second escape press - actually cancel the agent.
3562+ // Second escape press — actually cancel.
3542 m.isCanceling = false
3563 m.isCanceling = false
3564+
3565+ // Cancel a running bang command if one is in progress.
3566+ if m.bangCancel != nil {
3567+ m.bangCancel()
3568+ m.bangCancel = nil
3569+ }
3570+
3543 m.com.Workspace.AgentCancel(m.session.ID)
3571 m.com.Workspace.AgentCancel(m.session.ID)
3544 // Stop the spinning todo indicator.
3572 // Stop the spinning todo indicator.
3545 m.todoIsSpinning = false
3573 m.todoIsSpinning = false