Skip to content

fix(terminal): encode Ctrl+C when the kitty keyboard protocol is on - #24370

Open
innocarpe wants to merge 8 commits into
stablyai:mainfrom
innocarpe:fix-17665-kitty-interrupt
Open

innocarpe wants to merge 8 commits into
stablyai:mainfrom
innocarpe:fix-17665-kitty-interrupt

Conversation

@innocarpe

@innocarpe innocarpe commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

ELI5

Ctrl+C in the in-app terminal now interrupts a Kitty-mode terminal application using the Kitty Ctrl+C sequence, including when the key event comes from a non-Latin input source.

What Changed

  • A shared helper recognizes both the ordinary ETX byte and Kitty's Ctrl+C sequence as terminal interrupt input.
  • Renderer, runtime, RPC, and CLI interrupt paths use the same Kitty-aware byte selection.
  • The renderer's downstream intent detection and bracketed-paste reset now recognize the Kitty sequence too.
  • Kitty flags 1 and 8 select CSI 99;5u; flags 0, 2, 4, and 16 keep ETX (0x03).
  • Updated the terminal bypass policy documentation to match the current flag behavior.

Why

The renderer intercepts Ctrl+C before xterm can encode it. On a non-Latin key path, the renderer could send the Kitty sequence without setting a pending Ctrl+C intent, while the exact-byte fallback recognized only ETX. The interrupt was delivered, but intent tracking and bracketed-paste recovery were skipped. The shared recognition keeps the byte and the renderer's follow-up behavior aligned.

Linked Issue

Fixes #17665

Visual Proof

N/A — the layout does not change, and a screenshot cannot show terminal input bytes or intent tracking. Regression tests assert both byte encodings and the non-Latin renderer path.

Testing

  • I manually tested these changes locally.
  • Automated tests added/updated.

Ran on macOS:

node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts --cache false \
  src/shared/terminal-interrupt-bytes.test.ts \
  src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts \
  src/renderer/src/components/terminal-pane/pty-connection-interrupt-inference.test.ts \
  src/renderer/src/components/terminal-pane/pty-input-write-queue.test.ts \
  src/main/runtime/runtime-terminal-writer.test.ts \
  src/main/daemon/headless-emulator-kitty-flags.test.ts \
  src/main/runtime/headless-hydration-ownership.test.ts

All 86 tests passed. I did not manually test this on macOS, Linux, Windows, SSH, or mobile sessions. The full lint, typecheck, test, and build commands were not run locally.

AI Disclosure

OpenAI Codex (GPT-6) assisted with the implementation and review.

Review

Self-reviewed byte selection, the renderer's intent recognition, and bracketed-paste interruption. The integration test uses a non-Latin key event with code KeyC, sends Kitty Ctrl+C, and checks both inferred intent and paste-state reset.

Agent skill upstream boundary

  • Not applicable; no agent skill, installer, or skill registry changed.

Notes

The wire format remains ETX when Kitty disambiguation flags are not enabled. Automated tests cover the shared helper, renderer, runtime writer, headless emulator, and hydration paths; no live TUI or remote SSH session was tested.

Checklist

  • This PR is small and focused.
  • The change, user-visible behavior, mechanism, and reason are explained above.
  • Visual proof is marked N/A with the reason above.
  • Self-reviewed for correctness, security, and performance.
  • Cross-platform, SSH/remote, and backwards compatibility were considered above.
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass; only the focused tests above were run locally.

@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: bc94ef21-3767-4683-84eb-87eb9351c387
📥 Commits

Reviewing files that changed from the base of the PR and between 4e73ab6 and 7f93f86.

📒 Files selected for processing (5)
  • src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts
  • src/main/runtime/terminal-send-payload.ts
  • src/renderer/src/components/terminal-pane/terminal-pane-pane-input.ts
  • src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts
  • src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Terminal interrupt payloads now use the target PTY’s Kitty keyboard flags. The shared helper selects Kitty Ctrl+C bytes when flag bit 1 or 8 is set, and ETX otherwise. The runtime reads flags for direct and leaf PTYs. The headless emulator exposes the flags and reapplies numeric flags during snapshot restoration and renderer hydration. RuntimeTerminalWriter now writes the suffix already present in the payload. The renderer also resolves interrupt input from current Kitty keyboard flags. Tests cover flag access, snapshot restoration, interrupt-byte selection, and payload writing.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🔵 Low · up to 7f93f

