feat(protocol): one home for UI strings (improvise-x7sw, ktv1, k099)

Three tickets asked one question — where do UI strings live relative to a
serde wire enum — and answering it three times is the disease this unit exists
to cure. One answer, in `improvise-protocol/src/help.rs`:

**A UI string is keyed by a command name, never by a key, and never hangs off
the enum that crosses the wire.** `AppMode::label`/`hint_text` are gone;
`help::mode_label`/`help::hint_line` replace them, so the enum a client
deserializes carries no English at all (improvise-k099). **And wherever a
string names a key, the key is looked up rather than spelled**:
`Hint { keys, command, caption }` takes `keys` off `Keymaps::default_keymaps()`,
`command` is the semantic value a localized front end keys off, and `caption`
is the default English — the `AxisColor`/`axis_fg` shape.

- **improvise-x7sw**: `help::which_key` is the which-key popup's rows, keys and
  captions together. The client already holds the keymaps, which is the answer
  the ticket preferred, so `ViewState::transient_prefix` stays an address.
- **improvise-ktv1**: the help pages are data — `help::pages()` — with every
  key row derived. `src/ui/help.rs` keeps its ratatui styles and loses ~310
  lines of content; `web/app.js` paints rows it does not author, and
  `harness/tests/shell.spec.js` asserts that `?` really draws them.

**What this deletes is the measure of it.**
`every_hint_key_resolves_in_its_mode` and its 60-line hint tokenizer are gone:
they existed to hold a hand-written string honest against the keymap, and the
string is derived now, so the property is true by construction
(`doctrine(a_consolidation_that_keeps_its_compatibility_test_is_unfinished)`).
`Keymap::binding_hints` goes with them — `which_key` is the one walker, and the
binding dump reads it. What replaced the test checks the *other* direction, a
mode advertising a command nothing binds, which was never checkable before.

Two defects the derivation exposed, both fixed here: `key_label` dropped
modifiers, so Ctrl-arrows read as bare arrows, and `Char(' ')` printed a literal
space instead of `Space`.

**What is deliberately not derived**, and says so: the import wizard's keys
(its whole keymap is one `Any` binding into `HandleWizardKey`, which decides
per step inside the command — a real hole, left visible), `gg`/`yy`/`ZZ` and
the ex-commands (`HelpLine::Literal`), and the captions, which no test can
judge. `context/code/protocol.md` §8 has the argument.

Cost, stated: the hints changed shape. They are longer, they name arrow keys
beside `hjkl`, and two sub-lists on the Panels page lost their visual indent.
That is the price of the keys being true.

Evidence: 1321 tests pass, and the 7 Chromium shell tests pass against the
assembled site. Two faults were injected into the help overlay — dropping the
`paintHelp` call, and removing the `mode != Help` guard so it is never hidden —
and each fails the new spec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Edward Langley
2026-09-20 11:18:17 -07:00
parent 555d9f1ca7
commit 414d02c4a6
20 changed files with 1442 additions and 618 deletions
+59
View File
@@ -172,3 +172,62 @@ td input {
background: color-mix(in srgb, var(--line) 45%, transparent);
padding: 0 0.25rem;
}
/* The help overlay. Content is improvise_protocol::help's; this is paint. */
#help {
position: fixed;
inset: 2rem;
overflow: auto;
padding: 1rem;
border: 1px solid var(--accent);
background: var(--bg);
font-size: 0.9rem;
}
.help-tabs {
display: flex;
gap: 0.75rem;
border-bottom: 1px solid var(--line);
padding-bottom: 0.5rem;
margin-bottom: 0.75rem;
}
.help-tab.active {
color: var(--accent);
font-weight: 600;
}
.help-body {
display: grid;
grid-template-columns: 16rem 1fr;
gap: 0 1rem;
margin: 0;
}
.help-body dt,
.help-body dd {
margin: 0;
min-height: 1.2em;
}
.help-heading {
grid-column: 1 / -1;
font-weight: 600;
color: var(--accent);
}
.help-banner {
grid-column: 1 / -1;
font-weight: 700;
}
.help-text,
.help-accent,
.help-blank {
grid-column: 1 / -1;
white-space: pre;
}
.help-dim {
opacity: 0.7;
}