From 1be5f7e8e915a670de89fd8b723156667481e1d7 Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Thu, 28 May 2026 14:21:56 -0700 Subject: [PATCH 1/3] Mirror VS Code workbench shortcuts in terminal --- docs/specs/layout.md | 2 + docs/specs/shortcuts.md | 3 ++ docs/specs/transport.md | 2 + docs/specs/vscode.md | 1 + lib/src/lib/platform/index.ts | 4 ++ lib/src/lib/platform/types.ts | 4 ++ lib/src/lib/platform/vscode-adapter.test.ts | 11 +++++ lib/src/lib/platform/vscode-adapter.ts | 5 +++ lib/src/lib/terminal-lifecycle.ts | 14 ++++++- lib/src/lib/vscode-keybindings.test.ts | 46 +++++++++++++++++++++ lib/src/lib/vscode-keybindings.ts | 43 +++++++++++++++++++ vscode-ext/src/message-router.ts | 10 +++++ vscode-ext/src/message-types.ts | 2 + 13 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 lib/src/lib/vscode-keybindings.test.ts create mode 100644 lib/src/lib/vscode-keybindings.ts diff --git a/docs/specs/layout.md b/docs/specs/layout.md index 7e3e7f3a..6d2b84ed 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -140,6 +140,7 @@ Wall starts in `command` mode by default. Embedders may pass `initialMode="passt ### Passthrough mode - All keyboard input routes to the active session's xterm.js instance - Only the mode-exit gesture (LCmd → RCmd) is intercepted +- In the VS Code host, selected workbench chords are mirrored: xterm still processes the key, and Dormouse also asks the extension host to run the matching VS Code command. The allowlist is `Ctrl/Cmd+P` (Quick Open), `Ctrl/Cmd+Shift+P` / `F1` (Command Palette), and `Ctrl/Cmd+B` (toggle sidebar). - Selection overlay shows 2px solid border with glow - Terminal has DOM focus @@ -375,6 +376,7 @@ The deferred spawn also only calls `selectPane` if selection is null. The kill h | `lib/src/components/wall/MouseOverrideBanner.tsx` | Temporary mouse override banner shown from the header icon | | `lib/src/components/wall/use-dockview-ready.ts` | Dockview ready/setup handler: restore/create panels, DnD swap wiring, active panel sync, auto-spawn | | `lib/src/components/wall/use-wall-keyboard.ts` | Capture-phase keyboard dispatch for mode switching, pane/door commands, copy/paste, selection drag keys | +| `lib/src/lib/vscode-keybindings.ts` | VS Code-hosted workbench chord mirror allowlist | | `lib/src/components/wall/use-session-persistence.ts` | Debounced layout/session save, flush requests, pagehide, PTY exit, file-drop paste routing | | `lib/src/components/wall/use-window-focused.ts` | Window focus tracking hook for header and selection overlay dimming | | `lib/src/components/Baseboard.tsx` | Always-visible bottom strip with door components, overflow arrows, and shortcut hints | diff --git a/docs/specs/shortcuts.md b/docs/specs/shortcuts.md index 90d48f96..0b220e95 100644 --- a/docs/specs/shortcuts.md +++ b/docs/specs/shortcuts.md @@ -3,9 +3,12 @@ Complete reference for Dormouse's keyboard shortcuts. Shortcuts are grouped by the mode/context in which they apply. Dormouse has two modes: + - **Workspace mode** (a.k.a. "command" mode internally) — keys drive pane layout. - **Terminal mode** (a.k.a. "passthrough" mode) — keys go to the running program, except copy/paste and the mode-switch gesture. +In the VS Code extension host, `Ctrl/Cmd+P`, `Ctrl/Cmd+Shift+P`, `Ctrl/Cmd+B`, and `F1` are mirrored: the terminal receives the key, and Dormouse also runs the matching VS Code workbench command. + ## Mode switching | Key | Action | Description | diff --git a/docs/specs/transport.md b/docs/specs/transport.md index 1f74af28..2d0098bc 100644 --- a/docs/specs/transport.md +++ b/docs/specs/transport.md @@ -75,6 +75,8 @@ Source of truth: Non-obvious message contracts: +VS Code-only workbench chord mirroring uses `dormouse:runWorkbenchCommand` from webview to host. The host validates the requested command against a hardcoded allowlist before calling `vscode.commands.executeCommand`; generic command execution over the webview boundary is not allowed. + | Direction | Message | Source type | Contract | | --- | --- | --- | --- | | Webview → host | `dormouse:openExternal` | `WebviewMessage` | Request the host to open a user-confirmed external URI from an OSC 8 hyperlink. Hosts must revalidate and reject malformed, control-character-bearing, or blocked pseudo-scheme targets (`javascript:`, `data:`, `blob:`, `about:`). | diff --git a/docs/specs/vscode.md b/docs/specs/vscode.md index b4683d72..e832849e 100644 --- a/docs/specs/vscode.md +++ b/docs/specs/vscode.md @@ -71,6 +71,7 @@ Universal PTY/transport invariants live in `docs/specs/transport.md`. The rules - **mergeAlertStates on every save path.** Both the frontend periodic save (`onSaveState` callback) and the backend deactivate refresh (`refreshSavedSessionStateFromPtys`) must merge current alert states. Missing this causes alert state to revert on restore. - **retainContextWhenHidden.** Set on both `WebviewPanel` (editor tabs) and `WebviewView` (bottom panel) so that xterm.js DOM, scrollback, and PTY subscriptions survive panel hide/show without going through a resume. - **Two save sources.** Session state is saved from two places: the frontend (debounced 500ms + 30s interval via `dormouse:saveState`) and the backend (deactivate flushes webviews then refreshes from live PTYs). Both paths must produce consistent state. +- **Workbench keybindings mirror for selected chords.** `lib/src/lib/vscode-keybindings.ts` is the source of truth for the VS Code-hosted mirror allowlist. For `Ctrl/Cmd+P`, `Ctrl/Cmd+Shift+P`, `Ctrl/Cmd+B`, and `F1`, xterm still processes the key while the webview also posts `dormouse:runWorkbenchCommand`; `message-router.ts` validates that request against the same small command set before calling `vscode.commands.executeCommand`. ### Extension manifest (current) diff --git a/lib/src/lib/platform/index.ts b/lib/src/lib/platform/index.ts index 3b0a544b..4c0decda 100644 --- a/lib/src/lib/platform/index.ts +++ b/lib/src/lib/platform/index.ts @@ -46,6 +46,10 @@ export function getPlatform(): PlatformAdapter { return adapter; } +export function isVSCodePlatform(): boolean { + return adapter instanceof VSCodeAdapter || (typeof acquireVsCodeApi === 'function' && !(adapter instanceof FakePtyAdapter)); +} + export function initPlatform(override: 'fake'): FakePtyAdapter; export function initPlatform(): PlatformAdapter; export function initPlatform(override?: 'fake'): PlatformAdapter { diff --git a/lib/src/lib/platform/types.ts b/lib/src/lib/platform/types.ts index ffa45d47..b131b2c6 100644 --- a/lib/src/lib/platform/types.ts +++ b/lib/src/lib/platform/types.ts @@ -1,4 +1,5 @@ import type { AlertState } from '../alert-manager'; +import type { VSCodeWorkbenchCommand } from '../vscode-keybindings'; export interface PtyInfo { id: string; @@ -40,6 +41,9 @@ export interface PlatformAdapter { // terminal output is untrusted. openExternal?(uri: string): void; + // VS Code-only escape hatch for mirrored workbench shortcuts from webviews. + runWorkbenchCommand?(command: VSCodeWorkbenchCommand): void; + // PTY event listeners onPtyData(handler: (detail: { id: string; data: string }) => void): void; offPtyData(handler: (detail: { id: string; data: string }) => void): void; diff --git a/lib/src/lib/platform/vscode-adapter.test.ts b/lib/src/lib/platform/vscode-adapter.test.ts index 6260a1cf..5a6db36c 100644 --- a/lib/src/lib/platform/vscode-adapter.test.ts +++ b/lib/src/lib/platform/vscode-adapter.test.ts @@ -80,6 +80,17 @@ describe('VSCodeAdapter PTY exit handling', () => { }); }); + it('posts allowlisted VS Code workbench commands to the extension host', () => { + const adapter = new VSCodeAdapter(); + + adapter.runWorkbenchCommand('workbench.action.quickOpen'); + + expect(postMessage).toHaveBeenCalledWith({ + type: 'dormouse:runWorkbenchCommand', + command: 'workbench.action.quickOpen', + }); + }); + it('parses replay buffers into semantic events and strips OSCs before forwarding', () => { const adapter = new VSCodeAdapter(); const replays: Array<{ id: string; data: string }> = []; diff --git a/lib/src/lib/platform/vscode-adapter.ts b/lib/src/lib/platform/vscode-adapter.ts index 2bf4c65e..578fc766 100644 --- a/lib/src/lib/platform/vscode-adapter.ts +++ b/lib/src/lib/platform/vscode-adapter.ts @@ -7,6 +7,7 @@ import { import { applyTerminalSemanticEventsByPtyId, } from '../terminal-state-store'; +import type { VSCodeWorkbenchCommand } from '../vscode-keybindings'; export class VSCodeAdapter implements PlatformAdapter { private vscode: ReturnType; @@ -181,6 +182,10 @@ export class VSCodeAdapter implements PlatformAdapter { this.vscode.postMessage({ type: 'dormouse:openExternal', uri }); } + runWorkbenchCommand(command: VSCodeWorkbenchCommand): void { + this.vscode.postMessage({ type: 'dormouse:runWorkbenchCommand', command }); + } + onPtyData(handler: (detail: { id: string; data: string }) => void): void { this.dataHandlers.add(handler); } diff --git a/lib/src/lib/terminal-lifecycle.ts b/lib/src/lib/terminal-lifecycle.ts index 2ed1809a..15a51ad3 100644 --- a/lib/src/lib/terminal-lifecycle.ts +++ b/lib/src/lib/terminal-lifecycle.ts @@ -1,7 +1,7 @@ import { Terminal, type IBufferRange } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; import { UnicodeGraphemesAddon } from '@xterm/addon-unicode-graphemes'; -import { getPlatform } from './platform'; +import { getPlatform, IS_MAC, isVSCodePlatform } from './platform'; import { requestExternalLinkConfirmation } from './external-link-confirmation'; import { attachMouseModeObserver } from './mouse-mode-observer'; import { @@ -43,6 +43,7 @@ import { } from './terminal-state-store'; import { readLogicalLineFromBuffer, type BufferLike } from './terminal-buffer-read'; import { UNNAMED_PANEL_TITLE } from './terminal-state'; +import { vscodeWorkbenchCommandForKeydown } from './vscode-keybindings'; function makePromptLineReader(terminal: Terminal): PromptLineReader { return { @@ -115,6 +116,17 @@ function createXtermHost(): { terminal: Terminal; fit: FitAddon; element: HTMLDi }, }); + if (isVSCodePlatform()) { + terminal.attachCustomKeyEventHandler((event) => { + const command = vscodeWorkbenchCommandForKeydown(event, { isMac: IS_MAC }); + if (!command) return true; + event.preventDefault(); + event.stopPropagation(); + getPlatform().runWorkbenchCommand?.(command); + return true; + }); + } + terminal.loadAddon(new UnicodeGraphemesAddon()); const fit = new FitAddon(); terminal.loadAddon(fit); diff --git a/lib/src/lib/vscode-keybindings.test.ts b/lib/src/lib/vscode-keybindings.test.ts new file mode 100644 index 00000000..d0fe7231 --- /dev/null +++ b/lib/src/lib/vscode-keybindings.test.ts @@ -0,0 +1,46 @@ +/** + * @vitest-environment jsdom + */ +import { describe, expect, it } from 'vitest'; +import { vscodeWorkbenchCommandForKeydown } from './vscode-keybindings'; + +function keydown(init: Partial & { key: string }): KeyboardEvent { + return new KeyboardEvent('keydown', { + bubbles: true, + cancelable: true, + ...init, + }); +} + +describe('vscodeWorkbenchCommandForKeydown', () => { + it('maps Windows/Linux VS Code workbench chords', () => { + const opts = { isMac: false }; + + expect(vscodeWorkbenchCommandForKeydown(keydown({ key: 'p', code: 'KeyP', ctrlKey: true }), opts)).toBe('workbench.action.quickOpen'); + expect(vscodeWorkbenchCommandForKeydown(keydown({ key: 'P', code: 'KeyP', ctrlKey: true, shiftKey: true }), opts)).toBe('workbench.action.showCommands'); + expect(vscodeWorkbenchCommandForKeydown(keydown({ key: 'b', code: 'KeyB', ctrlKey: true }), opts)).toBe('workbench.action.toggleSidebarVisibility'); + expect(vscodeWorkbenchCommandForKeydown(keydown({ key: 'F1', code: 'F1' }), opts)).toBe('workbench.action.showCommands'); + }); + + it('uses Cmd as the platform modifier on macOS', () => { + const opts = { isMac: true }; + + expect(vscodeWorkbenchCommandForKeydown(keydown({ key: 'p', code: 'KeyP', metaKey: true }), opts)).toBe('workbench.action.quickOpen'); + expect(vscodeWorkbenchCommandForKeydown(keydown({ key: 'P', code: 'KeyP', metaKey: true, shiftKey: true }), opts)).toBe('workbench.action.showCommands'); + expect(vscodeWorkbenchCommandForKeydown(keydown({ key: 'b', code: 'KeyB', metaKey: true }), opts)).toBe('workbench.action.toggleSidebarVisibility'); + expect(vscodeWorkbenchCommandForKeydown(keydown({ key: 'p', code: 'KeyP', ctrlKey: true }), opts)).toBe(null); + }); + + it('keeps unrelated terminal control chords in xterm only', () => { + const opts = { isMac: false }; + + expect(vscodeWorkbenchCommandForKeydown(keydown({ key: 'r', code: 'KeyR', ctrlKey: true }), opts)).toBe(null); + expect(vscodeWorkbenchCommandForKeydown(keydown({ key: 'c', code: 'KeyC', ctrlKey: true }), opts)).toBe(null); + expect(vscodeWorkbenchCommandForKeydown(keydown({ key: 'p', code: 'KeyP', ctrlKey: true, altKey: true }), opts)).toBe(null); + }); + + it('only maps keydown events', () => { + const event = new KeyboardEvent('keyup', { key: 'p', code: 'KeyP', ctrlKey: true }); + expect(vscodeWorkbenchCommandForKeydown(event, { isMac: false })).toBe(null); + }); +}); diff --git a/lib/src/lib/vscode-keybindings.ts b/lib/src/lib/vscode-keybindings.ts new file mode 100644 index 00000000..7089c43d --- /dev/null +++ b/lib/src/lib/vscode-keybindings.ts @@ -0,0 +1,43 @@ +type KeyboardEventLike = Pick< + KeyboardEvent, + 'altKey' | 'code' | 'ctrlKey' | 'isComposing' | 'key' | 'metaKey' | 'shiftKey' | 'type' +>; + +export type VSCodeWorkbenchCommand = + | 'workbench.action.quickOpen' + | 'workbench.action.showCommands' + | 'workbench.action.toggleSidebarVisibility'; + +/** + * Xterm keyboard handling changes when foreground apps enable enhanced + * keyboard protocols, which makes VS Code workbench chords inconsistent. For + * the allowlisted chords, Dormouse lets xterm keep processing the key and also + * asks the VS Code host to run the matching workbench command. + */ +export function vscodeWorkbenchCommandForKeydown( + event: KeyboardEventLike, + options: { isMac: boolean }, +): VSCodeWorkbenchCommand | null { + if (event.type !== 'keydown') return null; + if (event.isComposing) return null; + + if (!event.ctrlKey && !event.metaKey && !event.altKey && event.key === 'F1') { + return 'workbench.action.showCommands'; + } + + const platformMod = options.isMac ? event.metaKey : event.ctrlKey; + if (!platformMod || event.altKey) return null; + + const key = event.key.toLowerCase(); + const isP = key === 'p' || event.code === 'KeyP'; + if (isP) { + return event.shiftKey + ? 'workbench.action.showCommands' + : 'workbench.action.quickOpen'; + } + + const isB = key === 'b' || event.code === 'KeyB'; + if (isB && !event.shiftKey) return 'workbench.action.toggleSidebarVisibility'; + + return null; +} diff --git a/vscode-ext/src/message-router.ts b/vscode-ext/src/message-router.ts index 28f29484..fee95d30 100644 --- a/vscode-ext/src/message-router.ts +++ b/vscode-ext/src/message-router.ts @@ -23,6 +23,11 @@ const clipboardOps = require('../../lib/clipboard-ops.cjs') as { const globalOwnedPtyIds = new Set(); const activeRouters = new Set<{ flushSessionSave(timeoutMs?: number): Promise }>(); let nextFlushRequestId = 0; +const ALLOWED_WORKBENCH_COMMANDS = new Set([ + 'workbench.action.quickOpen', + 'workbench.action.showCommands', + 'workbench.action.toggleSidebarVisibility', +]); // Shared alert manager — survives router disposal so alert state persists // across webview collapse/expand cycles. @@ -282,6 +287,11 @@ export function attachRouter( ); break; } + case 'dormouse:runWorkbenchCommand': + if (ALLOWED_WORKBENCH_COMMANDS.has(msg.command)) { + void vscode.commands.executeCommand(msg.command); + } + break; case 'dormouse:init': { // Webview has (re-)initialized — subscribe to live events. // Tear down previous subscriptions first (webview was destroyed and recreated). diff --git a/vscode-ext/src/message-types.ts b/vscode-ext/src/message-types.ts index 278f9d23..95e18f3a 100644 --- a/vscode-ext/src/message-types.ts +++ b/vscode-ext/src/message-types.ts @@ -1,5 +1,6 @@ import type { ActivityNotification, SessionStatus, TodoState } from '../../lib/src/lib/alert-manager'; import type { TerminalSemanticEvent } from '../../lib/src/lib/terminal-state'; +import type { VSCodeWorkbenchCommand } from '../../lib/src/lib/vscode-keybindings'; // Messages from webview → extension host export type WebviewMessage = @@ -13,6 +14,7 @@ export type WebviewMessage = | { type: 'clipboard:readFiles'; requestId: string } | { type: 'clipboard:readImage'; requestId: string } | { type: 'dormouse:openExternal'; uri: string } + | { type: 'dormouse:runWorkbenchCommand'; command: VSCodeWorkbenchCommand } | { type: 'dormouse:init' } | { type: 'dormouse:saveState'; state: unknown } | { type: 'dormouse:flushSessionSaveDone'; requestId: string } From 965bb52a1c46b85c406816fdee8e33cec553a5f0 Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Thu, 28 May 2026 14:44:34 -0700 Subject: [PATCH 2/3] Consolidate workbench-chord allowlist onto single sources of truth Code: derive the host-side allowlist Set from the new VSCODE_WORKBENCH_COMMANDS const in vscode-keybindings.ts (and the VSCodeWorkbenchCommand type from it too), so the command list is no longer enumerated separately in message-router.ts. Detection: drop isVSCodePlatform() and gate the keydown handler on the presence of getPlatform().runWorkbenchCommand, matching the existing openExternal?.() optional-method escape-hatch pattern. Docs: enumerate the mirrored chords only in vscode.md (which names the code source of truth); layout.md, shortcuts.md, and transport.md now link there instead of re-listing the keys. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/specs/layout.md | 2 +- docs/specs/shortcuts.md | 2 +- docs/specs/transport.md | 2 +- lib/src/lib/platform/index.ts | 4 ---- lib/src/lib/terminal-lifecycle.ts | 7 +++++-- lib/src/lib/vscode-keybindings.ts | 12 ++++++++---- vscode-ext/src/message-router.ts | 7 ++----- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/specs/layout.md b/docs/specs/layout.md index 6d2b84ed..a137d925 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -140,7 +140,7 @@ Wall starts in `command` mode by default. Embedders may pass `initialMode="passt ### Passthrough mode - All keyboard input routes to the active session's xterm.js instance - Only the mode-exit gesture (LCmd → RCmd) is intercepted -- In the VS Code host, selected workbench chords are mirrored: xterm still processes the key, and Dormouse also asks the extension host to run the matching VS Code command. The allowlist is `Ctrl/Cmd+P` (Quick Open), `Ctrl/Cmd+Shift+P` / `F1` (Command Palette), and `Ctrl/Cmd+B` (toggle sidebar). +- In the VS Code host, selected workbench chords are mirrored: xterm still processes the key, and Dormouse also asks the extension host to run the matching VS Code command. See [the VS Code host spec](vscode.md) for the allowlist. - Selection overlay shows 2px solid border with glow - Terminal has DOM focus diff --git a/docs/specs/shortcuts.md b/docs/specs/shortcuts.md index 0b220e95..43a41f18 100644 --- a/docs/specs/shortcuts.md +++ b/docs/specs/shortcuts.md @@ -7,7 +7,7 @@ Dormouse has two modes: - **Workspace mode** (a.k.a. "command" mode internally) — keys drive pane layout. - **Terminal mode** (a.k.a. "passthrough" mode) — keys go to the running program, except copy/paste and the mode-switch gesture. -In the VS Code extension host, `Ctrl/Cmd+P`, `Ctrl/Cmd+Shift+P`, `Ctrl/Cmd+B`, and `F1` are mirrored: the terminal receives the key, and Dormouse also runs the matching VS Code workbench command. +In the VS Code extension host, selected workbench chords are mirrored: the terminal receives the key, and Dormouse also runs the matching VS Code workbench command. See [the VS Code host spec](vscode.md) for the exact allowlist. ## Mode switching diff --git a/docs/specs/transport.md b/docs/specs/transport.md index 2d0098bc..823810c6 100644 --- a/docs/specs/transport.md +++ b/docs/specs/transport.md @@ -75,7 +75,7 @@ Source of truth: Non-obvious message contracts: -VS Code-only workbench chord mirroring uses `dormouse:runWorkbenchCommand` from webview to host. The host validates the requested command against a hardcoded allowlist before calling `vscode.commands.executeCommand`; generic command execution over the webview boundary is not allowed. +VS Code-only workbench chord mirroring uses `dormouse:runWorkbenchCommand` from webview to host. The host validates the requested command against the allowlist in `lib/src/lib/vscode-keybindings.ts` (see [the VS Code host spec](vscode.md)) before calling `vscode.commands.executeCommand`; generic command execution over the webview boundary is not allowed. | Direction | Message | Source type | Contract | | --- | --- | --- | --- | diff --git a/lib/src/lib/platform/index.ts b/lib/src/lib/platform/index.ts index 4c0decda..3b0a544b 100644 --- a/lib/src/lib/platform/index.ts +++ b/lib/src/lib/platform/index.ts @@ -46,10 +46,6 @@ export function getPlatform(): PlatformAdapter { return adapter; } -export function isVSCodePlatform(): boolean { - return adapter instanceof VSCodeAdapter || (typeof acquireVsCodeApi === 'function' && !(adapter instanceof FakePtyAdapter)); -} - export function initPlatform(override: 'fake'): FakePtyAdapter; export function initPlatform(): PlatformAdapter; export function initPlatform(override?: 'fake'): PlatformAdapter { diff --git a/lib/src/lib/terminal-lifecycle.ts b/lib/src/lib/terminal-lifecycle.ts index 15a51ad3..76f254b5 100644 --- a/lib/src/lib/terminal-lifecycle.ts +++ b/lib/src/lib/terminal-lifecycle.ts @@ -1,7 +1,7 @@ import { Terminal, type IBufferRange } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; import { UnicodeGraphemesAddon } from '@xterm/addon-unicode-graphemes'; -import { getPlatform, IS_MAC, isVSCodePlatform } from './platform'; +import { getPlatform, IS_MAC } from './platform'; import { requestExternalLinkConfirmation } from './external-link-confirmation'; import { attachMouseModeObserver } from './mouse-mode-observer'; import { @@ -116,7 +116,10 @@ function createXtermHost(): { terminal: Terminal; fit: FitAddon; element: HTMLDi }, }); - if (isVSCodePlatform()) { + // Only hosts that can run workbench commands (the VS Code adapter) opt in; + // on every other platform runWorkbenchCommand is undefined, so the chords + // stay in xterm exactly as before. + if (getPlatform().runWorkbenchCommand) { terminal.attachCustomKeyEventHandler((event) => { const command = vscodeWorkbenchCommandForKeydown(event, { isMac: IS_MAC }); if (!command) return true; diff --git a/lib/src/lib/vscode-keybindings.ts b/lib/src/lib/vscode-keybindings.ts index 7089c43d..22721886 100644 --- a/lib/src/lib/vscode-keybindings.ts +++ b/lib/src/lib/vscode-keybindings.ts @@ -3,10 +3,14 @@ type KeyboardEventLike = Pick< 'altKey' | 'code' | 'ctrlKey' | 'isComposing' | 'key' | 'metaKey' | 'shiftKey' | 'type' >; -export type VSCodeWorkbenchCommand = - | 'workbench.action.quickOpen' - | 'workbench.action.showCommands' - | 'workbench.action.toggleSidebarVisibility'; +/** The workbench commands the VS Code host is allowed to run on the webview's behalf. */ +export const VSCODE_WORKBENCH_COMMANDS = [ + 'workbench.action.quickOpen', + 'workbench.action.showCommands', + 'workbench.action.toggleSidebarVisibility', +] as const; + +export type VSCodeWorkbenchCommand = (typeof VSCODE_WORKBENCH_COMMANDS)[number]; /** * Xterm keyboard handling changes when foreground apps enable enhanced diff --git a/vscode-ext/src/message-router.ts b/vscode-ext/src/message-router.ts index fee95d30..ea319150 100644 --- a/vscode-ext/src/message-router.ts +++ b/vscode-ext/src/message-router.ts @@ -8,6 +8,7 @@ import { TerminalProtocolParser, } from '../../lib/src/lib/terminal-protocol'; import { normalizeExternalUri } from '../../lib/src/lib/external-links'; +import { VSCODE_WORKBENCH_COMMANDS } from '../../lib/src/lib/vscode-keybindings'; import type { TerminalSemanticEvent } from '../../lib/src/lib/terminal-state'; import type { PersistedSession } from '../../lib/src/lib/session-types'; import type { WebviewMessage, ExtensionMessage } from './message-types'; @@ -23,11 +24,7 @@ const clipboardOps = require('../../lib/clipboard-ops.cjs') as { const globalOwnedPtyIds = new Set(); const activeRouters = new Set<{ flushSessionSave(timeoutMs?: number): Promise }>(); let nextFlushRequestId = 0; -const ALLOWED_WORKBENCH_COMMANDS = new Set([ - 'workbench.action.quickOpen', - 'workbench.action.showCommands', - 'workbench.action.toggleSidebarVisibility', -]); +const ALLOWED_WORKBENCH_COMMANDS = new Set(VSCODE_WORKBENCH_COMMANDS); // Shared alert manager — survives router disposal so alert state persists // across webview collapse/expand cycles. From e878eb825b0e2360cd5a561e4056d4e11d67819f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 28 May 2026 15:12:57 -0700 Subject: [PATCH 3/3] Exclude accidental inclusion of `Shift+F1` when we only want bare `F1` Co-authored-by: dormouse-bot --- lib/src/lib/vscode-keybindings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/lib/vscode-keybindings.ts b/lib/src/lib/vscode-keybindings.ts index 22721886..2b9cc400 100644 --- a/lib/src/lib/vscode-keybindings.ts +++ b/lib/src/lib/vscode-keybindings.ts @@ -25,7 +25,7 @@ export function vscodeWorkbenchCommandForKeydown( if (event.type !== 'keydown') return null; if (event.isComposing) return null; - if (!event.ctrlKey && !event.metaKey && !event.altKey && event.key === 'F1') { + if (!event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey && event.key === 'F1') { return 'workbench.action.showCommands'; }