Skip to content

[Feature]: Return the prompt to the composer when aborting before the model replies #249

Description

@kele98

Product or interface

CLI - interactive TUI

Use case and problem

When I submit a prompt and immediately realize it has a typo, targets the wrong file, or asks the wrong question, the only escape is Esc — which stops the turn and leaves the prompt stuck inside a cancelled turn in the transcript. I have to retype it or recall it with Up and edit from history, losing the "fix one word and resend" flow.

Today the queue flow already returns queued messages to the composer (Alt+Up), and crash recovery already restores interrupted submissions on relaunch — but the most common case, "I just sent it and regret it", has no path.

Desired behavior

State-gated restore, keyed on what the aborted turn actually produced — not a timer:

When the user aborts a running turn AND that turn has produced no user-visible output (no assistant text, no tool call, no steer message), the prompt text returns to the composer for editing. Thinking does not disqualify: it is internal model process and the most common regret moment. Turns with output keep today's stop-only behavior, so intentional stops are never second-guessed.

Interaction example:

type a prompt, press Enter
Esc                                        (before any reply/tool output)
  -> "Stopped · message restored to the Composer."
  -> the prompt is back in the composer, editable
  -> the aborted turn's transcript row is removed
type the correction (merges below the restored text), Enter to resend

Esc after the model started replying
  -> turn stops as today; composer stays empty; transcript kept

Acceptance criteria:

  • The trigger is the confirmed stop of the aborted turn on the existing abort funnel — delegated-only stops (root still running) never fire it; a stop the runtime does not confirm never fires it.
  • Restore merges with text typed during the stop (never overwrites in-progress typing); restores drop the matching pending retry so a relaunch cannot duplicate the text; side conversations are skipped.
  • Exiting the app after an abort still persists the restored draft (existing draft-recovery path), so the text survives to the next launch.
  • A restore failure reports a warning without breaking Esc handling.

Why state-gated instead of a time window: a timer races the model's first token and behaves non-deterministically; gating on "no output yet" captures exactly the regret window with zero timing sensitivity. Why not unconditional: interrupting a turn that is already replying usually means "stop", not "give my words back" — unconditional restore would fight that intent.

Platform

Multiple platforms

Alternatives and additional context

What I tried / considered:

  • Up input-history recall — works, but loses cursor position and attachments and requires knowing the shortcut.
  • /history Edit on the committed message — a runtime-level rewind; far heavier than the regret-window case needs.
  • Doing nothing — the cancelled turn keeps the text, but editing it means retyping.

I implemented this on my fork (per the collaborators-only PR policy, not opening a PR; feel free to review the fork commit and port it as a maintainer PR — partial adoption welcome):

  • Branch feat/abort-prompt-restore, single commit 2dfc60b on top of current main: kele98@2dfc60b
  • TUI-only: 6 files (packages/tui abort funnel + app wiring + a small draft-lifecycle helper + tests + the Esc shortcut row in README/README_ZH updated in both languages). No runtime, persistence, or protocol changes.
  • Validation: tui-app.test.ts 270/281 — the 11 failures are the pre-existing zh-CN locale baseline on this machine, reproduced identically with the patch stashed; new/adjusted cases cover no-output restore, thinking-only restore, after-output no-restore, unconfirmed-stop no-restore, delegated-only-stop no-restore, merge-with-typed-text, and a two-instance relaunch non-duplication test through real draft persistence. pnpm verify passes all gates up to test:capabilities (stops on the same 23 environment-baseline failures: 11 locale assertions + 12 Windows symlink EPERM); remaining gates pass individually (status-contract 9/9, smoke 16 pass/1 skip, byok 11/11); typecheck and build clean. Interactive smoke via a real pty against a local fixture provider (stream holds open until abort) verified both timings end-to-end. Not claimed: macOS/Linux manual acceptance, live-model runs, Desktop app.

Key semantics (trigger scope, thinking exemption, transcript-row removal) are all open to adjustment if maintainers prefer a different shape.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestneeds-triageAwaiting maintainer assessmenttuiInteractive terminal UI (TUI)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions