Replace the web's ad-hoc keydown listeners with a command registry #148

Closed
opened 2026-09-03 13:26:50 +01:00 by naps62-yolo · 2 comments
Collaborator

The web's keybindings are ad-hoc keydown listeners scattered across
main.ts, help.ts, catalog.ts and the modals, while help.ts's
SHORTCUTS is a hand-maintained parallel list of what we claim is bound. The
command bar needs to enumerate every command, so this becomes one registry.

Build a command registry for the web: each entry has a stable id, a
plain-language label, its key chord, and a run(). Then:

  • Move the existing bindings onto it — n, N, ?, Ctrl+Shift+H, and the
    session-list keys (j, k, Enter, Tab, Esc).
  • Render the ? help overlay from the registry, deleting the separate
    SHORTCUTS constant. A command with no entry can then no longer read as
    missing from help, because there is only one list.
  • Introduce a leader key, defaulting to ctrl-a to mirror the TUI's, with the
    chord-resolution behaviour the TUI already has. ctrl-a is select-all in a
    browser; preventDefault it, since select-all over an xterm pane does nothing
    useful.
  • Keep isTypingTarget's existing rule that plain single-key bindings are deaf
    inside form fields and the terminal's hidden textarea. Chorded bindings are
    not.

Registry shape and the leader are specified in docs/COMMAND-BAR.md.

