Skip to content

feat(tui): interrupt the agent with ctrl+c - #108

Merged
quickbeard merged 1 commit into
mainfrom
ctrl-c-interrupt
Jul 20, 2026
Merged

quickbeard merged 1 commit into
mainfrom
ctrl-c-interrupt

Conversation

@quickbeard

@quickbeard quickbeard commented Jul 20, 2026 •

Copy link
Copy Markdown
Owner

While a turn is running, Ctrl+C now aborts it on a single press, and the prompt footer leads with ctrl+c interrupt instead of esc interrupt. escape keeps its existing double-press interrupt, so nobody's muscle memory breaks.

Behavior

ctrl+c prompt has draft prompt empty
busy clears draft aborts immediately
idle clears draft arms exit confirm, second press quits

While busy, Ctrl+C can no longer arm the double-press quit from #51. Ctrl+D, <leader>q, and /exit still quit immediately, including mid-turn.

Approach

Added a session_interrupt_now keybind (default ctrl+c) and its session.interrupt.now command rather than adding ctrl+c to session_interrupt — the two need different press counts, and inspecting the triggering key inside one handler breaks as soon as someone rebinds.

Precedence runs through enabled predicates rather than binding order. Order is not a stable disambiguator here: same-priority layers resolve by registration order, and useBindings re-registers a layer on every reactive change, which bumps it to newest.

  • session.interrupt.now requires a busy session and an empty prompt, so ctrl+c falls through to prompt.clear with a draft present and to app.exit.confirm while idle.
  • app_exit_confirm moves to its own binding layer that also requires the session to be idle. app_exit keeps the original predicate.

Notes

  • The home screen's "stop the AI mid-response" tip now points at session.interrupt.now, so it agrees with the footer.

  • Permission and question prompts keep ctrl+c as their scoped reject — unchanged.

  • The interactive split-footer mode (codev --mini, and --mini --attach) has its own separate two-press ctrl+c logic; left alone, it's an independent job. Plain non-interactive codev run "…" is unaffected either way — it builds no renderer, keymap, or footer, and ctrl+c there is an ordinary SIGINT.

  • Adding a keybind drifts packages/sdk/js/src/gen/types.gen.ts, which is not regenerated here — following the precedent set by feat(tui): require double Ctrl+C to quit #51, which added app_exit_confirm the same way.

    Correction, added after merge: that claim was wrong. Nothing regenerates src/gen — packages/sdk/js/script/build.ts outputs to src/v2/gen and merely runs prettier over src/gen (line 115). The v2 output contains no keybind types at all, and a freshly generated spec (bun dev generate) has no keybind schema and no keybinds property on Config, because keybinds were moved out of the server config: packages/opencode/src/config/config.ts:54-57 deletes keybinds as a legacy key and config/tui-migrate.ts migrates it into the TUI's own config.

    So KeybindsConfig (src/gen/types.gen.ts:772) and Config.keybinds (line 1184) are fossils of a pre-migration API. This PR does not drift them — regenerating correctly would delete them, which is a separate, breaking cleanup unrelated to this change. The schema that actually validates keybinds (packages/tui/src/config/index.tsx:56, built from TuiKeybind.KeybindOverrides) derives from Definitions at runtime and already knows about session_interrupt_now.

Testing

  • New packages/tui/test/interrupt.view.test.tsx boots the real app against a session the fixture server reports as busy, then drives actual ctrl+c key events: draft cleared with no abort, then abort on a single press, with the exit confirm verified disarmed.
  • bun typecheck clean across all 31 packages.
  • Full tui suite: 232 pass / 1 fail. The failure (app.exit prints the session epilogue after scoped cleanup) is pre-existing and fails identically on an unmodified tree.

Docs (en + es), DIVERGENCES.md, and CHANGELOG.md updated.

🤖 Generated with Claude Code

While a turn is running, ctrl+c now aborts it on a single press and the
prompt footer leads with "ctrl+c interrupt" instead of "esc interrupt".
Escape keeps its existing double-press interrupt.

Added a session_interrupt_now keybind (default ctrl+c) and its
session.interrupt.now command rather than adding ctrl+c to
session_interrupt, since the two need different press counts and
inspecting the triggering key inside one handler breaks on rebind.

Precedence runs through `enabled` predicates rather than binding order,
which is not stable (same-priority layers resolve by registration order,
and useBindings re-registers on every reactive change):

  - session.interrupt.now requires a busy session and an empty prompt, so
    ctrl+c falls through to prompt.clear with a draft present and to
    app.exit.confirm while idle.
  - app_exit_confirm moves to its own binding layer that also requires the
    session to be idle, so ctrl+c cannot arm the double-press quit mid-turn.
    app_exit (ctrl+d, <leader>q) keeps the shared predicate and still quits
    immediately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@quickbeard
quickbeard merged commit d7fbbee into main Jul 20, 2026
2 of 5 checks passed
@quickbeard
quickbeard deleted the ctrl-c-interrupt branch July 20, 2026 10:14
quickbeard added a commit that referenced this pull request Jul 20, 2026
Restores #108's entry to what merged (plus the PR link it was missing)
and adds a separate bullet for this change, per the convention that
follow-ups get their own entry rather than rewriting the earlier one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
quickbeard added a commit that referenced this pull request Jul 20, 2026
Both PRs land in the same unreleased version, and #109 walks back part of
#108, so two bullets would ship contradicting each other. One entry
describes the net behavior and cites both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
quickbeard added a commit that referenced this pull request Jul 20, 2026
* fix(tui): advertise the esc interrupt in the busy footer

#108 made the busy prompt footer lead with "ctrl+c interrupt". Put
"esc interrupt" back in front: escape is the interrupt we advertise, in
the footer and in the home screen tip.

Ctrl+C keeps the single-press interrupt from #108 and every precedence
rule around it — only what is on show changes. The footer shortcut now
resolves session.interrupt (mapping escape -> esc) and falls back to the
session.interrupt.now label when escape is unbound, the mirror of the
fallback #108 introduced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: give the esc-interrupt revert its own changelog entry

Restores #108's entry to what merged (plus the PR link it was missing)
and adds a separate bullet for this change, per the convention that
follow-ups get their own entry rather than rewriting the earlier one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: fold the esc-interrupt revert into the ctrl+c changelog entry

Both PRs land in the same unreleased version, and #109 walks back part of
#108, so two bullets would ship contradicting each other. One entry
describes the net behavior and cites both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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