Skip to content

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

Merged
stevenjj33 merged 5 commits into
webuifrom
feat/webui-settings-shortcuts
Oct 27, 2026
Merged

stevenjj33 merged 5 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 packages/webui/test/browser 165 passed; 19 failed, every failure environmental (a real Chrome this machine does not have, a PTY its node-pty cannot spawn, and the Linux-only visual baselines)
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

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) pass the whole full profile, including the real-browser suite
verify (macos-latest, 24) pass the platform profile
verification pass the aggregate gate: every planned check is success
source-history-artifact (Release audit) pass no leaks found on all three gitleaks steps
performance pass
docs skipped not a docs-only change

Every green run is on this branch's final head 9daf9cf8:
Source verification,
Release audit,
Performance.

The baseline's own gates were red, and are fixed separately

This branch inherited a baseline that failed on its own. Two pull requests, both
merged into webui before this one, carry the repair:

Neither pull request touches a file this pull request changes, so the diff here
is still only the shortcut page.

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"$''']

The release audit scans the history of every branch, so the entry has to be on
the baseline as well as here — which is why #266 carries the same entry. A
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.

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

pnpm test:webui on this branch, locally: 6 failed / 3340 passed / 34
skipped
. Every failure is No Chrome executable was found
(browser-core-agent-actions ×5, agent-transcript ×1): this machine has no
system Chrome, and the CI runners do. The same suite is green on the runners.

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

The one visual baseline this PR changes

settings-dialog-linux.png is re-recorded here. The shortcuts tab is enabled,
so the settings dialog's nav renders that row in the active colour instead of
the greyed-out one — the picture changes, the code does not.

The recording was done on Linux, on the gate's own image, the same way the
other nine baselines in that folder were. Re-recording all ten produced nine
files that are byte-identical to the ones already in the repository and one
that is not, so the difference is this PR's change and not a drifted
environment.

The launcher's baseline also changed, but that was #266's doing, and it landed
on the baseline before this branch was updated with it.

Not verified here

  • The browser suite was run locally on macOS only; the CI runners ran the gates on Linux and Windows as well.
  • The local browser run cannot run the visual baselines at all (the repository tracks *-linux.png only), so the re-recorded picture was verified on the gate's Linux environment, not locally.

…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.
The webui baseline's own gates are green again after #266, so this branch
is brought up to it before its own checks run.
The baseline's own gates are green again after #266 and #267, so this
branch is brought up to it before its own checks run.
The shortcuts tab is enabled now, so the settings dialog renders differently:
its nav row is no longer greyed out. The baseline was recorded on the gate's
Linux environment, the same way the other nine baselines in this folder were.
@stevenjj33
stevenjj33 merged commit ba946c0 into webui Oct 27, 2026
8 checks passed
@stevenjj33
stevenjj33 deleted the feat/webui-settings-shortcuts branch October 27, 2026 02:56
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