Skip to content

feat(webui): let the settings page rebind the composer's send and newline keys - #265

Open
stevenjj33 wants to merge 2 commits into
webuifrom
feat/webui-settings-shortcuts
Open

stevenjj33 wants to merge 2 commits into
webuifrom
feat/webui-settings-shortcuts

Conversation

@stevenjj33

@stevenjj33 stevenjj33 commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

What this changes

The settings page's 快捷键 tab was a label with disabled: true behind it, and the composer decided in code what Enter meant. This PR makes the tab a real page:

  • 快捷键 is enabled and opens a page that lists the composer's keyboard actions, the key each one is bound to, and a control that captures a new binding.
  • 发送消息 (send the message, built-in Enter) and 换行 (break the line, built-in Shift+Enter) are rebindable.
  • 停止生成 is listed as 仅按钮,无快捷键. It is a button and nothing else: Escape is already claimed by the mention menu, the slash menu, the permission menu, goal mode and plan mode, so a shortcut for it would fight them. No Esc binding was added, per the task book.
  • The binding is stored under webui-shortcuts.v1 — a key of its own, so it cannot collide with feat(webui): let the settings page switch between Chinese and English #250's mavis-locale.
  • The composer reads the stored binding when a key is pressed, not when it renders, so a rebinding takes effect at once and survives a reload.
  • A capture refuses a bare modifier (a binding is a combination) and a combination another action already holds — and the message names the action that holds it.
  • 恢复默认 puts the built-in bindings back, and the composer follows them back.

The built-in behaviour is unchanged: with nothing stored, Enter sends and Shift+Enter breaks the line.

How it is put together

Piece Where
The notation, the matching, the conflict rule packages/webui/src/client/projection/common/shortcuts.ts — one canonical string (Control+Shift+Enter) that the page stores, shows and the composer matches against
The page's own model (which actions, their names, which are bindable) packages/webui/src/client/features/settings/projection/settings-shortcuts.ts
The page packages/webui/src/client/features/settings/ui/ShortcutsSettings.tsx
The rule the composer follows resolveWebuiComposerShortcutAction in packages/webui/src/client/features/composer/projection/composer-state.ts

resolveWebuiComposerEnterAction is retired: the rule it encoded is the default configuration, expressed as the values the page shows. The three-valued answer (submit / newline / ignore) is what lets a press that names no binding fall through to the browser's own behaviour — which is how a key the user unbound stops doing the composer's business.

ADR packages/webui/docs/adr/0026-the-composer-follows-the-bindings-the-settings-page-stores.md records the decision.

Evidence

Check Result
pnpm --filter @mavis/webui typecheck:client / pnpm typecheck:webui pass
pnpm exec vitest run … settings-shortcuts.test.ts composer-intent.test.ts settings-modal.test.tsx browser-settings-page.test.tsx 103 passed
pnpm exec playwright test … settings-shortcuts.spec.mjs 7 passed
pnpm exec playwright test … settings-account-tab.spec.mjs settings-theme.spec.mjs composer.spec.mjs 25 passed
pnpm build:webui pass
node scripts/source-inventory.mjs --write + pnpm check:source pass (5456 files)
pnpm check:webui-dependency 0 direction / 0 cycle / 0 role / 0 publication violations; 2 pre-existing violations, see below

The browser suite covers the six acceptance points one by one: the tab opens and lists the actions with their current keys; a captured combination becomes the binding the page shows; the binding survives closing the settings and coming back; the binding survives a reload; the composer sends on the new key and no longer on the old one (the draft keeps the text and the line break, and nothing was sent for it); 恢复默认 returns to Enter and the behaviour follows it back. A seventh case covers a newline bound to a key the browser does not break the line on (Control+J), which the composer breaks itself.

Negative controls (mutation, then restore)

Mutation What failed
The composer ignores the stored binding and always uses the defaults exactly the two cases that depend on the composer following the stored binding — "the composer sends on the new binding" and "a newline bound to Control+J"
The page stops writing webui-shortcuts.v1 exactly the four cases that depend on persistence — close-and-reopen, reload, the composer's new binding, and the Control+J newline

Both mutations were reverted and the suite was re-run green.

CI

