Files
improvise/web/index.html
T
Edward Langley 414d02c4a6 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>
2026-09-20 11:18:17 -07:00

87 lines
3.7 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>improvise</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<!--
The browser shell (improvise-cr3, improvise-djm).
Deliberately four elements and no framework. Everything that could be
*wrong* — what a cell is, whether it may be typed into, where the
selection sits, how the category tree flattens — is decided in Rust and
arrives already decided (see `render.rs`). This page paints it.
Build both wasm bundles first:
./web/build.sh
then serve this directory over http (a module script will not load from
file://):
python3 -m http.server 8080 --directory web
That is the whole standalone deployment: the document lives in an
in-page Web Worker and persists to this browser's private storage.
?doc= and ?seed= name the document and the file it is first seeded from.
To talk to a separate `improvise-ws-server` process instead — which is
how a document open in the TUI is shared — pass ?ws=host:port; the
scheme defaults to wss:// on https and ws:// on http.
-->
<header>
<h1>improvise</h1>
<span id="view-name"></span>
<span id="mode"></span>
<span id="hint"></span>
<span id="status" role="status"></span>
</header>
<!--
This page is the *client* half and nothing else: it holds no model. What
it now has is a peer it can always reach — a Web Worker in this same tab
hosting the whole App over OPFS (improvise-9x6, improvise-djm) — so a
copy served from a static host, GitHub Pages included, is a working
application rather than a client with nowhere to connect.
The notice below stays static HTML rather than something app.js decides,
for the same reason as before: it is true on every load in either mode,
and a page that cannot reach its peer cannot be trusted to explain why.
What the peer is actually *doing* — starting, connected, unreachable,
gone — is live, and app.js writes it to #status.
-->
<p id="backend-notice">
Everything here runs in this tab: the document lives in a Web Worker
and is saved to this browser's private storage, so edits survive a
reload and never leave the machine. Load a different document with
<code>?doc=name.improv</code>, seeded from
<code>?seed=url</code>. To open a document hosted by a separate
process instead, run
<code>improvise-ws-server model.improv --port 8787</code> and reload
with <code>?ws=host:port</code>. <strong>The scheme defaults to
<code>wss://</code></strong>, because this page is served over https and
an https origin refuses a <code>ws://</code> address as mixed content.
Note that <code>improvise-ws-server</code> speaks plain
<code>ws://</code> only, so reaching it from <em>this</em> page means
terminating TLS in front of it with a reverse proxy or a tunnel — the
README has a worked Caddy example. Served over
<code>http://localhost</code> the same page defaults to
<code>ws://</code> and pairs with it directly, with nothing in between;
that is the case the pairing is really for. Sharing a document is what
the server is for — not running one is what everything above is for.
</p>
<main>
<aside id="categories" aria-label="Categories"></aside>
<div id="grid" tabindex="0" aria-label="Grid"></div>
</main>
<div id="help" role="dialog" aria-label="Help" hidden></div>
<script type="module" src="./app.js"></script>
</body>
</html>