Skip to content

TUI: stop button in the composer while a turn is running - #316

Merged
plombeer31 merged 2 commits into
mainfrom
feat/composer-stop-button
Sep 3, 2026
Merged

TUI: stop button in the composer while a turn is running#316
plombeer31 merged 2 commits into
mainfrom
feat/composer-stop-button

Conversation

@plombeer31

@plombeer31 plombeer31 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Problem

While the agent is working there is no visible way to stop it. Esc, Ctrl+C and /abort all abort the turn, but every one of them is keyboard lore — nothing on screen tells the operator a running turn can be stopped, or where. And once stopped, the chat renders an error wall (Turn failed [cancelled]: This operation was aborted — or, when the abort races the stream, even Turn failed [model]: model returned empty content) for something the operator did on purpose.

Change

1. A clickable stop chip inside the input field, next to send →, rendered only while status === "running":

  • ComposerStopButton — a sibling of ComposerSendButton with the same MouseTarget pattern and overlay layer. No disabled state: it only exists when there is a run to stop.
  • Drawn on the palette's error ground with a measured ink (readableOn), legible across all eleven themes.
  • PromptShell gains running / onStop props; tui-app wires onStop to exactly the pair of calls the Esc branch makes (onAbort() + abort_requested).

2. A stopped turn says who stopped it, and offers a retry. The reducer treats loop_failed as stopped-by-user when the category is cancelled or an abort_requested is on the books (state.aborting — every abort entry point sets it, finishRun clears it), which also catches the abort/stream race that used to dress the user's own stop as a provider failure. Instead of the warn wall it leaves a calm system notice — Agent stopped by user. — carrying the aborted turn's prompt as retryText, and chat-log hangs the existing [try again] beside [copy] on exactly that notice, resending the user's prompt: a mistaken stop is one click to undo.

Keyboard behaviour is unchanged; both pieces are additive.

Tests

  • App-level mouse test through real Ink layout and hit-testing: chip absent while idle, appears when a submit starts a turn, click lands on onAbort exactly once, chip leaves when the run ends.
  • Reducer tests: cancelled loop_failed → calm notice + retryText + cancelled outcome; [model] failure during a requested abort → still stopped-by-user; the same failure with no abort on the books keeps the warn styling; no retryText when there is no prompt to re-run.
  • Chat-log render test: [try again] hangs under the stopped notice and only there.

tsc --noEmit clean; components + reducer + key-binding/submit suites: 675 tests green. Verified live in the TUI against a real provider (stop mid-turn → Agent stopped by user. + [copy] [try again]).

Esc, Ctrl+C and /abort all stop the agent, but they are keyboard lore -
nothing on screen says a running turn can be stopped at all. Put a
clickable stop chip inside the input field, next to Send, rendered only
while status is running; a press takes exactly the path Esc does
(onAbort + abort_requested), so there is one abort path however it was
asked for.

The chip paints on the palette's error ground with a measured ink
(readableOn), the same trick the composer buffer uses, so it stays
legible across all eleven themes.
Aborting via the stop chip (or Esc / Ctrl+C / /abort) used to land a
warn-styled 'Turn failed [cancelled]: This operation was aborted' in
the chat — an error wall for something the operator did on purpose.
Worse, the abort races the LLM stream, so it sometimes surfaced as
'Turn failed [model]: model returned empty content', dressing the
user's own stop as a provider failure.

The reducer now treats loop_failed as stopped-by-user when the
category is 'cancelled' OR an abort_requested is on the books
(state.aborting — every abort entry point sets it, finishRun clears
it). It leaves a calm system notice — 'Agent stopped by user.' — that
carries the aborted turn's prompt as retryText, and chat-log hangs the
existing [try again] beside [copy] on exactly that notice, resending
the user's prompt: a mistaken stop is one click to undo.
@plombeer31
plombeer31 merged commit 9536859 into main Sep 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant