|
| 1 | +# The composer follows the bindings the settings page stores |
| 2 | + |
| 3 | +**Status:** Accepted and implemented. |
| 4 | +**Date:** 2026-10-26. |
| 5 | + |
| 6 | +The settings page's **快捷键** tab was a label with no panel behind it: `shortcuts` was the one |
| 7 | +preference entry declared `disabled: true` next to 语音 and 连接, and the composer decided what |
| 8 | +Enter meant in code — `resolveWebuiComposerEnterAction` answered "submit" or "newline" from the |
| 9 | +modifier flags alone. A user could not see that rule, could not change it, and a page that lists |
| 10 | +actions it cannot bind is a page that lies. |
| 11 | + |
| 12 | +The settings page now **lists the composer's keyboard actions, shows the key each one is bound to, |
| 13 | +and changes the binding**. Two actions are bindable, because two actions have keyboard meaning |
| 14 | +today: |
| 15 | + |
| 16 | +| Action | Built-in binding | |
| 17 | +| --- | --- | |
| 18 | +| 发送消息 — send the message | `Enter` | |
| 19 | +| 换行 — break the line | `Shift+Enter` | |
| 20 | + |
| 21 | +## What a binding is |
| 22 | + |
| 23 | +A binding is **one canonical string**: the modifiers in a fixed order, then the base key — |
| 24 | +`Control+Shift+Enter`. The same string is what the browser stores, what the page shows and what the |
| 25 | +composer matches against, so a binding cannot mean one thing on the page and another in the |
| 26 | +composer. The notation, the matching, the conflict rule and the fallback to the built-in bindings |
| 27 | +live in one module (`client/projection/common/shortcuts.ts`) that both sides read, because both sides |
| 28 | +have to agree on what a binding is. |
| 29 | + |
| 30 | +The string is not a keymap. It is the smallest representation that lets a stored preference and a |
| 31 | +key press be compared without either side inventing its own vocabulary. |
| 32 | + |
| 33 | +## Who owns the change, and who reads it |
| 34 | + |
| 35 | +The **settings page owns the change**: it is the only surface that rebinds an action, and it hands |
| 36 | +the result to the settings owner, which persists it under `webui-shortcuts.v1` — the same habit as |
| 37 | +`webui-theme`, `mavis-locale` and `file_line_wrap`, and a key of its own so it cannot collide with |
| 38 | +the language preference. |
| 39 | + |
| 40 | +The **composer reads it when a key is pressed**, not when it renders. The settings page and the |
| 41 | +composer are open at the same time, and a binding changed in the page has to be the binding the |
| 42 | +next key press obeys; a value read at press time is that, and a value held in component state would |
| 43 | +be whatever it was when the composer last rendered. |
| 44 | + |
| 45 | +The composer therefore keeps no shortcut state of its own. What it owns is the rule: a press that |
| 46 | +names the send binding submits, a press that names the newline binding breaks the line, and a press |
| 47 | +that names neither is left to the browser — which is why a key the user unbound stops doing the |
| 48 | +composer's business and starts doing the textarea's. |
| 49 | + |
| 50 | +## What the page refuses |
| 51 | + |
| 52 | +- **A modifier on its own.** A binding is a combination; capturing `Shift` alone would leave a |
| 53 | + shortcut that fires whenever the user holds a key down. |
| 54 | +- **A combination another action already holds.** The page says which action holds it and keeps |
| 55 | + both bindings as they were. |
| 56 | + |
| 57 | +## What this is not |
| 58 | + |
| 59 | +- **Not a keybinding framework.** Two actions, one notation, one page. The voice, connection and |
| 60 | + personalization tabs stay disabled, and nothing here changes the server or the runtime. |
| 61 | +- **Not a new binding for stopping a generation.** 停止生成 is a button and nothing else, and the |
| 62 | + page says so. `Escape` is already spoken for by the menus it closes — the mention menu, the slash |
| 63 | + menu, the permission menu, goal mode and plan mode all claim it — so a shortcut for it would |
| 64 | + fight them rather than serve the user. |
| 65 | +- **Not the language pack.** This page's strings are hard-coded Chinese, like the rest of the |
| 66 | + settings page on this baseline. Where the settings page's language pack lands, these strings move |
| 67 | + with the others; the two changes touch the same tab table and nothing else. |
| 68 | + |
| 69 | +## Consequences |
| 70 | + |
| 71 | +- The composer no longer hard-codes what Enter means. `resolveWebuiComposerEnterAction` is retired: |
| 72 | + the rule it encoded is the default configuration, expressed as the same values the page shows. |
| 73 | +- The built-in behaviour is unchanged. With nothing stored, `Enter` sends and `Shift+Enter` breaks |
| 74 | + the line, and a press that names neither binding is left to the browser — which is what the |
| 75 | + retired rule did with every modified Enter. |
| 76 | +- A newline bound to a key the browser does not break the line on (`Control+J`, say) is broken by |
| 77 | + the composer itself, because a binding the user can set is a binding the user can rely on. |
0 commit comments