Ctrl+C can still be ignored by a Kitty-aware TUI after a snapshot and nested keyboard-mode change. The issue is limited to that state sequence and can be addressed as follow-up.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 7f93f

The inspected changes preserve terminal destinations and existing input checks. No introduced security vulnerability was established, but successful interruption still depends on synchronized keyboard state during recovery, and coverage is incomplete.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — For the inspected runtime send route, protocol flags affect the bytes sent to the already-resolved PTY, not destination selection. A terminal application can influence its protocol state through output, but this change does not establish a new route to another PTY or additional authority.

Trust Boundaries and Controls

  • observed — The direct and leaf send branches retain their existing handle resolution, connected or writable checks, input-size checks, and leaf-absence validation. Their base-to-head changes add flag lookup to payload construction without changing these controls.

Resilience and Maintainability Implications

  • observed — Failed renderer hydration is treated as completed while queued live output remains usable. This policy predates the PR; the new restoration improves successful hydration but does not add recovery from an unsuccessful snapshot attempt.
  • inferred — Restoring current numeric flags does not restore omitted push history. The existing tracker test describes a restored nonzero state becoming unknown and zero after a push/pop transition; the new renderer selector then falls back to ETX. This limits reliable interruption after recovery, but the inspected baseline also sent ETX, so that case does not establish a newly worsened security condition.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #17665 requires kitty-aware --interrupt bytes and automated coverage. The shared terminalInterruptBytes helper returns CSI 99;5u for flags 1 or 8 and ETX otherwise. `buildTerminalSendPaylo…
Out of Scope Changes check ✅ Passed The renderer change makes keyboard Ctrl+C use the same encoding requested by issue #17665. Headless flag exposure, hydration restoration, payload handling, and related tests support or verify correct …
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 16 files.
Title check ✅ Passed The title clearly identifies the main change: encoding Ctrl+C for terminals that use the Kitty keyboard protocol.
Description check ✅ Passed The description covers the required sections and explains the user-visible change, implementation, reason, linked issue, visual-proof status, testing, and compatibility considerations. It also states …
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Terminal interrupt encoding for kitty keyboard protocol.

The PR appears safe to merge; no outstanding or new actionable findings remain.

Summary

The PR makes runtime terminal interrupts use kitty-encoded Ctrl+C when the headless model has negotiated the relevant keyboard flags, while retaining ETX otherwise. It also restores flags from provider and renderer snapshots and uses the selected payload for both writing and byte counting.

  • The latest change handles kitty flags carried by an empty renderer buffer.
  • The two previous findings are resolved and are not reposted.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Terminal interrupt] --> B[Read headless kitty flags]
  B --> C{Ctrl+C encoding flags active?}
  C -->|Yes| D[CSI 99;5u]
  C -->|No| E[ETX]
  D --> F[Write selected payload and report its byte count]
  E --> F
Loading

Reviews (5) · Last reviewed commit: "fix(terminal): apply kitty flags from an..."

Comment thread src/main/runtime/orca-runtime-fit-override-listeners.ts
Comment thread src/main/runtime/runtime-terminal-writer.ts Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one minor test suggestion inline, plus a scope observation.

