Skip to content

fix(terminal): map macOS Command+. to interrupt running command like Ctrl+C - #3409

Draft
netcatty-bot wants to merge 8 commits into
mainfrom
ai/issue-3408-35052760130
Draft

netcatty-bot wants to merge 8 commits into
mainfrom
ai/issue-3408-35052760130

Conversation

@netcatty-bot

Copy link
Copy Markdown
Collaborator

Summary

  • Adds the macOS Terminal convention ⌘. (Command-Period) as a terminal interrupt: pressing ⌘. in a connected terminal now sends the same urgent interrupt (SIGINT, \x03) that plain Ctrl+C sends, so long-running commands like tail -f can be stopped the way macOS users expect.
  • The new chord is recognized only when nothing is selected, so ⌘. never swallows a copy action, and it requires Meta with no other modifiers (Ctrl/Alt/Shift variants are left alone).
  • Layout handling follows the existing Ctrl+C interrupt logic: a printable ASCII layout character takes precedence, otherwise the physical Period key code is used, so non-Latin keyboard layouts still work.
  • The interrupt reuses the existing urgent-interrupt path in the xterm runtime, so it also aborts a pending sudo password assist, clears stale input state, and propagates to broadcast peers exactly like Ctrl+C.

Why

Issue #3408: on macOS, Terminal.app (and iTerm2) use ⌘. as the standard cancel/interrupt shortcut for a running command. Netcatty does not bind ⌘. to anything, so it is free to be mapped to the Ctrl+C interrupt without conflicting with existing shortcuts (verified against the default key-binding table).

Changes

  • components/terminal/runtime/terminalInterruptShortcut.ts: new pure helper isMacCommandPeriodInterruptChord recognizing the ⌘. chord.
  • components/terminal/runtime/createXTermRuntime.ts: the keydown handler's urgent-interrupt branch now also fires for ⌘. on macOS (via isMacPlatform()), going through the same interruptSession / \x03 / trace / broadcast flow as Ctrl+C.
  • components/terminal/runtime/terminalInterruptShortcut.test.ts: unit tests for the chord (modifier requirements, layout-character preference, physical Period fallback).

