fix(web): an advertised key is never silent, a formula is visible, and +cat commits

improvise-rmvx, improvise-fdt6, improvise-gcvb — three faces of the same
thing: a browser offering something and then doing nothing.

rmvx. `the_command_names_that_still_need_the_host_are_these` was built with
`in_mode(AppMode::Normal)`, so it only ever asked about Normal-mode bindings:
`add-record-row` (RecordsNormal) and `delete-formula-at-cursor` (FormulaPanel)
were outside its reach entirely and could never have failed it. A named list
that silently scopes itself to one mode is a guard that looks broader than it
is. `every_advertised_command_answers_in_its_own_mode` crosses
`help::advertised` — command NAMES, so no hint string is tokenised — against
`resolve` in every mode, over a fixture furnished enough that an `Upstream` can
only mean an unhandled name. It found a seventh instance the ticket had not
predicted: `view-back` is advertised in RecordsNormal.

Six names now decline out loud, each with its own true subject, out of one
`declined` table; `stranding` returns the subject rather than a bool, so `:`
says the command line is unavailable instead of doing nothing. A declining step
takes its whole `Binding::Sequence` with it, which is what keeps `o` ("add a
record row, then edit it") from opening an editor on a row nobody created.

fdt6. `commit-buffer formula` already resolved; what did not exist was any way
to SEE the typing. `RenderModel::editing` is scoped to the two cell-editing
modes and reads the "edit" buffer by name, so FormulaEdit, CategoryAdd, ItemAdd
and the three prompts opened and swallowed every character. `RenderModel::
minibuffer` is `AppMode::minibuffer()` plus the buffer that config names, read
through `improvise_protocol::read_buffer` so gotcha 11 keeps its one home, and
`web/app.js` paints it.

gcvb. The records-mode early return was broader than its reason. A
`CellId::Create` commits the typed NAME as one existing `ModelCommand`, reads
no `record_col` and writes no cell, so the return narrows rather than
disappears. `+cat` must NOT advance: an inserted column renumbers every index
to its right and the advance was computed from the pre-commit layout, so a
client that advanced anyway would land one column off, silently. Both halves
proved by injection against `CommitAndAdvance` in `tests/lockstep.rs`.

Every new guard was proved by injecting the fault it names. The one worth
repeating: `web/tests/render_surface.test.js` went green over a page that
painted nothing, because the string `model.minibuffer` was inside a function
nothing called. The static check is a completeness check, never an execution
check — `harness/tests/keys.spec.js` caught it in Chromium on the first run.

1394 → 1397 Rust tests; harness 17 → 19 passed, 3 skipped; node 26.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
Edward Langley
2026-09-20 16:36:03 -07:00
parent 00f7678822
commit d1ce2ecbc3
12 changed files with 695 additions and 97 deletions
+12
View File
@@ -30,6 +30,7 @@ const el = {
views: document.getElementById("views"),
tiles: document.getElementById("tiles"),
mode: document.getElementById("mode"),
minibuffer: document.getElementById("minibuffer"),
hint: document.getElementById("hint"),
help: document.getElementById("help"),
status: document.getElementById("status"),
@@ -158,6 +159,7 @@ function repaint() {
// itself; see `RenderModel::hint`.
el.hint.textContent = model.hint;
paintHelp(model);
paintMinibuffer(model);
shell.setModel(model.status);
el.viewName.textContent = model.view_name;
el.grid.replaceChildren(gridTable(model));
@@ -179,6 +181,16 @@ function repaint() {
if (changed) sendViewport();
}
/** The bottom-bar prompt. `null` is "no text-entry mode is open", decided in
* `render.rs`, so the page asks nothing about the mode (improvise-fdt6). */
function paintMinibuffer(model) {
const mb = model.minibuffer;
el.minibuffer.hidden = mb === null || mb === undefined;
if (el.minibuffer.hidden) return;
el.minibuffer.className = mb.color.toLowerCase();
el.minibuffer.textContent = mb.prompt + mb.value;
}
/** The help overlay. An empty `help` is what says it is closed -- the page
* asks no question about the mode, and writes none of the content: the rows,
* their keys and the page titles are `improvise_protocol::help`'s, the same
+7
View File
@@ -83,6 +83,13 @@
<div id="grid" tabindex="0" aria-label="Grid"></div>
</main>
<!--
The bottom bar. The TUI draws the minibuffer INSTEAD of the status line;
here it is a line of its own, because #status also carries the transport's
own failures and a repaint must not erase one (gotcha 48).
-->
<div id="minibuffer" role="status" hidden></div>
<div id="help" role="dialog" aria-label="Help" hidden></div>
<script type="module" src="./app.js"></script>
+18
View File
@@ -272,3 +272,21 @@ td input {
.help-dim {
opacity: 0.7;
}
/* The bottom-bar prompt while a text-entry mode is open. The colour is the
mode's own answer (`MinibufColor`); this is the last translation, exactly
as `draw.rs::minibuf_fg` is the TUI's. */
#minibuffer {
padding: 0.25rem 0.5rem;
font-family: inherit;
border-top: 1px solid var(--accent);
}
#minibuffer.green {
color: #7ec699;
}
#minibuffer.cyan {
color: #6fd0d8;
}
#minibuffer.yellow {
color: #d8c96f;
}