Reviewed changes

  • Shared interrupt bytes helper — new src/shared/terminal-interrupt-bytes.ts exports TERMINAL_INTERRUPT_ETX, TERMINAL_INTERRUPT_KITTY_CTRL_C (\x1b[99;5u), and terminalInterruptBytes(flags), which picks the kitty encoding only when flags > 0.
  • Runtime write path encodes Ctrl+C — RuntimeTerminalWriter gains a getKittyKeyboardFlags(ptyId) getter and routes action.interrupt through the helper; buildTerminalSendPayload(action, kittyKeyboardFlags) uses it too, so bytesWritten reflects the bytes actually written.
  • Flag source — HeadlessEmulator.kittyKeyboardFlags() exposes the flags xterm parsed from PTY output (the emulator opts into vtExtensions.kittyKeyboard), and readPtyKittyKeyboardFlags reads them from headlessTerminals, falling back to 0 when no model exists yet.
  • Wiring — the getter is injected into the RuntimeId writer and both sendTerminal call sites.
  • Tests — the three new test files cover the helper, the writer with/without flags, and the emulator reader (5 pass locally).

The flags > 0 gate matches xterm's own KittyKeyboard.shouldUseProtocol(flags), so the runtime now agrees with what xterm would encode for Ctrl+C under the same flags. The no-flags path still writes ETX, so plain shells are unaffected.

ℹ️ The renderer's Ctrl+C path still sends bare ETX, so the "shared helper" is only half wired

The PR body says in-app Ctrl+C "already arrives as CSI 99;5u because xterm encodes it", but the renderer actually intercepts plain Ctrl+C in xterm-bypass-policy.ts (shouldHandleTerminalInterruptKeyboardEvent) and writes a bare ETX through pane.terminal.input(TERMINAL_INTERRUPT_INPUT) — and the kitty reset that used to accompany that write was removed in 3eac4d93d3 (#23584). So the two interrupt paths now produce different bytes, and issue #17665 specifically asked for "one shared helper so keyboard, RPC and CLI all produce the same bytes." This is not a bug in the runtime fix you shipped, but it is worth confirming the keyboard path shouldn't use terminalInterruptBytes as well.

Technical details
# Keyboard interrupt path diverges from the runtime helper

## Affected sites
- `src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts:306` — `shouldHandleTerminalInterruptKeyboardEvent` bypasses xterm's kitty encoder for plain Ctrl+C.
- `src/renderer/src/components/terminal-pane/terminal-pane-pane-input.ts:140-143` — sends `TERMINAL_INTERRUPT_INPUT` (`\x03`) via `pane.terminal.input()`; the `resetTerminalKeyboardProtocolAfterInterrupt` call was deleted in `3eac4d93d3`.
- `src/shared/terminal-interrupt-bytes.ts` — the new helper is only consumed by `runtime-terminal-writer.ts` and `terminal-send-payload.ts`.

## Open questions for the human
- Is the in-app Ctrl+C path out of scope for this PR by design, or should it also encode `CSI 99;5u` for a kitty PTY?
- If in-app Ctrl+C keeps sending bare ETX and still works against Claude Code, that would contradict the issue's premise that ETX is swallowed — worth reconciling before calling this a parity fix.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/daemon/headless-emulator-kitty-flags.test.ts Outdated
@innocarpe
innocarpe force-pushed the fix-17665-kitty-interrupt branch from 149878f to 7958201 Compare October 1, 2026 12:30
@innocarpe

Copy link
Copy Markdown
Contributor Author

Sync update (7958201051)

A replaced headless terminal reapplies the provider snapshot's kitty keyboard flags, so interrupt still sends the kitty Ctrl+C. The writer sends the interrupt bytes chosen for that payload, and the flag test expects 1.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: d786b6dd-ce16-43f1-a37e-a3b0c01db0c2

📥 Commits

Reviewing files that changed from the base of the PR and between 744e772 and 7958201.

📒 Files selected for processing (11)
  • src/main/daemon/headless-emulator-kitty-flags.test.ts
  • src/main/daemon/headless-emulator.ts
  • src/main/runtime/headless-seed-ownership.test.ts
  • src/main/runtime/orca-runtime-controller-knows-pty-is-live.ts
  • src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts
  • src/main/runtime/orca-runtime-fit-override-listeners.ts
  • src/main/runtime/runtime-terminal-writer.test.ts
  • src/main/runtime/runtime-terminal-writer.ts
  • src/main/runtime/terminal-send-payload.ts
  • src/shared/terminal-interrupt-bytes.test.ts
  • src/shared/terminal-interrupt-bytes.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Comment thread src/shared/terminal-interrupt-bytes.ts Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — the incremental commit cleanly addresses both prior findings; one open question on a sibling hydration path is inline.

Reviewed changes

  • Precomputed interrupt payload — buildTerminalSendPayload now takes the PTY's kitty flags, and RuntimeTerminalWriter derives the suffix as payload.slice(text.length) rather than re-reading flags, so the bytes written always match the reported bytesWritten.
  • Flags restored across a replaced model — replaceHeadlessTerminalAfterExecutionContextChange re-applies snapshot.kittyKeyboardFlags after the provider seed, since rehydrate sequences omit kitty pushes.
  • Flag lookup moved to the runtime — new readPtyKittyKeyboardFlags on the fit-override base class reads headlessTerminals; both sendTerminal payload builds pass it; the writer's flag getter was removed.
  • Tests tightened and added — exact toBe(1) assertion in headless-emulator-kitty-flags.test.ts, and a provider-replacement restore test in headless-seed-ownership.test.ts.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/main/runtime/orca-runtime-create-pty-headless-terminal-state.ts
@innocarpe
innocarpe force-pushed the fix-17665-kitty-interrupt branch from 7958201 to df4a0e1 Compare October 1, 2026 12:52
@innocarpe

Copy link
Copy Markdown
Contributor Author

Sync update (df4a0e1ef8)

Ctrl+C is encoded as CSI u only when disambiguation or report-all-keys is enabled. Alternate-key reporting alone (CSI >4u) still interrupts with ETX.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — the incremental commit is correct; one description nit below.

Reviewed changes

  • Narrowed the kitty gate to the flags that actually change Ctrl+C — terminalInterruptBytes now tests flags & (1 | 8) instead of flags > 0, so alternate-key (4), event-type (2), and associated-text (16) modes keep the bare ETX.
  • Regression tests for the untouched bits — terminal-interrupt-bytes.test.ts pins flag 8 to the kitty encoding and flags 4 / 2|4|16 to ETX.

This matches the kitty spec: only "disambiguate escape codes" (1) and "report all keys as escape codes" (8, which implies disambiguation) turn legacy Ctrl+C into CSI 99;5u; alternate keys (4) are a pure enhancement to already-escaped events, and 2/16 alone do not change Ctrl+C. The four interrupt/emulator test files pass locally.

ℹ️ Nitpicks

  • The PR description still says an interrupt writes CSI 99;5u whenever the kitty flags are "non-zero"; after this commit only flags 1/8 do (2, 4, 16 stay ETX). A one-line description update would keep it matching the code.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@innocarpe

Copy link
Copy Markdown
Contributor Author

Sync update (6f6e819ecf)

A model seeded from the mounted renderer now restores the kitty flags carried beside that buffer. An interrupt encodes Ctrl+C for a TUI that already enabled the protocol, including after an SSH or relaunch hydrate.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Renderer hydration now restores kitty flags — maybeHydrateHeadlessFromRenderer re-applies rendered.kittyKeyboardFlags after the seed write, matching the provider-snapshot path, so an interrupt for a still-running kitty TUI encodes CSI 99;5u instead of falling back to ETX.
  • Regression test — headless-hydration-ownership.test.ts seeds a renderer snapshot carrying kittyKeyboardFlags: 1 and asserts the model reports 1 and terminalInterruptBytes yields CSI 99;5u.

The new reapply is placed after the seed and before the live writes chain, so a kitty push from later live output still wins. The renderer serializer only publishes these flags when the renderer is the ordered-stream authority and has a proven baseline, so the guard fires exactly in the renderer-authoritative reattach case it targets. All five interrupt/emulator test files pass locally (30 tests).

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: f5641587-c5c0-47c3-b2d3-0b389036df53

📥 Commits

Reviewing files that changed from the base of the PR and between df4a0e1 and 6f6e819.

📒 Files selected for processing (2)
  • src/main/runtime/headless-hydration-ownership.test.ts
  • src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

@innocarpe
innocarpe force-pushed the fix-17665-kitty-interrupt branch from 6f6e819 to 4e73ab6 Compare October 1, 2026 13:38
@innocarpe

Copy link
Copy Markdown
Contributor Author

Sync update (4e73ab6793)

An empty renderer buffer still restores the kitty flags carried beside that payload. Interrupt keeps encoding Ctrl+C for a TUI that already enabled the protocol, instead of falling back to ETX.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Empty renderer-buffer hydration now applies kitty flags — maybeHydrateHeadlessFromRenderer splits the empty-data return so a blank renderer buffer that still carries proven kittyKeyboardFlags seeds them into the fresh emulator, instead of leaving it at 0 and falling back to ETX on interrupt.
  • Regression test — headless-hydration-ownership.test.ts resolves an empty data / kittyKeyboardFlags: 1 snapshot and asserts the model reports 1 and terminalInterruptBytes yields CSI 99;5u.

The new branch places the flag application before its early return and mirrors the non-empty path's this.headlessTerminals.get(ptyId) !== state guard outcome (both end the chain link via finally), so a replacement model during the write cannot be seeded twice. All five interrupt/emulator test files pass locally (31 tests).

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@innocarpe
innocarpe force-pushed the fix-17665-kitty-interrupt branch from 4e73ab6 to 7f93f86 Compare October 4, 2026 11:57
@innocarpe

Copy link
Copy Markdown
Contributor Author

Thanks for pointing out the renderer path and the flag wording.

The renderer intercepts Ctrl+C before xterm can encode it. I routed that key path through the shared interrupt encoder using the pane's current Kitty flags, so flags 1 and 8 select CSI 99;5u, while no flags and unrelated flags such as 2, 4, and 16 keep ETX. The renderer regression test covers both sides of that selection.

I also updated the PR description to match the exact flag behavior and current scope. The focused run passed all 27 tests across the renderer, shared helper, runtime writer, and headless emulator test files. I have not run a manual TUI session or manual tests on other platforms.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The new renderer encoder emits the kitty Ctrl+C bytes, but the renderer's own Ctrl+C recognizers nowhere else learned to read them, so interrupt intent is dropped on a supported input path.

Reviewed changes

  • Renderer Ctrl+C now uses the shared encoder — terminal-pane-pane-input.ts replaces the bare TERMINAL_INTERRUPT_INPUT with resolveTerminalInterruptKeyboardInput(paneKittyKeyboardModesRef.current.get(pane.id)?.flags ?? 0), so an in-app interrupt to a kitty PTY writes \x1b[99;5u, closing the parity gap raised in an earlier pullfrog review.
  • Constant replaced by a resolver — xterm-bypass-policy.ts drops TERMINAL_INTERRUPT_INPUT for a documented resolveTerminalInterruptKeyboardInput that delegates to terminalInterruptBytes, and the unit test pins kitty flags 1/8 to \x1b[99;5u and 4 to ETX.
  • Comment tidy-ups — JSDoc rewritten in orca-runtime-maybe-hydrate-headless-from-renderer.ts and terminal-send-payload.ts.

ℹ️ Nitpicks

  • shouldHandleTerminalInterruptKeyboardEvent's JSDoc (xterm-bypass-policy.ts:307-310) still says the chord is "sent as ETX through Terminal.input()"; it now sends CSI 99;5u whenever the pane's kitty flags are set.
  • The PR description's "Out" list still names "Keyboard handling in the renderer" as out of scope, but this commit changes exactly that.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@innocarpe
innocarpe force-pushed the fix-17665-kitty-interrupt branch from 7f93f86 to 080b232 Compare October 4, 2026 12:50

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this commit — it closes the interrupt-intent and bracketed-paste recognition gap. The prior review thread stays open: two downstream Ctrl+C consumers (terminal-input-quarantine.ts, shell-command-inference.ts) still match only the literal ETX byte, so a Kitty-encoded interrupt is not fully recognized across the input path.

Reviewed changes

  • Shared Ctrl+C predicate — src/shared/terminal-interrupt-bytes.ts gains isTerminalInterruptInput(data), which recognizes both TERMINAL_INTERRUPT_ETX and TERMINAL_INTERRUPT_KITTY_CTRL_C as Ctrl+C.
  • Renderer recognizers routed through the predicate — interrupt-input-intent.ts now uses isTerminalInterruptInput in inputMatchesIntent, inferIntentFromExactTerminalInput, and observeAcceptedTerminalInput, so a Kitty Ctrl+C still records ctrl-c intent and marks bracketed paste stale on the fallback path where no keydown intent was captured.
  • Regression test for the non-Latin key path — pty-connection-interrupt-inference.test.ts dispatches ㅊ/KeyC (which isCtrlCKeyEvent rejects, so no pending intent is set), feeds the \x1b[99;5u bytes, and asserts inferInterrupt receives intent: 'ctrl-c' plus the bracketed-paste stale marking. Unit tests in terminal-interrupt-bytes.test.ts cover the predicate.
  • JSDoc tidy — shouldHandleTerminalInterruptKeyboardEvent's comment now describes ETX-or-Kitty selection.

Note the commit is correct for the consumers it touches; the open thread is not re-raised inline per the re-review dedup rule.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

terminal send --interrupt wrote a bare ETX. A TUI that enabled the kitty
keyboard protocol ignores that byte, so the CLI reported success while
the turn kept running. When the PTY's headless model has kitty flags,
write CSI 99;5u. Flags of 0 still send ETX.

Fixes stablyai#17665
…odel

A namespace change rebuilt the headless terminal from a provider snapshot and dropped the kitty keyboard flags, so a later interrupt sent ETX. The replacement now reapplies those flags, and the writer sends the interrupt bytes already chosen for that send.
Flag 4 does not change legacy Ctrl+C encoding, so an interrupt in that mode was sending CSI 99;5u instead of ETX.
…odel

A mounted renderer snapshot omits kitty pushes. The provider replacement
path already restores the flags carried beside the payload. Renderer
hydration now does the same, so an interrupt still encodes Ctrl+C.
A blank serialized buffer can still carry the keyboard flags beside the
payload. Hydration used to return before applying them, so a fresh
emulator stayed at 0 and an interrupt sent ETX.
@innocarpe
innocarpe force-pushed the fix-17665-kitty-interrupt branch from 080b232 to 17d74ca Compare October 4, 2026 15:07

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This run reviewed the delta since pullfrog review 5406270718 (080b232), which is head commit 17d74ca teaching the remaining downstream Ctrl+C consumers to recognize the Kitty encoding.

  • Shared contains predicate — src/shared/terminal-interrupt-bytes.ts gains containsTerminalInterruptInput(data), recognizing either Ctrl+C encoding anywhere in an accepted chunk (alongside the existing whole-string isTerminalInterruptInput).
  • Quarantine disarms on Kitty Ctrl+C — terminal-input-quarantine.ts's LINE_TERMINATORS now lists both TERMINAL_INTERRUPT_ETX and TERMINAL_INTERRUPT_KITTY_CTRL_C, so a Kitty interrupt drops the surviving line tail and disarms exactly as ETX did.
  • Shell-command inference recognizes Kitty Ctrl+C — shell-command-inference.ts routes its top-level reconfirmation and suspended-inference checks through containsTerminalInterruptInput, and its per-char scanner consumes the Kitty sequence before falling through to CSI parsing.
  • Pure-function extraction — observeAcceptedShellCommandInput is now an exported function over a ShellCommandInputState adapter, making the Ctrl+C handling directly unit-testable; installShellCommandInference binds it to the live session.
  • Regression tests — new pty-connection-shell-command-inference.test.ts covers both encodings for line reset and suspended-inference cancel; the quarantine suite adds a Kitty-tail case; terminal-interrupt-bytes.test.ts covers the contains predicate.

The prior open thread (Kitty Ctrl+C not recognized by terminal-input-quarantine.ts and shell-command-inference.ts) is closed by this commit. Focused tests pass locally: 42 across 4 files.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

This branch has not been deployed

No deployments
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.

[Bug]: terminal send --interrupt writes a bare 0x03 and is a no-op against TUIs using the kitty keyboard protocol (Claude Code)

1 participant