Testing

  • node --test --import tsx components/terminal/runtime/terminalInterruptShortcut.test.ts — 7 tests pass.
  • node --test --import tsx components/terminal/runtime/*.test.ts — 972 tests pass.
  • npx eslint on the three changed files — clean.
  • npx tsc --noEmit shows only pre-existing errors in createXTermRuntime.ts (verified identical on HEAD via git stash).
  • Manual check not run here (no macOS environment): on a Mac, connect a session, run tail -f <file>, press ⌘. — the command should abort just like Ctrl+C.

Fixes #3408

Automation

  • Automated implement pass
  • Review gate: @codex review (own/bot PRs only)
  • Draft until Codex reports clean findings

@netcatty-bot netcatty-bot added automation:bot-pr PR created or owned by Cursor automation automation:codex-loop Own/bot PR waiting on Codex review↔fix loop triage Touched by Cursor automation labels Sep 16, 2026
@binaricat

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-16T08:08:18.446123Z f8c64c8 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0ef88c085

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// macOS Terminal convention: ⌘. interrupts the running command like
// Ctrl+C (#3408). Only when nothing is selected so copy wins first.
const macCommandPeriodInterrupt =
!hasCopyableSelection && isMacPlatform() && isMacCommandPeriodInterruptChord(e);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize Command-period before Kitty forwarding

When the active application has negotiated Kitty event reporting, or input is broadcast to a Kitty-enabled peer, this condition enters the Ctrl+C branch while retaining the original Command-period event. The branch records and broadcasts that event, so the local keyup can emit an unmatched Super+Period release, while a peer encodes Super+Period and suppresses the following legacy \x03 instead of being interrupted. Forward a normalized Ctrl+C event for Kitty bookkeeping and broadcast rather than the original chord.

Useful? React with 👍 / 👎.

@binaricat

Copy link
Copy Markdown
Owner

@codex review

Repository owner deleted a comment from binaricat Sep 16, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 789a5ef2ec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

} as unknown as KeyboardEvent
: e;
const kittyEvent = toKittyKeyboardEvent(interruptEventForKitty);
const identity = kittyKeyIdentity(interruptEventForKitty);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pair the normalized Ctrl+C press with the Period keyup

When Kitty event reporting is enabled locally or on a broadcast peer, this records the normalized press under KeyC, but the browser later delivers the physical release as Period; the keyup handler looks up only e.code || e.key, so it neither removes this entry nor sends the paired Ctrl+C release. Although the prior Super+Period mismatch is now normalized, the new identity mismatch leaves Ctrl+C logically held for Kitty consumers; retain a mapping from the physical chord to the normalized event or synthesize its release.

Useful? React with 👍 / 👎.

@binaricat

Copy link
Copy Markdown
Owner

@codex review

Repository owner deleted a comment from binaricat Sep 16, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 671ce69519

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

kittyNormalizedPressAliases.delete(physicalIdentity);
return null;
}
return forwardedPress.event;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove the normalized alias after its paired keyup

After a Command-period interrupt with local or broadcast Kitty release tracking, a successful lookup returns without deleting the Period -> KeyC alias. Until focus loss or reconnection, a later Period keyup while another KeyC press is outstanding—for example, overlapping c and . while typing in a report-all TUI—is therefore rewritten as a KeyC release, prematurely releasing C and leaving Period logically pressed until transient state is flushed. Consume the alias on the successful path as well as when no forwarded press is found.

Useful? React with 👍 / 👎.

@binaricat

Copy link
Copy Markdown
Owner

@codex review

Repository owner deleted a comment from binaricat Sep 16, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

upsertKittyKeyboardForwardedPress(
kittyForwardedKeys,
identity,
kittyEvent,
[],

P2 Badge Keep normalized interrupt presses separate from physical KeyC

When a Kitty release-reporting session already has the physical C key held, pressing Command+Period upserts the normalized interrupt under the same KeyC entry and replaces that outstanding press (the broadcast map has the same collision). The Period keyup then releases and deletes this shared entry while physical C is still down, and the eventual C keyup has no press to pair with, producing premature or modifier-mismatched releases in report-all TUIs. The new alias deletion only fixes a later stale Period keyup; it does not prevent this simultaneous identity collision, so the normalized press needs independently keyed/stacked bookkeeping.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@binaricat

Copy link
Copy Markdown
Owner

@codex review

Repository owner deleted a comment from binaricat Sep 16, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

const forwarded = broadcastKittyInput({ kind: "key", event: kittyEvent });

P2 Badge Carry the dedicated identity through broadcast pairing

With input broadcasting enabled, holding a physical C key and then pressing Command-period still collapses both presses on each peer because this dispatch sends only the normalized event whose code is KeyC; the new pressIdentity exists solely in the source-side maps, while resolveKittyKeyboardBroadcastInput() keys peer state from input.event.code. On a legacy peer the outstanding C entry can suppress the following urgent \x03 as duplicate, and on a Kitty peer the first release clears the shared pairing entry. Fresh evidence after the local alias fix is that no distinct correlation identity crosses this broadcast call, so it must be propagated and used by peer pairing.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2042 to +2044
const macCommandPeriodInterrupt =
!hasCopyableSelection && isMacPlatform() && isMacCommandPeriodInterruptChord(e);
if (urgentInterrupt || macCommandPeriodInterrupt) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reserve Command-period before accepting custom shortcuts

On macOS, a user can still assign Meta+. to a snippet or configurable terminal action, but this branch executes before both the snippet loop and checkAppShortcut() and returns after sending an interrupt. The snippet editors accept this chord because findActiveSystemShortcutConflict() only checks configured key bindings and this new hard-coded shortcut was not registered there, so the UI can save a shortcut that silently becomes unusable; either reject/reserve this chord during validation or give configured shortcuts precedence.

Useful? React with 👍 / 👎.

Comment on lines +1820 to +1824
releaseEvent = {
...aliasedRelease.event,
type: "keyup",
} as unknown as KeyboardEvent;
aliasedReleaseIdentity = aliasedRelease.identity;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep aliased releases out of physical Win32 tracking

When the source terminal has negotiated Win32 input mode, broadcasting is enabled, and a physical KeyC remains held while Command-period is pressed, the Period keyup is rewritten here to the normalized KeyC event. The subsequent win32InputModeForwardedKeys.delete(identity) therefore consumes the outstanding physical C press and returns true, causing a premature native C release while the eventual real C keyup is dropped. Fresh evidence after the source-map alias fix is that the separate win32InputModeForwardedKeys lookup still uses the rewritten identity; preserve the physical e identity for that lookup and suppress the aliased interrupt's native keyup.

Useful? React with 👍 / 👎.

@binaricat

Copy link
Copy Markdown
Owner

@codex review

Repository owner deleted a comment from binaricat Sep 16, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 15f209d24d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2128 to +2129
key: "c",
code: "KeyC",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid layout lookup for synthesized Ctrl+C

When macOS uses a non-QWERTY layout (for example, Dvorak), assigning the synthesized event code: "KeyC" makes toKittyKeyboardEvent() obtain that physical key's layout character and getUnicodeKeyCode() prioritizes it. The normalized interrupt can therefore encode as Ctrl+J (or another layout character) instead of Ctrl+C; a Kitty-enabled broadcast peer then suppresses the legacy \x03 fallback and is not interrupted. Construct the normalized Kitty event without unrelated layout metadata, or explicitly force its unshifted character to c.

Useful? React with 👍 / 👎.

@binaricat

Copy link
Copy Markdown
Owner

@codex review

Repository owner deleted a comment from binaricat Sep 16, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1c1fcf3995

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

win32InputModePendingEvent = null;
win32InputModeForwardedKeys.clear();
kittyForwardedKeys.clear();
kittyNormalizedPressAliases.clear();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve normalized aliases across connection resets

When a source session reconnects between a Command-period keydown and its Period keyup, Terminal.tsx calls resetKittyConnectionInputState() while broadcastForwardedKeys is intentionally retained for pending peer releases. Clearing only this alias makes the later physical keyup unable to find the dedicated normalized identity, so broadcast Kitty/Win32 peers can retain Ctrl+C as logically pressed until another blur flushes the stale entry. Preserve aliases that still have a broadcast press, or flush those presses before clearing them.

Useful? React with 👍 / 👎.

// can pair the release.
const kittyNormalizedPressAliases = new Map<string, string>();
const kittyNormalizedPressIdentity = (identity: string): string =>
`${identity}mac-period-interrupt`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Escape the separator instead of embedding a NUL byte

This template literal contains a literal NUL byte, which causes default repository text-search tooling to classify createXTermRuntime.ts as binary (rg reports only “binary file matches” and suppresses matching lines). That makes searches and text-oriented automation silently miss this large runtime file; use an escaped separator such as \0 or a printable sentinel so the source remains ordinary searchable text.

Useful? React with 👍 / 👎.

@binaricat

Copy link
Copy Markdown
Owner

@codex review

Repository owner deleted a comment from binaricat Sep 16, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8c64c879f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1821 to +1827
const aliasedRelease = resolveKittyNormalizedPressRelease(e);
if (aliasedRelease) {
releaseEvent = {
...aliasedRelease.event,
type: "keyup",
} as unknown as KeyboardEvent;
aliasedReleaseIdentity = aliasedRelease.identity;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve a held Period press when normalizing the interrupt

If Period is already held and auto-repeating when Command is pressed, its original keydown has been forwarded under Period, while the repeated keydown creates this normalized interrupt alias. The eventual physical keyup is then replaced exclusively with the Ctrl+C release: Kitty and broadcast paths leave the original Period press pending until blur, and Win32 deletes the Period entry but emits a Ctrl+C keyup instead, leaving ConPTY's Period logically held. The aliased keyup must release both the pre-existing physical press and the normalized interrupt press, or repeated keydowns must not create the alias in this situation.

Useful? React with 👍 / 👎.

@netcatty-bot

Copy link
Copy Markdown
Collaborator Author

The automatic Codex fix was preserved, but it did not pass verification. The PR remains draft for maintainer review.

The candidate patch and verification report were preserved as codex-fix-patch-35072855449.

View this run: https://github.com/binaricat/Netcatty/actions/runs/35072855449

@netcatty-bot netcatty-bot added ready-for-human Requires human implementation and removed automation:codex-loop Own/bot PR waiting on Codex review↔fix loop labels Sep 16, 2026
Repository owner deleted a comment from binaricat Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automation:bot-pr PR created or owned by Cursor automation ready-for-human Requires human implementation triage Touched by Cursor automation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 将Mac 常用的 Command+. 中断执行映射为 CTRL+C

2 participants