feat(browser): the page draws the mode's hint instead of spelling keys

`?` now enters `Help` — the keymap says so and this client resolves
`enter-mode` — but the page has no help overlay: `src/ui/help.rs` is 600
lines of ratatui and nothing shared carries its content. Landing the key
without this would have swapped the mode label to HELP over an unchanged
grid, which looks broken rather than unfinished.

`AppMode::hint_text()` already exists in the protocol crate and is what the
TUI's footer draws, so `RenderModel::hint` relays it. Every mode now states
its own keys in the browser, the page spells none of them, and the modes
this client can enter but not yet draw say what they are. A real help
overlay needs the content moved into the shared layer first, which is the
same move design-principles §5 asks for and is filed rather than faked.

`web/*` is unrun; `node --check` only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
This commit is contained in:
Edward Langley
2026-09-19 21:35:54 -07:00
parent e13635638c
commit 59cc6720b5
4 changed files with 25 additions and 0 deletions
@@ -56,6 +56,15 @@ pub struct RenderModel {
pub categories: Vec<RenderCatRow>, pub categories: Vec<RenderCatRow>,
/// Whether the panel is open, so the page does not decide. /// Whether the panel is open, so the page does not decide.
pub category_panel_open: bool, pub category_panel_open: bool,
/// The keys this mode offers, off `AppMode::hint_text()` — the same
/// string the TUI's footer draws.
///
/// The page shows it rather than spelling any keys of its own, which is
/// what keeps a second front end from growing its own idea of the
/// bindings (design-principles §5). It also makes the modes this client
/// can *enter* but not yet *draw* — `Help` above all — state what they
/// are instead of looking like a broken grid.
pub hint: String,
/// What the user has typed so far, while a keyboard edit is open on the /// What the user has typed so far, while a keyboard edit is open on the
/// selected cell — `None` when none is. /// selected cell — `None` when none is.
/// ///
@@ -168,6 +177,7 @@ impl ClientState {
.map(|(i, entry)| RenderCatRow::of(entry, i == self.view.cat_panel_cursor)) .map(|(i, entry)| RenderCatRow::of(entry, i == self.view.cat_panel_cursor))
.collect(), .collect(),
category_panel_open: self.view.category_panel_open, category_panel_open: self.view.category_panel_open,
hint: self.view.mode.hint_text().to_string(),
editing: self editing: self
.view .view
.mode .mode
@@ -392,6 +402,11 @@ mod tests {
let model = client.model(); let model = client.model();
assert_eq!(model.mode, improvise_protocol::AppMode::Normal.label()); assert_eq!(model.mode, improvise_protocol::AppMode::Normal.label());
assert_eq!(model.status, "saved"); assert_eq!(model.status, "saved");
assert!(
model.hint.contains("i:edit"),
"the page draws the mode's own hint rather than spelling keys: {}",
model.hint
);
assert_eq!(model.revision, client.revision()); assert_eq!(model.revision, client.revision());
} }
+4
View File
@@ -55,6 +55,7 @@ const el = {
grid: document.getElementById("grid"), grid: document.getElementById("grid"),
categories: document.getElementById("categories"), categories: document.getElementById("categories"),
mode: document.getElementById("mode"), mode: document.getElementById("mode"),
hint: document.getElementById("hint"),
status: document.getElementById("status"), status: document.getElementById("status"),
viewName: document.getElementById("view-name"), viewName: document.getElementById("view-name"),
}; };
@@ -204,6 +205,9 @@ function repaint() {
painted = model.revision; painted = model.revision;
el.mode.textContent = model.mode; el.mode.textContent = model.mode;
// The mode's own hint, off `AppMode::hint_text()`. The page spells no
// keys itself; see `RenderModel::hint`.
el.hint.textContent = model.hint;
modelStatus = model.status; modelStatus = model.status;
paintStatus(); paintStatus();
el.viewName.textContent = model.view_name; el.viewName.textContent = model.view_name;
+1
View File
@@ -36,6 +36,7 @@
<h1>improvise</h1> <h1>improvise</h1>
<span id="view-name"></span> <span id="view-name"></span>
<span id="mode"></span> <span id="mode"></span>
<span id="hint"></span>
<span id="status" role="status"></span> <span id="status" role="status"></span>
</header> </header>
+5
View File
@@ -89,6 +89,11 @@ td.selected {
outline: 1px solid var(--accent); outline: 1px solid var(--accent);
} }
#hint {
opacity: 0.7;
font-size: 0.85em;
}
/* A keyboard edit in progress. Left-aligned even in a numeric column, /* A keyboard edit in progress. Left-aligned even in a numeric column,
because what is drawn is the text being typed rather than a value. */ because what is drawn is the text being typed rather than a value. */
td.typing { td.typing {