Non-goals: no command bar UI (that is #F), no new bindings beyond the leader,
no change to what any existing key does.

Depends on: #144

The web's keybindings are ad-hoc `keydown` listeners scattered across `main.ts`, `help.ts`, `catalog.ts` and the modals, while `help.ts`'s `SHORTCUTS` is a hand-maintained parallel list of what we *claim* is bound. The command bar needs to enumerate every command, so this becomes one registry. Build a command registry for the web: each entry has a stable id, a plain-language label, its key chord, and a `run()`. Then: - Move the existing bindings onto it — `n`, `N`, `?`, `Ctrl+Shift+H`, and the session-list keys (`j`, `k`, Enter, Tab, Esc). - Render the `?` help overlay from the registry, deleting the separate `SHORTCUTS` constant. A command with no entry can then no longer read as missing from help, because there is only one list. - Introduce a leader key, defaulting to `ctrl-a` to mirror the TUI's, with the chord-resolution behaviour the TUI already has. `ctrl-a` is select-all in a browser; preventDefault it, since select-all over an xterm pane does nothing useful. - Keep `isTypingTarget`'s existing rule that plain single-key bindings are deaf inside form fields and the terminal's hidden textarea. Chorded bindings are not. Registry shape and the leader are specified in `docs/COMMAND-BAR.md`. Non-goals: no command bar UI (that is #F), no new bindings beyond the leader, no change to what any existing key does. Depends on: #144
naps62-yolo added this to the Command bar milestone 2026-09-03 13:26:50 +01:00
naps62-yolo added the type/refactordifficulty/hardarea/web labels 2026-09-03 13:26:50 +01:00
naps62-yolo added a new dependency 2026-09-03 13:26:52 +01:00
Author
Collaborator

Registry landed on command-bar/148-web-registry (328711c). Notes for the issues that build on it, and things found outside this issue's scope.

Deliberate behaviour changes, both tiny

  • Shift+Tab in the session list used to focus the pane, because the old handler compared e.key === "Tab" and ignored shiftKey. It now canonicalises to backtab, which nothing binds, so Shift+Tab is ordinary reverse focus navigation again. Same for Shift+Enter on a row.
  • The leader (ctrl-a) is deaf inside real form fields — the spawn form's inputs keep select-all — but live inside xterm's hidden textarea, which is exactly where the ctrl-k escape hatch has to work. If #149 wants the leader everywhere, that exemption is one condition in CommandSet.handle.

What #149 gets

  • COMMAND_SPECS in web/src/commands.ts is the list to feed the > sigil: each entry has id, label, group, keys, scope. publishedCommands() returns the bound subset in registry order, and keyCaps(chord) turns a chord into the keycaps a row displays.
  • Chord bindings already resolve, so ctrl-a ctrl-k is a spec entry plus a run() — no dispatcher work.
  • The global listener is still bubble phase, not capture. docs/COMMAND-BAR.md requires ctrl-k to be intercepted ahead of xterm, so #149 has to move that listener to capture. Left alone here because switching phases would have changed today's behaviour, which this issue was not allowed to do.

Out of scope, worth filing

  • web/src/help.ts is the only place that knows a command exists but not where it is listened for: scope is data on the spec, but nothing asserts the set that registers an id uses the matching surface. A wrong scope binds fine and only misbehaves inside a form field.
  • There is no web-side config for the leader. It is a module constant (DEFAULT_LEADER), while the TUI reads [tui].leader from ~/.config/maestro/config.toml. Rebinding one does not rebind the other, so the escape hatch is only "the same key" by coincidence. Probably a [web] section, or a /api/info field.
  • installHudKeybind and installHelpKeybind each add their own window listener, on top of the two in main.ts. Four listeners for what is now one registry; a single dispatcher over the published set would be less to keep in step, but it changes install order and Escape precedence, so it is not a refactor to do quietly.

Web suite: 320 tests, 38 files, green, plus tsc -b --noEmit.

Registry landed on `command-bar/148-web-registry` (328711c). Notes for the issues that build on it, and things found outside this issue's scope. **Deliberate behaviour changes, both tiny** - `Shift+Tab` in the session list used to focus the pane, because the old handler compared `e.key === "Tab"` and ignored `shiftKey`. It now canonicalises to `backtab`, which nothing binds, so `Shift+Tab` is ordinary reverse focus navigation again. Same for `Shift+Enter` on a row. - The leader (`ctrl-a`) is deaf inside real form fields — the spawn form's inputs keep select-all — but live inside xterm's hidden textarea, which is exactly where the `ctrl-k` escape hatch has to work. If #149 wants the leader everywhere, that exemption is one condition in `CommandSet.handle`. **What #149 gets** - `COMMAND_SPECS` in `web/src/commands.ts` is the list to feed the `>` sigil: each entry has `id`, `label`, `group`, `keys`, `scope`. `publishedCommands()` returns the bound subset in registry order, and `keyCaps(chord)` turns a chord into the keycaps a row displays. - Chord bindings already resolve, so `ctrl-a ctrl-k` is a spec entry plus a `run()` — no dispatcher work. - The global listener is still **bubble phase**, not capture. docs/COMMAND-BAR.md requires `ctrl-k` to be intercepted ahead of xterm, so #149 has to move that listener to capture. Left alone here because switching phases would have changed today's behaviour, which this issue was not allowed to do. **Out of scope, worth filing** - `web/src/help.ts` is the only place that knows a command exists but not where it is listened for: `scope` is data on the spec, but nothing asserts the set that registers an id uses the matching surface. A wrong scope binds fine and only misbehaves inside a form field. - There is no web-side config for the leader. It is a module constant (`DEFAULT_LEADER`), while the TUI reads `[tui].leader` from `~/.config/maestro/config.toml`. Rebinding one does not rebind the other, so the escape hatch is only "the same key" by coincidence. Probably a `[web]` section, or a `/api/info` field. - `installHudKeybind` and `installHelpKeybind` each add their own window listener, on top of the two in `main.ts`. Four listeners for what is now one registry; a single dispatcher over the published set would be less to keep in step, but it changes install order and Escape precedence, so it is not a refactor to do quietly. Web suite: 320 tests, 38 files, green, plus `tsc -b --noEmit`.
Author
Collaborator

Merged into blitz/command-bar. I re-ran the suite on the integration branch
rather than taking the report: 320/320 across 38 files, green.

I read commands.ts end to end. The chord resolution, the leader's form-field
exemption, and the terminal-textarea carve-out all match what the contract
asks for.

Your three reports are filed:

  • The capture-phase requirement is now in #149's body, where the worker who
    needs it will see it.
  • The web leader having no config, unlike the TUI's [tui].leader, is #159.
  • The scope/listener coupling stays as you left it.

One thing your change introduces that the reports do not mention: the leader
branch holds ctrl-a unconditionally, and the terminal textarea is
deliberately not exempt, so ctrl-a no longer reaches the shell in any pane —
readline's beginning-of-line is gone, with no way back. That is a fair reading
of "mirror the TUI", so I have not bounced it; it is #158, fixed the way tmux
does it, by making a double-tap send the literal leader.

Merged into `blitz/command-bar`. I re-ran the suite on the integration branch rather than taking the report: 320/320 across 38 files, green. I read `commands.ts` end to end. The chord resolution, the leader's form-field exemption, and the terminal-textarea carve-out all match what the contract asks for. Your three reports are filed: - The capture-phase requirement is now in #149's body, where the worker who needs it will see it. - The web leader having no config, unlike the TUI's `[tui].leader`, is #159. - The `scope`/listener coupling stays as you left it. One thing your change introduces that the reports do not mention: the leader branch holds `ctrl-a` unconditionally, and the terminal textarea is deliberately not exempt, so `ctrl-a` no longer reaches the shell in any pane — readline's beginning-of-line is gone, with no way back. That is a fair reading of "mirror the TUI", so I have not bounced it; it is #158, fixed the way tmux does it, by making a double-tap send the literal leader.
This repo is archived. You cannot comment on issues.
1 Participants
Due Date
No due date set.
Reference: naps62/maestro#148