From 49e198e52d46631678867661829bd31317866790 Mon Sep 17 00:00:00 2001 From: Hako Date: Thu, 8 Oct 2026 20:37:00 +0900 Subject: [PATCH 1/3] fix(shortcuts): align custom bindings and mobile key defaults --- CHANGELOG.md | 6 + DESIGN.md | 17 ++- docs/guide.md | 18 ++- docs/screenshots/shortcuts/key-bar-phone.png | Bin 0 -> 20113 bytes docs/screenshots/shortcuts/settings-phone.png | Bin 0 -> 54729 bytes docs/terminal-input.md | 13 +- .../key-bar-customization-demo-regression.ts | 144 +++++++++++++++++- src/AGENTS.md | 3 +- src/components/CommandPalette.tsx | 10 +- src/components/SettingsDialog.css | 4 +- src/components/SettingsDialog.tsx | 53 ++++--- src/lib/i18n.ja.ts | 6 +- src/lib/i18n.ko.ts | 6 +- src/lib/i18n.zh.ts | 6 +- src/lib/keyBar.test.ts | 13 +- src/lib/keyBar.ts | 7 +- src/lib/settings.test.ts | 4 +- src/lib/settings.ts | 5 +- src/lib/shortcutBindings.ts | 18 +++ src/lib/shortcuts.test.ts | 75 ++++++++- src/lib/shortcuts.ts | 20 ++- 21 files changed, 377 insertions(+), 51 deletions(-) create mode 100644 docs/screenshots/shortcuts/key-bar-phone.png create mode 100644 docs/screenshots/shortcuts/settings-phone.png diff --git a/CHANGELOG.md b/CHANGELOG.md index de64262ab..fce7663e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ between releases do not reach them. Remote-PC runtime bundles are versioned sepa [#599](https://github.com/devswha/herdr-web-ui/pull/599)) ### Changed +- The default mobile terminal key bar puts Esc, Tab and Ctrl+C first, before the held modifiers + and arrows. Saved layouts keep their order. +- **Settings → Shortcuts** explains which keys control the app and marks known browser/OS + reservations without changing saved bindings. - **Settings → Terminal → Clipboard from a pane** is on again: vim, tmux and Claude Code copy to your clipboard from a pane without a trip to Settings. It is also on for anyone 0.4.1 left off without asking (it saved the switch with any other setting). Turn it off if a pane runs output @@ -25,6 +29,8 @@ between releases do not reach them. Remote-PC runtime bundles are versioned sepa Bug fixes as an install shows them, with the full changelog folded underneath. ### Fixed +- Command palette shortcut hints follow changed bindings and disappear for disabled ones. + App shortcuts also recognize physical letter keys on non-Latin layouts outside IME composition. - macOS Safari direct terminal input preserves Korean syllables when the input method replaces text without emitting composition events, including after switching from English. ([#524](https://github.com/devswha/herdr-web-ui/pull/524) by @suho-han) diff --git a/DESIGN.md b/DESIGN.md index cad51bdde..c93703320 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -949,7 +949,7 @@ One set for both themes: the card is island black wherever it shows. and Escape closes. IME candidate, commit and cancel keys stay with composition. Buttons reached with Tab keep their native activation, and arrow navigation scrolls the selected result into view. - Actions cover new workspace, lens/sidebar/theme, settings, notifications, lock and refresh, with - `.kbd` hints where a global shortcut exists. + `.kbd` hints resolved from this browser's shortcut settings; disabled bindings have no hint. ### Settings dialog - A Settings shortcut opened over a file preview places Settings above it (`--z-modal + 2`, one @@ -973,7 +973,7 @@ One set for both themes: the card is island black wherever it shows. (panes open in, show thinking, chat width, chat font size and family; then **Composer**: Enter sends, suggestion chip; then **Quick replies**), **Terminal** (font size and family, wheel speed, input mode, Key bar), **Alerts**, **Voice input**, **Subscription usage**, - **Shortcuts** (the complete platform-resolved list), **Phone & devices** (the phone address, + **Shortcuts** (the platform-resolved global bindings), **Phone & devices** (the phone address, Keep screen on, Install; then paired devices), **Remote PCs**, **About** (Updates, herdr, the repository links). A button that points at Updates opens the dialog on About. - Every page is built from the same parts (`components/SettingsControls.tsx`). A group is an @@ -1021,7 +1021,7 @@ One set for both themes: the card is island black wherever it shows. - xterm has `scrollback: 0`; wheel/touch gestures reach herdr's alternate-screen scrollback. The mount clips its own gutter and hides the unused xterm scrollbar. - Terminal banners stack top-right for ended, reconnecting, observe and held-draft review states. -- The mobile key bar is Esc, Tab, sticky Ctrl/Alt/Shift, Enter, arrows and `^C`; +- The mobile key bar starts with Esc, Tab and `^C`, then sticky Ctrl/Alt/Shift, Enter and arrows; it never steals typing focus. Modifiers remain highlighted until toggled off, leaving the pane/lens or disconnecting; each exposes `aria-pressed`. - That is the default row. Settings → Terminal → Key bar can add, remove and reorder every @@ -1110,7 +1110,16 @@ One set for both themes: the card is island black wherever it shows. - Touch targets grow to `--touch-target`; fields stay `--fs-input` where mobile zoom is a risk. - `prefers-reduced-motion` is honored. Lucide/inline SVG decoration is hidden from assistive tech. - Global shortcuts use the convention **Mod+Shift+key**: Mod is Command on Apple platforms and Ctrl - elsewhere. The settings table is the discoverable source of the complete mapping. + elsewhere. Settings lists the global bindings and allows changing their final key or disabling + them, with duplicate assignments blocked and known browser/OS reservations warned about. + Reservations vary by browser and installed-app mode; a warning is not a guarantee of delivery. + Bindings belong to this browser, not the remote PC or pane. Hold-to-dictate remains fixed. +- Text-field selection and IME composition keep their keys. Focus-local list/tab navigation and + reordering are separate from global bindings; they must not intercept terminal input. + Global actions target the selected pane, and a handled shortcut must never also reach its PTY. +- On touch screens app actions stay in buttons and menus, including the palette in More on phones. + The key bar sends terminal input only; it is not an app-action toolbar. Hardware keyboards use + the same global bindings on phones and desktops. - `document.title` is ` · herdr` while selected, otherwise `herdr web ui`. ### Accepted debt diff --git a/docs/guide.md b/docs/guide.md index a0d8a1341..9a8f06a9f 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -243,7 +243,7 @@ whether the test was sent or failed; a missing subscription offers **Turn alerts On a phone: - Agent panes open in the chat. -- The terminal gets a key bar above the keyboard (Esc, Tab, Ctrl, Alt, Shift, Enter, arrows, Ctrl+C). +- The terminal gets a key bar above the keyboard (Esc, Tab, Ctrl+C, Ctrl, Alt, Shift, Enter, arrows). Ctrl, Alt and Shift stay held until tapped again and combine with each other. Enable direct terminal typing with the keyboard button to use them with letters and symbols. Herdr encodes each chord for the program's keyboard protocol; legacy @@ -438,7 +438,7 @@ A message holds the event (`install` or `update`), a random ID made on this PC, ## Keyboard shortcuts -`Mod` is **⌘** on macOS and **Ctrl** elsewhere. Every shortcut adds Shift, so the terminal keeps its own Ctrl keys. +`Mod` is **⌘** on Apple platforms and **Ctrl** elsewhere. Global app shortcuts add Shift, so the terminal keeps its own Ctrl keys. These are the defaults: | Shortcut | Action | | --- | --- | @@ -449,6 +449,20 @@ A message holds the event (`install` or `update`), a random ID made on this PC, | `Mod+Shift+↑` / `↓` | Previous / next pane | | `Mod+Shift+,` | Settings | +**Settings → Shortcuts** changes the final key of each app binding or turns it off. Settings +are saved in this browser, not on the remote PC. The command palette shows the current bindings +and omits disabled ones. Reset shortcuts restores the defaults. Hold-to-dictate +(`Mod+Shift+Space`) remains fixed. + +Known browser/OS reservations are marked, but other combinations can also be intercepted by +your browser, OS or extensions. Installed-app mode may behave differently. While an IME is +composing, keys stay with it. In text fields, Mod+Shift+arrows keep their text-selection behavior. +List and tab controls also have focus-local keys; those do not become global terminal shortcuts. + +On a phone, use the header and More menu for app actions; Command palette is available there +without a shortcut. The terminal key bar is configured separately under **Settings → Terminal**. +An external keyboard uses the same app bindings as a desktop. + Enter sends and Shift+Enter adds a line. Settings can switch sending to Mod+Enter. ## How it works diff --git a/docs/screenshots/shortcuts/key-bar-phone.png b/docs/screenshots/shortcuts/key-bar-phone.png new file mode 100644 index 0000000000000000000000000000000000000000..5d98a06fe9b3e1eff9d63c88083ba461ea403e07 GIT binary patch literal 20113 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 literal 0 HcmV?d00001 diff --git a/docs/screenshots/shortcuts/settings-phone.png b/docs/screenshots/shortcuts/settings-phone.png new file mode 100644 index 0000000000000000000000000000000000000000..331cddf85cdb65243577d1350b2f4d1b90b59132 GIT binary patch literal 54729 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 N@lHmhR7ltFe*xk|=ZydW literal 0 HcmV?d00001 diff --git a/docs/terminal-input.md b/docs/terminal-input.md index 8fae9d140..d60e0937d 100644 --- a/docs/terminal-input.md +++ b/docs/terminal-input.md @@ -11,8 +11,10 @@ user previously chose direct typing; a fine pointer uses direct typing. The key button switches modes too, on a touch screen only: a desktop changes the mode in Settings. Settings → Shortcuts can change each app action's Mod+Shift key or return its keys to the terminal. The hold-to-dictate binding remains fixed. Conflicts include the legacy New workspace -alias, and Reset restores the defaults. Browser-reserved keys still depend on the browser and -installed-app mode. +alias, and Reset restores the defaults. Palette hints reflect the saved bindings and disappear +when a binding is off. Known browser/OS reservations are marked; delivery still depends on the +browser, OS and installed-app mode. These settings belong to this browser, not the remote PC. +Global bindings do not replace focus-local list/tab controls or text-field selection keys. The input line keeps its unsent text per `paneStorageId(machineId, paneId)`, including across lens changes and reloads. Storage refusal falls back to memory for view changes. Pending sends @@ -48,12 +50,15 @@ left out whole and the notice says some input was left out, also when nothing el draft is replayed on reconnect. The input-line and chat Send buttons preserve an active composition, and the key bar waits for composition to finish. Leaving the input clears its composition guard. -The key bar defaults to Esc, Tab, Ctrl, Alt, Shift, Enter, the arrows and ^C. +The key bar defaults to Esc, Tab, ^C, Ctrl, Alt, Shift, Enter and the arrows. Esc, Tab and ^C +come first so they stay reachable without scrolling on narrow phones. Settings → Terminal → Key bar → Edit key bar opens the complete list: add or remove keys, move each key up or down, and register a custom combination such as Ctrl+W. The catalog includes editing keys and F1–F12; a custom combination can use any single printable character, including space and `+`. Existing extra-key preferences migrate to the same visible order. Restore defaults returns the -original bar; an empty list stays empty. The keyboard mode button stays first on touch screens. +default bar; an empty list stays empty. Saved custom layouts keep their order. +The keyboard mode button stays first on touch screens. App actions belong in the header and +menus, not this terminal-input row. Ctrl, Alt and Shift stay held until tapped again and can be combined. Removing a modifier button clears its held state immediately. Ordinary key buttons use the held modifiers, while saved combinations send exactly their configured modifiers without changing the held state. diff --git a/scripts/key-bar-customization-demo-regression.ts b/scripts/key-bar-customization-demo-regression.ts index fb9c858b8..c5265299b 100644 --- a/scripts/key-bar-customization-demo-regression.ts +++ b/scripts/key-bar-customization-demo-regression.ts @@ -15,6 +15,7 @@ const app = mkdtempSync(join(tmpdir(), "herdr-key-bar-demo-")); const evidence = process.env.UI_EVIDENCE_DIR; const SETTINGS_KEY = "herdr-web-ui:settings"; const MIGRATED_KEYS = ["direct", "Escape", "Tab", "BackTab", "Control", "Alt", "Shift", "Enter", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown", "ctrl-c", "ctrl-d", "ctrl-z", "pipe", "tilde", "slash"]; +const DEFAULT_KEY_BAR_KEYS = ["direct", "Escape", "Tab", "ctrl-c", "Control", "Alt", "Shift", "Enter", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]; interface InputFrame { type: "input" | "keys"; pane_id: string; text?: string; keys?: string[] } const framesOf = (page: Page): Promise => page.evaluate(() => (window as unknown as { keyBarFrames: InputFrame[] }).keyBarFrames); @@ -271,9 +272,148 @@ try { dialog = await openSettings(page); await dialog.locator(".key-bar-settings").getByRole("button", { name: "Restore defaults", exact: true }).tap(); await closeSettings(page, dialog); - assert.deepEqual(await keysOf(page), ["direct", "Escape", "Tab", "Control", "Alt", "Shift", "Enter", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "ctrl-c"]); + assert.deepEqual(await keysOf(page), DEFAULT_KEY_BAR_KEYS); + for (const width of [320, 390]) { + await page.setViewportSize({ width, height: 844 }); + await settled(page); + const controlC = await page.evaluate(() => { + const bar = document.querySelector(".key-bar"); + const key = bar?.querySelector('[data-key="ctrl-c"]'); + if (!bar || !key) return null; + bar.scrollLeft = 0; + const barRect = bar.getBoundingClientRect(); + const keyRect = key.getBoundingClientRect(); + return { + visibleAtStart: getComputedStyle(bar).display !== "none" + && keyRect.left >= barRect.left + && keyRect.right <= barRect.left + bar.clientWidth, + scrollLeft: bar.scrollLeft, + }; + }); + assert.deepEqual(controlC, { visibleAtStart: true, scrollLeft: 0 }, `Ctrl+C is visible without horizontal scrolling at ${width}px`); + if (evidence && width === 390) { + mkdirSync(evidence, { recursive: true }); + await appFaces(page); + await page.screenshot({ path: join(evidence, "key-bar-defaults-390.png") }); + } + } + + await page.setViewportSize({ width: 1280, height: 844 }); + await settled(page); + const palette = page.getByRole("dialog", { name: "Command palette", exact: true }); + const openSidebarAction = async (): Promise => { + await page.keyboard.press("ControlOrMeta+Shift+k"); + await palette.waitFor({ timeout: 5_000 }); + await palette.getByRole("searchbox", { name: "Search panes and actions", exact: true }).fill("Toggle sidebar"); + const action = palette.getByRole("option").filter({ hasText: "Toggle sidebar" }); + await action.waitFor({ state: "visible", timeout: 5_000 }); + return action; + }; + const closePalette = async (): Promise => { + await page.keyboard.press("Escape"); + await palette.waitFor({ state: "hidden", timeout: 5_000 }); + }; + const modifierLabel = await page.evaluate(() => /Mac|iPhone|iPad|iPod/i.test(navigator.platform || navigator.userAgent) ? "⌘" : "Ctrl"); + const expectSidebarHint = async (action: Locator, key: string): Promise => { + const hint = action.locator(".palette-shortcut"); + await hint.waitFor({ state: "visible", timeout: 5_000 }); + assert.equal(await hint.getAttribute("aria-label"), `${modifierLabel} + Shift + ${key}`); + assert.deepEqual(await hint.locator("kbd").allTextContents(), [modifierLabel, "Shift", key]); + }; + const openShortcutSettings = async (): Promise => { + await page.keyboard.press("ControlOrMeta+Shift+Comma"); + const settings = page.getByRole("dialog", { name: "Settings", exact: true }); + await settings.waitFor({ timeout: 5_000 }); + await openSettingsPage(page, "Shortcuts"); + return settings; + }; + let action = await openSidebarAction(); + await expectSidebarHint(action, "B"); + await closePalette(); + + dialog = await openShortcutSettings(); + let sidebarShortcut = dialog.getByRole("combobox", { name: "Toggle sidebar", exact: true }); + await sidebarShortcut.selectOption("x"); + await page.waitForFunction((key) => JSON.parse(localStorage.getItem(key)!).shortcutOverrides["toggle-sidebar"] === "x", SETTINGS_KEY, { timeout: 5_000 }); + await closeSettings(page, dialog); + action = await openSidebarAction(); + await expectSidebarHint(action, "X"); + await closePalette(); + await page.reload(); + await ready(page); + const savedOverrides = (await settingsOf(page)).shortcutOverrides as Record; + assert.equal(savedOverrides["toggle-sidebar"], "x", "a shortcut rebinding survives reload"); + action = await openSidebarAction(); + await expectSidebarHint(action, "X"); + await closePalette(); + + dialog = await openShortcutSettings(); + sidebarShortcut = dialog.getByRole("combobox", { name: "Toggle sidebar", exact: true }); + await sidebarShortcut.selectOption("off"); + await page.waitForFunction((key) => JSON.parse(localStorage.getItem(key)!).shortcutOverrides["toggle-sidebar"] === null, SETTINGS_KEY, { timeout: 5_000 }); + await closeSettings(page, dialog); + action = await openSidebarAction(); + assert.equal(await action.locator(".palette-shortcut").count(), 0, "disabling a shortcut removes its CommandPalette hint"); + await closePalette(); + + dialog = await openShortcutSettings(); + sidebarShortcut = dialog.getByRole("combobox", { name: "Toggle sidebar", exact: true }); + assert.match(await sidebarShortcut.locator('option[value="t"]').innerText(), /Reserved/); + await sidebarShortcut.selectOption("t"); + await dialog.getByText(`Your browser or operating system may intercept ${modifierLabel}+⇧+T.`, { exact: true }).waitFor({ timeout: 5_000 }); + assert.equal(((await settingsOf(page)).shortcutOverrides as Record)["toggle-sidebar"], "t", "a reservation warns without silently changing the configured key"); + await dialog.getByRole("button", { name: "Reset shortcuts", exact: true }).click(); + await page.waitForFunction((key) => Object.keys(JSON.parse(localStorage.getItem(key)!).shortcutOverrides).length === 0, SETTINGS_KEY, { timeout: 5_000 }); + sidebarShortcut = dialog.getByRole("combobox", { name: "Toggle sidebar", exact: true }); + await page.waitForFunction(() => (document.querySelector('.settings-dialog select[aria-label="Toggle sidebar"]')?.value ?? "") === "default", undefined, { timeout: 5_000 }); + if (evidence) { + mkdirSync(evidence, { recursive: true }); + await sidebarShortcut.scrollIntoViewIfNeeded(); + await appFaces(page); + await page.screenshot({ path: join(evidence, "shortcuts-settings-defaults.png") }); + await page.setViewportSize({ width: 390, height: 844 }); + await settled(page); + await dialog.locator(".settings-body").evaluate((node) => { node.scrollTop = 0; }); + await page.screenshot({ path: join(evidence, "shortcuts-settings-phone.png") }); + await page.setViewportSize({ width: 1280, height: 844 }); + await settled(page); + } + await closeSettings(page, dialog); + action = await openSidebarAction(); + await expectSidebarHint(action, "B"); + if (evidence) { + await appFaces(page); + await page.screenshot({ path: join(evidence, "command-palette-shortcut-defaults.png") }); + } + await closePalette(); + + const dispatchPhysicalK = async (isComposing: boolean): Promise => { + await input.focus(); + await input.evaluate((node, composing) => { + const mac = /Mac|iPhone|iPad|iPod/i.test(navigator.platform || navigator.userAgent); + node.dispatchEvent(new KeyboardEvent("keydown", { + key: "ㅏ", + code: "KeyK", + ctrlKey: !mac, + metaKey: mac, + shiftKey: true, + isComposing: composing, + bubbles: true, + cancelable: true, + })); + }, isComposing); + }; + const beforePhysicalShortcut = (await framesOf(page)).length; + await dispatchPhysicalK(false); + await palette.waitFor({ state: "visible", timeout: 5_000 }); + await settled(page); + assert.equal((await framesOf(page)).length, beforePhysicalShortcut, "a non-Latin physical Mod+Shift+K opens the palette without sending terminal input"); + await closePalette(); + await dispatchPhysicalK(true); + await settled(page); + assert.equal(await palette.count(), 0, "the composing equivalent does not open the command palette"); assert.deepEqual(errors, []); - console.log("PASS an empty key list keeps the keyboard mode toggle usable, persists, and can restore the defaults"); + console.log("PASS restoring defaults keeps Ctrl+C visible; shortcut hints update, disable and reset; physical non-Latin Mod+Shift+K opens the palette without terminal input, but composition does not"); // The frames above go to an agent pane, which the demo never types into. Its shell must // answer a chord as a terminal would: the demo has no herdr to turn the names into keys. diff --git a/src/AGENTS.md b/src/AGENTS.md index c0a5e5404..1af1c48aa 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -35,6 +35,7 @@ React 18 + xterm.js browser client. The render-state machine, xterm lifecycle an - Snapshot reads in App go through `SnapshotRequests`: an event or a newer read supersedes an in-flight fetch, its error included. - Chat path links (lib/filePaths.ts, ASCII `[\w@.+-]` names) and terminal path links (lib/terminalFileLinks.ts, Unicode letters) are separate matchers; a change to path recognition checks both. - Helpers outside React call the module-level `t`; components take `useT()`, whose identity changes with the language, so a memo that formats text lists it. +- Global shortcut hints resolve through `shortcutDisplayKeys` with the current `shortcutOverrides`, never the defaults alone. Global bindings remain Mod+Shift; focus-local list/tab keys are separate and never intercept terminal input. The touch key bar sends terminal keys, not app actions. - Work-block counts skip todo tools (`isTodoTool`: `TodoWrite` would match the write category). The Agents list never includes a shell just because it reports working or blocked. Pane-status pushes never trigger a worktree inventory poll. ## WHERE TO LOOK @@ -54,7 +55,7 @@ React 18 + xterm.js browser client. The render-state machine, xterm lifecycle an | New workspace / New tab | components/NewSessionDialog.tsx | `createWorkspace`, or `createTab` (POST /api/tab/create) when given a `tab` prop; agent start may take 60s and the dialog stays open while pending; `agent_started:false` + error offers to open the already-created workspace | | Command palette | components/CommandPalette.tsx + lib/paletteSearch.ts | Mod+Shift+K; panes (recent first, fuzzy ranked) + actions with `.kbd` hints; Escape closes | | Settings | components/SettingsDialog.tsx + SettingsControls.tsx + lib/settings.ts | `SettingsProvider` wraps App in main.tsx and writes `data-theme` / `data-density` on ``; the dialog shows one page at a time (`PAGES`; a phone lists them first) and mounts per opening, so a page's requests start when it is opened; a new setting is a `SettingsRow` in a `SettingsGroup` with one of the SettingsControls, never markup of its own; the dialog's accessible name stays Settings on every page, and a browser script reaches a page through `openSettingsPage` (scripts/settings-page.ts); each step in (dialog, a phone's page, key bar editor) is a history entry kept by lib/settingsHistory.ts, so open and close go through App's `settingsOpen` and the dialog's own state only, never a `history` call of their own; PhonePanel.tsx picks the address that works through lib/phone.ts `phonePlan`; QrCode.tsx is always dark-on-white | -| Shortcuts | lib/shortcuts.ts | `useShortcuts(actions, enabled)`: capture-phase window keydown; every binding is Mod+Shift+key so the pty keeps Ctrl+; `SHORTCUTS` drives the hint tables | +| Shortcuts | lib/shortcuts.ts | `useShortcuts(actions, enabled)`: capture-phase window keydown; every global binding is Mod+Shift+key so the pty keeps Ctrl+; `SHORTCUTS` defines defaults and `shortcutDisplayKeys` resolves user-facing hints | | OSC 52 clipboard | lib/osc52.ts + PaneTerminal `parser.registerOscHandler(52)` | client half of a clipboard bridge; DORMANT on herdr 0.9.x (herdr's screen-diff attach stream consumes OSC 52 before clients — live-verified); fires as a transient pill once sequences arrive | | Sidebar badges | components/Sidebar.tsx `StatusBadge` + lib/status.ts | ONE vocabulary for every surface (sidebar, palette, composer status line): idle→READY, working→RUN, blocked→INPUT, done→DONE, unknown→`—` (`STATUS_WORD`, `knownStatus`); the compact sidebar variant draws a glyph for RUN, INPUT and DONE and nothing for READY and unknown, but always renders the element with `data-status`, its label and a box (the browser scripts wait on it); background tasks are `.background-count`, not a `.badge`; the composer alone reads a blank state as READY (a shell is always ready); pushed statuses land through `applyPaneStatus` (lib/snapshot.ts) | | Notifications | lib/notifications.ts | tab alerts: policy re-exported from shared/notify-policy.ts; hidden-tab guard; shown via `registration.showNotification` (Android has no `new Notification()`), constructor only as fallback; skipped when this device has push (`pushOnRef`) | diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index 8da7d6f1c..5d4766eca 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -7,7 +7,8 @@ import "./CommandPalette.css"; import type { PaneInfo, SessionSnapshot } from "../../shared/protocol.ts"; import type { AppActions, PaneView } from "../lib/actions.ts"; import { rankPanes } from "../lib/paletteSearch.ts"; -import { SHORTCUTS, formatKeys, type ShortcutId } from "../lib/shortcuts.ts"; +import { shortcutDisplayKeys, formatKeys, type ShortcutId } from "../lib/shortcuts.ts"; +import { useSettings } from "../lib/settings.ts"; import { AgentMark } from "./AgentMark.tsx"; import { displayPaneTitle, StatusBadge } from "./Sidebar.tsx"; import { placeLine } from "../lib/paneName.ts"; @@ -60,10 +61,11 @@ function cwdBasename(path: string | null | undefined): string { } function ShortcutHint({ shortcutId }: { shortcutId?: ShortcutId }) { + const { settings } = useSettings(); if (!shortcutId) return null; - const shortcut = SHORTCUTS.find((item) => item.id === shortcutId); - if (!shortcut) return null; - return {formatKeys(shortcut.keys).map((key) => {key})}; + const keys = formatKeys(shortcutDisplayKeys(shortcutId, settings.shortcutOverrides)); + if (keys.length === 0) return null; + return {keys.map((key) => {key})}; } export function CommandPalette({ open, onClose, snapshot, selectedPaneId, view, actions }: CommandPaletteProps) { diff --git a/src/components/SettingsDialog.css b/src/components/SettingsDialog.css index b7e673cb7..aadc4ed70 100644 --- a/src/components/SettingsDialog.css +++ b/src/components/SettingsDialog.css @@ -132,7 +132,7 @@ } .settings-shortcut-select { - width: 11rem; + width: 14rem; } .settings-link { @@ -310,7 +310,7 @@ } .settings-shortcut-select { - width: 9.5rem; + width: 100%; } /* the key takes the line; Save and Remove share the next one */ diff --git a/src/components/SettingsDialog.tsx b/src/components/SettingsDialog.tsx index 8761dc1db..ce7244111 100644 --- a/src/components/SettingsDialog.tsx +++ b/src/components/SettingsDialog.tsx @@ -5,7 +5,8 @@ import "./SettingsDialog.css"; import type { AppActions } from "../lib/actions.ts"; import { useInstallPrompt } from "../lib/install.ts"; -import { SHORTCUTS, formatKeys, shortcutKeys, shortcutConflict } from "../lib/shortcuts.ts"; +import { SHORTCUTS, formatKeys, isMacPlatform, shortcutDisplayKeys, shortcutKeys, shortcutConflict } from "../lib/shortcuts.ts"; +import { isReservedShortcutKey } from "../lib/shortcutBindings.ts"; import { CHAT_FONT_MAX, CHAT_FONT_MIN, CHAT_WIDTHS, chatFontSize, DEFAULT_SETTINGS, QUICK_REPLIES_MAX, QUICK_REPLY_MAX_CHARS, TERMINAL_FONT_MAX, TERMINAL_FONT_MIN, TERMINAL_WHEEL_SPEED_MAX, TERMINAL_WHEEL_SPEED_MIN, VOICE_BUTTONS, useSettings, forgetPaneViews, type VoiceButton } from "../lib/settings.ts"; import { LANGUAGE_NAMES, LANGUAGE_SETTINGS, useT } from "../lib/i18n.ts"; import { useFocusTrap } from "../lib/useFocusTrap.ts"; @@ -459,28 +460,38 @@ const SHORTCUT_KEYS: readonly string[] = [..."abcdefghijklmnopqrstuvwxyz01234567 function ShortcutsPage() { const { settings, update } = useSettings(); const t = useT(); + const platformIsMac = isMacPlatform(); return ( <> - - {SHORTCUTS.map((shortcut) => ( - - {shortcut.id === "voice" ? {formatKeys(shortcut.keys).map((key) => {key})} : ( - - )} - - ))} + {t("Bindings apply to this browser and device; Mod+Shift is fixed.")} {t("Text selection in focused fields stays native; Tab/list keys are UI-local, not global shortcuts.")} {t("The mobile key bar sends terminal keys, not app actions.")}}> + {SHORTCUTS.map((shortcut) => { + const displayedKeys = shortcutDisplayKeys(shortcut.id, settings.shortcutOverrides); + const selectedKey = displayedKeys[displayedKeys.length - 1]; + const selectedReserved = selectedKey !== undefined && isReservedShortcutKey(selectedKey, platformIsMac); + const defaultKeys = shortcutDisplayKeys(shortcut.id, {}); + const defaultKey = defaultKeys[defaultKeys.length - 1]; + const defaultReserved = defaultKey !== undefined && isReservedShortcutKey(defaultKey, platformIsMac); + return ( + + {shortcut.id === "voice" ? {formatKeys(shortcut.keys).map((key) => {key})} : ( + + )} + + ); + })}
diff --git a/src/lib/i18n.ja.ts b/src/lib/i18n.ja.ts index 6a47cb5e8..b8d789011 100644 --- a/src/lib/i18n.ja.ts +++ b/src/lib/i18n.ja.ts @@ -15,7 +15,11 @@ export const JA: Record = { "Automatic": "自動", "Terminal input mode": "端末入力モード", "Reset shortcuts": "ショートカットをリセット", - "Some keys are reserved by the browser. Changes apply to this device.": "一部のキーはブラウザーが使用します。変更はこの端末に適用されます。", + "Bindings apply to this browser and device; Mod+Shift is fixed.": "ショートカットはこのブラウザーとデバイスに適用されます。Mod+Shiftは固定です。", + "Text selection in focused fields stays native; Tab/list keys are UI-local, not global shortcuts.": "フォーカス中の入力欄でのテキスト選択は標準動作のままです。Tabやリストのキーは UI 内でのみ使い、グローバルショートカットではありません。", + "The mobile key bar sends terminal keys, not app actions.": "モバイルのキー列はアプリ操作ではなく、端末にキーを送ります。", + "Your browser or operating system may intercept {keys}.": "ブラウザーや OS が {keys} を先に処理する場合があります。", + "Reserved": "予約済み", "Already assigned": "割り当て済み", "Send keys to terminal": "キーを端末に送る", "Default": "既定", diff --git a/src/lib/i18n.ko.ts b/src/lib/i18n.ko.ts index 4985c6f31..21ebeb286 100644 --- a/src/lib/i18n.ko.ts +++ b/src/lib/i18n.ko.ts @@ -13,7 +13,11 @@ export const KO: Record = { "Automatic": "자동", "Terminal input mode": "터미널 입력 방식", "Reset shortcuts": "단축키 초기화", - "Some keys are reserved by the browser. Changes apply to this device.": "일부 키는 브라우저가 사용합니다. 변경은 이 기기에 적용됩니다.", + "Bindings apply to this browser and device; Mod+Shift is fixed.": "단축키는 이 브라우저와 기기에 적용됩니다. Mod+Shift는 고정입니다.", + "Text selection in focused fields stays native; Tab/list keys are UI-local, not global shortcuts.": "포커스된 입력란의 텍스트 선택은 기본 동작을 따릅니다. Tab/목록 키는 UI 안에서만 작동하며 전역 단축키가 아닙니다.", + "The mobile key bar sends terminal keys, not app actions.": "모바일 키 바는 앱 동작이 아닌 터미널 키를 보냅니다.", + "Your browser or operating system may intercept {keys}.": "브라우저나 운영체제가 {keys} 입력을 가로챌 수 있습니다.", + "Reserved": "예약됨", "Already assigned": "이미 지정됨", "Send keys to terminal": "키를 터미널로 전달", "Default": "기본값", diff --git a/src/lib/i18n.zh.ts b/src/lib/i18n.zh.ts index 752b8189c..aa33f861a 100644 --- a/src/lib/i18n.zh.ts +++ b/src/lib/i18n.zh.ts @@ -17,7 +17,11 @@ export const ZH: Record = { "Automatic": "自动", "Terminal input mode": "终端输入模式", "Reset shortcuts": "重置快捷键", - "Some keys are reserved by the browser. Changes apply to this device.": "部分按键由浏览器保留。更改适用于此设备。", + "Bindings apply to this browser and device; Mod+Shift is fixed.": "快捷键仅应用于此浏览器和设备;Mod+Shift 固定不变。", + "Text selection in focused fields stays native; Tab/list keys are UI-local, not global shortcuts.": "焦点位于文本输入框时,文本选择仍由系统处理;Tab/列表按键仅在界面内生效,不是全局快捷键。", + "The mobile key bar sends terminal keys, not app actions.": "手机上的按键栏发送终端按键,不会触发应用操作。", + "Your browser or operating system may intercept {keys}.": "浏览器或操作系统可能会拦截 {keys}。", + "Reserved": "系统保留", "Already assigned": "已分配", "Send keys to terminal": "将按键发送到终端", "Default": "默认", diff --git a/src/lib/keyBar.test.ts b/src/lib/keyBar.test.ts index b606c0074..529ed104a 100644 --- a/src/lib/keyBar.test.ts +++ b/src/lib/keyBar.test.ts @@ -5,8 +5,8 @@ import { DEFAULT_KEY_BAR_ITEMS, KEY_BAR_ITEMS_MAX, KEY_BAR_KEYS, keyBarInputSequ describe("key bar migration", () => { const caps = (items: KeyBarItem[]) => items.map(keyBarItemLabel); - it("preserves the current default and a legacy device's extra selections in display order", () => { - expect(caps(DEFAULT_KEY_BAR_ITEMS)).toEqual(["Esc", "Tab", "Ctrl", "Alt", "Shift", "Enter", "↑", "↓", "←", "→", "^C"]); + it("preserves legacy migration order and selected extras", () => { + expect(caps(migrateKeyBarItems(["alt"]))).toEqual(["Esc", "Tab", "Ctrl", "Alt", "Shift", "Enter", "↑", "↓", "←", "→", "^C"]); expect(caps(migrateKeyBarItems(["slash", "home-end", "shift-tab", "ctrl-z"]))).toEqual([ "Esc", "Tab", "⇧Tab", "Ctrl", "Shift", "Enter", "↑", "↓", "←", "→", "Home", "End", "^C", "^Z", "/", ]); @@ -16,6 +16,15 @@ describe("key bar migration", () => { }); }); +describe("default key bar", () => { + const caps = (items: KeyBarItem[]) => items.map(keyBarItemLabel); + + it("prioritizes core keys ahead of modifiers and navigation", () => { + expect(caps(DEFAULT_KEY_BAR_ITEMS)).toEqual(["Esc", "Tab", "^C", "Ctrl", "Alt", "Shift", "Enter", "↑", "↓", "←", "→"]); + expect(caps(sanitizeKeyBarItems(undefined))).toEqual(caps(DEFAULT_KEY_BAR_ITEMS)); + }); +}); + describe("saved key bars", () => { it("preserves reordered keys, exact chords, sticky toggles and intentional empty bars", () => { const items: KeyBarItem[] = [ diff --git a/src/lib/keyBar.ts b/src/lib/keyBar.ts index b77d79ab0..560615e1e 100644 --- a/src/lib/keyBar.ts +++ b/src/lib/keyBar.ts @@ -57,7 +57,12 @@ export function migrateKeyBarItems(extras: readonly KeyBarExtra[]): KeyBarItem[] return items; } -export const DEFAULT_KEY_BAR_ITEMS: KeyBarItem[] = migrateKeyBarItems(["alt"]); +export const DEFAULT_KEY_BAR_ITEMS: KeyBarItem[] = [ + { type: "key", key: "Escape" }, { type: "key", key: "Tab" }, { type: "key", key: "ctrl-c" }, + { type: "modifier", modifier: "ctrl" }, { type: "modifier", modifier: "alt" }, { type: "modifier", modifier: "shift" }, + { type: "key", key: "Enter" }, + { type: "key", key: "ArrowUp" }, { type: "key", key: "ArrowDown" }, { type: "key", key: "ArrowLeft" }, { type: "key", key: "ArrowRight" }, +]; /** Stable descriptor identity; an inherited key and an exact bare key have different behavior. */ export function keyBarItemId(item: KeyBarItem): string { diff --git a/src/lib/settings.test.ts b/src/lib/settings.test.ts index 6a04f06c9..275f9ad73 100644 --- a/src/lib/settings.test.ts +++ b/src/lib/settings.test.ts @@ -459,9 +459,11 @@ it("sanitizes input modes and shortcut overrides without accepting arbitrary com }); describe("key bar settings", () => { - it("migrates existing optional keys without restoring keys a new layout removed", () => { + it("uses the new default for fresh partial records and migrates explicit legacy records", () => { expect(DEFAULT_SETTINGS.keyBarItems).toEqual(DEFAULT_KEY_BAR_ITEMS); expect(sanitizeSettings({}).keyBarItems).toEqual(DEFAULT_KEY_BAR_ITEMS); + expect(sanitizeSettings({ theme: "light" }).keyBarItems).toEqual(DEFAULT_KEY_BAR_ITEMS); + expect(sanitizeSettings({ keyBarExtras: ["alt"] }).keyBarItems).toEqual(migrateKeyBarItems(["alt"])); expect(sanitizeSettings({ keyBarExtras: ["home-end", "slash", "unknown"] }).keyBarItems).toEqual(migrateKeyBarItems(["home-end", "slash"])); expect(sanitizeSettings({ keyBarExtras: [], keyBarItems: [] }).keyBarItems).toEqual([]); expect(sanitizeSettings({ keyBarExtras: ["alt"], keyBarItems: [] }).keyBarItems).toEqual([]); diff --git a/src/lib/settings.ts b/src/lib/settings.ts index db167b54a..42a554af7 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -260,10 +260,13 @@ export function sanitizeSettings(raw: unknown): Settings { const font = record["terminalFontSize"]; const chatFont = record["chatFontSize"]; const keyBarExtras = sanitizeKeyBarExtras(record["keyBarExtras"], DEFAULT_SETTINGS.keyBarExtras); + const keyBarFallback = Array.isArray(record["keyBarExtras"]) + ? migrateKeyBarItems(keyBarExtras) + : DEFAULT_KEY_BAR_ITEMS; return { terminalInputMode: record["terminalInputMode"] === "line" || record["terminalInputMode"] === "direct" ? record["terminalInputMode"] : "auto", keyBarExtras, - keyBarItems: sanitizeKeyBarItems(record["keyBarItems"], migrateKeyBarItems(keyBarExtras)), + keyBarItems: sanitizeKeyBarItems(record["keyBarItems"], keyBarFallback), shortcutOverrides: sanitizeShortcutOverrides(record["shortcutOverrides"]), theme: theme === "dark" || theme === "light" || theme === "system" ? theme : DEFAULT_SETTINGS.theme, density: density === "compact" || density === "comfortable" ? density : DEFAULT_SETTINGS.density, diff --git a/src/lib/shortcutBindings.ts b/src/lib/shortcutBindings.ts index c37ad44f3..c37cb7d0c 100644 --- a/src/lib/shortcutBindings.ts +++ b/src/lib/shortcutBindings.ts @@ -1,6 +1,24 @@ /** Overrides keep the app's Mod+Shift convention; null returns a key to the terminal. */ export type ShortcutOverrides = Record; export const CUSTOM_SHORTCUT_IDS = ["palette", "toggle-view", "toggle-sidebar", "new-session", "previous-pane", "next-pane", "settings"]; + +/** + * Narrow warning list for Mod+Shift browser/OS reservations; this is not exhaustive: + * Mod+Shift+N opens Chromium private browsing, Mod+Shift+T reopens a closed browser tab, + * Mod+Shift+B opens or toggles browser bookmarks, and Firefox uses Mod+Shift+P for a private + * window. Mod+Shift+W closes a browser window. On non-Mac platforms Chromium/Firefox also + * reserve Mod+Shift+C/I/J for DevTools and Chromium reserves Mod+Shift+O for bookmarks. + * macOS uses Cmd+Shift+3/4/5 for screenshots and Cmd+Shift+Q for logout. + * This only reports likely conflicts; configured and default bindings remain valid. + */ +export function isReservedShortcutKey(key: string, platformIsMac: boolean): boolean { + const normalized = /^[a-z]$/i.test(key) ? key.toLowerCase() : key; + if (normalized === "n" || normalized === "t" || normalized === "b" || normalized === "p" || normalized === "w") return true; + return platformIsMac + ? normalized === "3" || normalized === "4" || normalized === "5" || normalized === "q" + : normalized === "c" || normalized === "i" || normalized === "j" || normalized === "o"; +} + export function sanitizeShortcutOverrides(raw: unknown): ShortcutOverrides { if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}; const result: ShortcutOverrides = {}; diff --git a/src/lib/shortcuts.test.ts b/src/lib/shortcuts.test.ts index 5440d5410..cd8bbf063 100644 --- a/src/lib/shortcuts.test.ts +++ b/src/lib/shortcuts.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "bun:test"; -import { matchShortcut, type ShortcutEventLike, keepsArrowsForText, shortcutConflict, shortcutKeys } from "./shortcuts.ts"; +import { isAppShortcut, isMacPlatform, matchShortcut, type ShortcutEventLike, keepsArrowsForText, shortcutConflict, shortcutDisplayKeys, shortcutKeys } from "./shortcuts.ts"; +import { isReservedShortcutKey, sanitizeShortcutOverrides } from "./shortcutBindings.ts"; function keyEvent(key: string, patch: Partial = {}): ShortcutEventLike { return { key, ctrlKey: false, metaKey: false, shiftKey: true, altKey: false, ...patch }; @@ -71,3 +72,75 @@ it("matches shifted digits and detects alias and default-restoration conflicts", expect(shortcutConflict("palette", ["n"], {})).toBe(true); expect(shortcutConflict("palette", shortcutKeys("palette", {}), { palette: "p", settings: "k" })).toBe(true); }); + +describe("shortcutDisplayKeys", () => { + it("shows the default chord, custom chord, disabled state, and uncustomizable voice chord", () => { + expect(shortcutDisplayKeys("palette", {})).toEqual(["Mod", "Shift", "K"]); + expect(shortcutDisplayKeys("toggle-view", { "toggle-view": "p" })).toEqual(["Mod", "Shift", "P"]); + expect(shortcutDisplayKeys("settings", { settings: null })).toEqual([]); + expect(shortcutDisplayKeys("voice", { voice: "p" })).toEqual(["Mod", "Shift", "Space"]); + }); +}); + +describe("non-Latin keyboard layouts", () => { + it("uses KeyA-Z for a non-ASCII single character but preserves an ASCII event.key layout", () => { + expect(matchShortcut(keyEvent("한", { ctrlKey: true, code: "KeyK" }), false, { palette: "k" })).toBe("palette"); + expect(matchShortcut(keyEvent("ㅏ", { ctrlKey: true, code: "KeyK" }), false)).toBe("palette"); + expect(matchShortcut(keyEvent("л", { metaKey: true, code: "KeyK" }), true)).toBe("palette"); + expect(matchShortcut(keyEvent("ㅏ", { ctrlKey: true, code: "KeyK" }), false, { palette: null })).toBeNull(); + expect(matchShortcut(keyEvent("q", { ctrlKey: true, code: "KeyK" }), false, { palette: "q" })).toBe("palette"); + expect(matchShortcut(keyEvent("q", { ctrlKey: true, code: "KeyK" }), false)).toBeNull(); + expect(matchShortcut(keyEvent("🙂", { ctrlKey: true, code: "KeyK" }), false, { palette: "k" })).toBeNull(); + }); + + it("keeps IME guards and Comma/Digit code normalization ahead of layout fallback", () => { + expect(matchShortcut(keyEvent("한", { ctrlKey: true, code: "KeyK", isComposing: true }), false, { palette: "k" })).toBeNull(); + expect(matchShortcut(keyEvent("한", { ctrlKey: true, code: "KeyK", keyCode: 229 }), false, { palette: "k" })).toBeNull(); + expect(matchShortcut(keyEvent("한", { ctrlKey: true, code: "Comma" }), false, { settings: "," })).toBe("settings"); + expect(matchShortcut(keyEvent("한", { ctrlKey: true, code: "Digit1" }), false, { palette: "1" })).toBe("palette"); + }); + + it("keeps app-level and terminal shortcut detection on the same matcher", () => { + const overrides = { palette: "k", "toggle-sidebar": "x" }; + const events = [ + keyEvent("한", { ctrlKey: true, code: "KeyK" }), + keyEvent("x", { ctrlKey: true }), + keyEvent("k", { ctrlKey: true, isComposing: true }), + keyEvent("z", { ctrlKey: true }), + ]; + for (const event of events) { + expect(isAppShortcut(event, overrides)).toBe(matchShortcut(event, isMacPlatform(), overrides) !== null); + } + }); +}); + +describe("isReservedShortcutKey", () => { + it("reports documented browser reservations and macOS screenshot chords", () => { + expect(isReservedShortcutKey("n", false)).toBe(true); + expect(isReservedShortcutKey("T", false)).toBe(true); + expect(isReservedShortcutKey("p", true)).toBe(true); + expect(isReservedShortcutKey("b", true)).toBe(true); + expect(isReservedShortcutKey("w", false)).toBe(true); + expect(isReservedShortcutKey("w", true)).toBe(true); + expect(isReservedShortcutKey("q", true)).toBe(true); + expect(isReservedShortcutKey("j", false)).toBe(true); + expect(isReservedShortcutKey("o", false)).toBe(true); + expect(isReservedShortcutKey("3", true)).toBe(true); + expect(isReservedShortcutKey("4", true)).toBe(true); + expect(isReservedShortcutKey("5", true)).toBe(true); + }); + + it("does not classify unrelated or platform-inapplicable keys as reserved", () => { + expect(isReservedShortcutKey("k", false)).toBe(false); + expect(isReservedShortcutKey("ArrowUp", true)).toBe(false); + expect(isReservedShortcutKey("j", true)).toBe(false); + expect(isReservedShortcutKey("3", false)).toBe(false); + }); + + it("warns without rejecting a configured key or changing an existing default", () => { + const overrides = sanitizeShortcutOverrides({ "new-session": "t" }); + expect(overrides).toEqual({ "new-session": "t" }); + expect(matchShortcut(keyEvent("t", { ctrlKey: true }), false, overrides)).toBe("new-session"); + expect(matchShortcut(keyEvent("n", { ctrlKey: true }), false)).toBe("new-session"); + }); +}); diff --git a/src/lib/shortcuts.ts b/src/lib/shortcuts.ts index 41f5f0e56..670d04308 100644 --- a/src/lib/shortcuts.ts +++ b/src/lib/shortcuts.ts @@ -1,6 +1,7 @@ import { useEffect } from "react"; import { useSettings } from "./settings.ts"; import type { ShortcutOverrides } from "./shortcutBindings.ts"; +import { physicalKey } from "./keys.ts"; import type { AppActions } from "./actions.ts"; @@ -46,6 +47,15 @@ export function shortcutKeys(id: ShortcutId, overrides: ShortcutOverrides): stri if (Object.hasOwn(overrides, id)) return overrides[id] ? [overrides[id]!] : []; return Object.entries(KEY_TO_ID).filter(([, action]) => action === id).map(([key]) => key); } + +export function shortcutDisplayKeys(id: ShortcutId, overrides: ShortcutOverrides): string[] { + const shortcut = SHORTCUTS.find((candidate) => candidate.id === id)!; + if (id === "voice" || !Object.hasOwn(overrides, id)) return [...shortcut.keys]; + const key = overrides[id]; + if (!key) return []; + return ["Mod", "Shift", key.length === 1 ? key.toUpperCase() : key]; +} + export function shortcutConflict(id: ShortcutId, keys: string[], overrides: ShortcutOverrides): boolean { return SHORTCUTS.some((other) => other.id !== id && shortcutKeys(other.id, overrides).some((key) => keys.includes(key))); } @@ -55,7 +65,14 @@ export function matchShortcut(event: ShortcutEventLike, platformIsMac: boolean, const hasMod = platformIsMac ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey; if (!hasMod || !event.shiftKey || event.altKey) return null; // Shift+Comma produces "<" on common keyboard layouts. - const key = event.code === "Comma" ? "," : /^Digit[0-9]$/.test(event.code ?? "") ? event.code!.slice(-1) : event.key.length === 1 ? event.key.toLowerCase() : event.key; + const code = event.code ?? ""; + // Share the terminal's letter-position convention, but never turn a typed symbol into an app action. + const letter = /^\p{L}$/u.test(event.key) ? physicalKey(event.key, code) : event.key; + const key = code === "Comma" + ? "," + : /^Digit[0-9]$/.test(code) + ? code.slice(-1) + : letter.length === 1 ? letter.toLowerCase() : letter; for (const shortcut of SHORTCUTS) { if (shortcut.id === "voice") continue; if (Object.hasOwn(overrides, shortcut.id) && overrides[shortcut.id] === key) return shortcut.id; @@ -122,7 +139,6 @@ export function useShortcuts(actions: AppActions, enabled: boolean): void { if (!enabled) return; const platformIsMac = isMacPlatform(); const onKeyDown = (event: KeyboardEvent): void => { - if ((event.target as HTMLElement | null)?.closest?.("[data-shortcut-recorder]")) return; const shortcut = matchShortcut(event, platformIsMac, settings.shortcutOverrides); if (shortcut === null) return; if (event.key.startsWith("Arrow") && keepsArrowsForText(event.target)) return; From 4ece1be21fecc29fd2aca966a42c604d3d52d269 Mon Sep 17 00:00:00 2001 From: Hako Date: Thu, 8 Oct 2026 20:40:02 +0900 Subject: [PATCH 2/3] docs(changelog): link shortcut cleanup to #607 --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fce7663e6..569fd4f2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,9 @@ between releases do not reach them. Remote-PC runtime bundles are versioned sepa ### Changed - The default mobile terminal key bar puts Esc, Tab and Ctrl+C first, before the held modifiers - and arrows. Saved layouts keep their order. + and arrows. Saved layouts keep their order. ([#607](https://github.com/devswha/herdr-web-ui/pull/607)) - **Settings → Shortcuts** explains which keys control the app and marks known browser/OS - reservations without changing saved bindings. + reservations without changing saved bindings. ([#607](https://github.com/devswha/herdr-web-ui/pull/607)) - **Settings → Terminal → Clipboard from a pane** is on again: vim, tmux and Claude Code copy to your clipboard from a pane without a trip to Settings. It is also on for anyone 0.4.1 left off without asking (it saved the switch with any other setting). Turn it off if a pane runs output @@ -31,6 +31,7 @@ between releases do not reach them. Remote-PC runtime bundles are versioned sepa ### Fixed - Command palette shortcut hints follow changed bindings and disappear for disabled ones. App shortcuts also recognize physical letter keys on non-Latin layouts outside IME composition. + ([#607](https://github.com/devswha/herdr-web-ui/pull/607)) - macOS Safari direct terminal input preserves Korean syllables when the input method replaces text without emitting composition events, including after switching from English. ([#524](https://github.com/devswha/herdr-web-ui/pull/524) by @suho-han) From 9e24327cfec44eba707a4cd4c87923b142280731 Mon Sep 17 00:00:00 2001 From: Hako Date: Thu, 8 Oct 2026 22:45:31 +0900 Subject: [PATCH 3/3] fix(shortcuts): leave accented Latin letters to the terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The physical-key fallback for non-Latin layouts matched any Unicode letter, so a Latin layout's accented letter fired the binding of the key position it sits on: BÉPO's É on KeyW, Turkish F's Ç on KeyB (the default sidebar key). Only letters outside the Latin script use the position now. --- src/lib/shortcuts.test.ts | 7 +++++++ src/lib/shortcuts.ts | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/lib/shortcuts.test.ts b/src/lib/shortcuts.test.ts index cd8bbf063..133bffa3d 100644 --- a/src/lib/shortcuts.test.ts +++ b/src/lib/shortcuts.test.ts @@ -93,6 +93,13 @@ describe("non-Latin keyboard layouts", () => { expect(matchShortcut(keyEvent("🙂", { ctrlKey: true, code: "KeyK" }), false, { palette: "k" })).toBeNull(); }); + it("leaves an accented Latin letter to the terminal, whatever key position it sits on", () => { + // BÉPO: É is on KeyW; Turkish F: Ç is on KeyB, the default sidebar key's position + expect(matchShortcut(keyEvent("É", { ctrlKey: true, code: "KeyW" }), false, { "toggle-sidebar": "w" })).toBeNull(); + expect(matchShortcut(keyEvent("Ç", { ctrlKey: true, code: "KeyB" }), false)).toBeNull(); + expect(matchShortcut(keyEvent("ı", { ctrlKey: true, code: "KeyI" }), false, { palette: "i" })).toBeNull(); + }); + it("keeps IME guards and Comma/Digit code normalization ahead of layout fallback", () => { expect(matchShortcut(keyEvent("한", { ctrlKey: true, code: "KeyK", isComposing: true }), false, { palette: "k" })).toBeNull(); expect(matchShortcut(keyEvent("한", { ctrlKey: true, code: "KeyK", keyCode: 229 }), false, { palette: "k" })).toBeNull(); diff --git a/src/lib/shortcuts.ts b/src/lib/shortcuts.ts index 670d04308..db707b9b0 100644 --- a/src/lib/shortcuts.ts +++ b/src/lib/shortcuts.ts @@ -67,7 +67,10 @@ export function matchShortcut(event: ShortcutEventLike, platformIsMac: boolean, // Shift+Comma produces "<" on common keyboard layouts. const code = event.code ?? ""; // Share the terminal's letter-position convention, but never turn a typed symbol into an app action. - const letter = /^\p{L}$/u.test(event.key) ? physicalKey(event.key, code) : event.key; + // A Latin layout's own letter stays as typed, accented ones included: BÉPO's É sits on KeyW and + // Turkish F's Ç on KeyB, and neither is that binding. + const nonLatinLetter = /^\p{L}$/u.test(event.key) && !/^\p{Script=Latin}$/u.test(event.key); + const letter = nonLatinLetter ? physicalKey(event.key, code) : event.key; const key = code === "Comma" ? "," : /^Digit[0-9]$/.test(code)