Check Result Notes
changes pass
verify (windows-latest, 24) pass
verify (ubuntu-latest, 24) fail fails at check:webui-dependency, on the baseline's two violations — see below
verify (macos-latest, 24) fail fails at the same gate; check:source (PASS) and typecheck:webui (PASS) both passed before it
verification fail the aggregate gate: it requires the whole matrix to be success, so it fails because of the two above
source-history-artifact (Release audit) pass 1311 commits scanned, no leaks found on all three gitleaks steps
performance pass
docs skipped not a docs-only change

The check:webui-dependency failure is the baseline's

The gate reports exactly two violations, both of them the same one on the baseline:

client/features/workspace/ui/WorkspacePanels.tsx:528: bindings may not access ambient browser capability "document" directly
client/features/workspace/ui/WorkspacePanels.tsx:573: bindings may not access ambient browser capability "document" directly

This PR adds zero direction, cycle, role or publication violations. Fixing the baseline's document access in WorkspacePanels.tsx is a separate change to a file this PR does not touch.

The gitleaks failure was mine, and it is fixed

The first run of the Release audit failed with leaks found: 1. It was this branch's commit, not somebody else's:

rule=generic-api-key  file=packages/webui/src/client/projection/common/shortcuts.ts  line=44
match=SHORTCUT_STORAGE_KEY = "webui-shortcuts.v1"

webui-shortcuts.v1 is a localStorage key name, but it is high-entropy enough (generic-api-key wants entropy > 3.5; this one is 3.84) to read as a credential to gitleaks' generic-api-key rule. The fix is the one this repository already uses for exactly this class of false positive: a documented entry in .gitleaks.toml, next to the other "not a credential" entries:

[[rules.allowlists]]
description = "WebUI shortcut storage key name, not a credential"
condition = "AND"
paths = ['''(^|/)packages/webui/src/client/projection/common/shortcuts\.ts$''']
regexTarget = "match"
regexes = ['''^SHORTCUT_STORAGE_KEY = "webui-shortcuts\.v1"$''']

Re-run of the official gitleaks v8.24.3 (the version the release audit pins) against this branch's history and against the whole origin ref set: no leaks found — and the release audit's three steps (history, source snapshot, built distribution) are green.

Pre-existing failures on the baseline (not this PR)

pnpm test:webui on this branch: 12 failed / 3305 passed / 63 skipped. Every failure is present on the untouched baseline e2de9a1 as well:

  • 6 — No Chrome executable was found (browser-core-agent-actions ×5, agent-transcript ×1). This machine has no system Chrome; the CI runners do.
  • 6 — stale tests against the baseline's terminal/workspace work: webui-w0-css-structure (the suite cannot read the new @xterm/xterm/css/xterm.css import), browser-web-tab-toolbar, workspace-tab-strip, workspace-panel-state ×2, webui-shell, and the check:webui-dependency gate's browser-globals case (WorkspacePanels.tsx reaches for document directly). Running the same files on a clean origin/webui worktree fails identically.

Two assertions that said 快捷键 was still disabled were updated, because it no longer is: packages/webui/test/unit/settings-modal.test.tsx and packages/webui/test/browser/settings-account-tab.spec.mjs.

Scope, and the boundary with #250

Not verified here

  • The visual baselines are untouched: this PR adds a page but changes no existing surface's rendering, so no baseline was regenerated.
  • The browser suite was run locally on macOS only; the CI runners ran the gates on Linux and Windows as well.

…line keys

The 快捷键 tab was a label with `disabled: true` behind it, and the composer
decided in code what Enter meant. The tab is now a page: it lists the
composer's keyboard actions, shows the key each one is bound to, and changes
the binding.

- The notation is one canonical string (`Control+Shift+Enter`) shared by the
  page and the composer, in `client/projection/common/shortcuts.ts`.
- 发送消息 (built-in `Enter`) and 换行 (built-in `Shift+Enter`) are
  rebindable; 停止生成 is listed as 仅按钮,无快捷键, because `Escape` is
  already claimed by the menus it closes.
- The binding is stored under `webui-shortcuts.v1`, a key of its own, and the
  composer reads it when a key is pressed, so a rebinding takes effect at once.
- A capture refuses a bare modifier and a combination another action already
  holds, and names the action that holds it.
- `resolveWebuiComposerEnterAction` is retired: the rule it encoded is the
  default configuration, expressed as the values the page shows.

ADR 0026 records the decision.
The settings shortcut storage key reads like a credential to gitleaks'
generic-api-key rule, so the release audit rejected this branch. It is a
localStorage key name, not a secret, so allowlist it next to the other
documented non-credentials.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant