@@ -212,6 +212,12 @@ type UI struct {
212212 bangWasEmpty bool // true when bang prompt became empty on last keystroke
213213 pendingShellItem *chat.ShellItem
214214
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+
215221 header *header
216222
217223 // 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) {
982988 })
983989 }
984990 case shellResultMsg:
991+ // Clear the bang cancel func — command is done.
992+ if m.bangCancel != nil {
993+ m.bangCancel()
994+ m.bangCancel = nil
995+ }
985996 // Complete the pending shell item if it exists, otherwise create a new one.
986997 if msg.PendingID != "" {
987998 if item := m.chat.MessageItem(msg.PendingID); item != nil {
@@ -3275,6 +3286,9 @@ func isWhitespace(b byte) bool {
32753286// isAgentBusy returns true if the agent coordinator exists and is currently
32763287// busy processing a request.
32773288func (m *UI) isAgentBusy() bool {
3289+ if m.bangCancel != nil {
3290+ return true
3291+ }
32783292 return m.com.Workspace.AgentIsReady() &&
32793293 m.com.Workspace.AgentIsBusy()
32803294}
@@ -3497,20 +3511,27 @@ func (m *UI) runShellCommand(command string) tea.Cmd {
34973511 return shellStreamMsg{PendingID: pendingID, Chunk: chunk, streamCh: streamCh}
34983512 })
34993513
3514+ ctx, cancel := context.WithCancel(context.Background())
3515+ m.bangCancel = cancel
3516+
35003517 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)
35023519 close(streamCh)
3503− if err != nil {
3520+ if err != nil && !errors.Is(err, context.Canceled) {
35043521 return util.InfoMsg{
35053522 Type: util.InfoTypeError,
35063523 Msg: fmt.Sprintf("shell: %v", err),
35073524 }
35083525 }
3526+ exitCode := resp.ExitCode
3527+ if errors.Is(err, context.Canceled) {
3528+ exitCode = 130 // conventional SIGINT exit code
3529+ }
35093530 return shellResultMsg{
35103531 PendingID: pendingID,
35113532 Command: command,
35123533 Output: resp.Output,
3513− ExitCode: resp.ExitCode,
3534+ ExitCode: exitCode,
35143535 }
35153536 })
35163537 return tea.Batch(cmds...)
@@ -3538,8 +3559,15 @@ func (m *UI) cancelAgent() tea.Cmd {
35383559 }
35393560
35403561 if m.isCanceling {
3541− // Second escape press - actually cancel the agent.
3562+ // Second escape press — actually cancel.
35423563 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+
35433571 m.com.Workspace.AgentCancel(m.session.ID)
35443572 // Stop the spinning todo indicator.
35453573 m.todoIsSpinning = false