Commit Graph

136 Commits

Author SHA1 Message Date
Edward Langley a8737f3fa5 One page rule for both ends; the help overlay shows how to page
improvise-5xbq: resolve::page_rows is now the one home of the page-scroll
rule; the TUI's Move calls it instead of repeating visible_rows * 3 / 4.
A lockstep case pages (PgDn/PgUp, Ctrl-d/Ctrl-u) on a non-square window,
8 rows by 3 columns, and pins the TUI's landing rows as well as comparing
host and client, since one home moves both ends together. Both faults
the ticket injected (page_rows = window_rows / 2; paging by window_cols)
now fail at the first PgDn, before and after the move to one home.

improvise-1c5m: the help overlay draws the mode's own hint (hint_line,
the string #hint carries) in a sticky footer, so a narrow window whose
app bar wraps under #help still says how to page. Playwright case at
600px, red at base, and red again with the footer made non-sticky.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-23 07:41:03 -07:00
Edward Langley 8ba64c8ef9 Merge gh/main (4b88084) into wire2/2wu3
Resolved context/test-evidence/clients.md: gh/main's version, whose
improvise-w7qd/5kdr paragraphs continue the entry above the conflict,
followed by this branch's two improvise-0j0f/4img entries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 23:14:23 -07:00
Ed Langley 4b880846b9 Merge pull request #202 from fiddlerwoaroof/fix/6yvg-dz8c-json-float
Every JSON parser reads numbers exactly; derive the float_roundtrip check (improvise-6yvg, improvise-dz8c)
2026-09-22 23:06:31 -07:00
Edward Langley 6b3b9f03de Merge remote-tracking branch 'gh/main' into fix/6yvg-dz8c-json-float
# Conflicts:
#	context/test-evidence/clients.md
#	context/test-evidence/command.md
2026-09-22 22:57:58 -07:00
Edward Langley 3cad9c2936 Merge commit '98d10ca5' into wire2/2wu3
# Conflicts:
#	context/test-evidence/clients.md
2026-09-22 22:51:46 -07:00
Edward Langley 5be1306281 improvise-4img: test the clear variant; it is RecordMoved, not RecordGone
B clears the cell while A's editor is open, then A commits. The commit is
refused as RecordMoved. The store no longer holds the record, but
App::resolve_record falls back to the layout and still resolves it, and its
counter has moved. Under the claim-at-commit rival, the same commit is accepted
and re-creates the cell as 42. Corrects the evidence that reasoned RecordGone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 22:51:05 -07:00
Edward Langley 170d94807b Merge remote-tracking branch 'gh/main' into wire2/2wu3 2026-09-22 22:44:47 -07:00
Edward Langley 9512a2db1f The grid-move walk stands far from every edge, so it sees magnitude (5kdr)
21x21 grid, 8x8 window, start (10,10): moves land at 10+d, page-scroll at
10+page_rows(8)*d, jumps at 0/20. grid_move's page rule is page_rows(),
which the test reads rather than restating. Doubled move-selection, a
one-row scroll-rows and a page-size-blind page-scroll each fail.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 22:39:36 -07:00
Edward Langley 66398f163f Merge branch 'fix/help-keys-checks' into fix/w7qd-ssoe-help-guards 2026-09-22 22:37:19 -07:00
Edward Langley 95bd593dbe Merge remote-tracking branch 'gh/main' into fix/help-keys-checks
# Conflicts:
#	context/test-evidence/clients.md
#	context/test-evidence/protocol.md
2026-09-22 22:30:19 -07:00
Edward Langley feefe17d53 Merge remote-tracking branch 'gh/main' into fix/6yvg-dz8c-json-float
# Conflicts:
#	context/test-evidence/command.md
#	context/test-evidence/tooling.md
2026-09-22 22:19:30 -07:00
Edward Langley 95a35e8cce Every JSON parser reads numbers exactly, and the check derives its crates (improvise-6yvg, improvise-dz8c)
improvise-6yvg reproduced. A JSON import in improvise-command and a legacy
JSON .improv load in improvise-io stored 0.24815714285714285 as
0.24815714285714283 in each crate's own test build. The root binary's
`improvise import` used the same lossy parser. Now float_roundtrip is on every
normal serde_json dependency in the workspace: root, improvise-command,
improvise-io, and the optional test-support dependency of improvise-protocol.

improvise-dz8c: the hand-listed cargo-tree check over three frame-parsing
crates is replaced by tests/json_numbers.rs. It reads
`cargo metadata --no-deps` and names every package, including target-specific
entries, whose normal serde_json dependency lacks the feature. The rule and why
it beats "parses a frame", the build graph, and enabling the feature once in
the protocol crate are in context/code/protocol.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 22:19:05 -07:00
Edward Langley 59e67df3bb The inline editor claims the version it opened on, not the one at commit (improvise-0j0f)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 22:14:56 -07:00
Edward Langley d5854bc11c Merge remote-tracking branch 'gh/main' into fix/i0v9-second-tab-lock
# Conflicts:
#	context/test-evidence/clients.md
2026-09-22 22:11:29 -07:00
Edward Langley a1b86001db Merge remote-tracking branch 'gh/main' into wire2/2wu3
# Conflicts:
#	context/test-evidence/clients.md
#	crates/improvise-wasm-client/tests/browser_round_trip.rs
2026-09-22 22:11:28 -07:00
Edward Langley c960713f8c The worker's status claims success only on its first frame (improvise-i0v9)
The worker transport wrote "in-page worker · <doc>" when it spawned the
worker, before anything had answered, so a tab whose worker could not start
showed a success-looking status over a blank page. It now writes "starting
the worker · <doc>…", and createShell's arm() takes an `answer` that sawFrame
writes on the first frame, but only while the watchdog is still armed. A
ReadOnly or SeedMissing notice lands BEFORE the Snapshot, and fail() has
already disarmed, so the claim cannot overwrite the precise reason.

The two-tab spec now logs every #status and denies both the premature claim
and "did not answer" before the page clock is walked. A new spec pins the
exact status sequence on the writable path. Reasoning:
context/code/clients.md, "The watchdog".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 22:10:29 -07:00
Edward Langley 1c4a27243d Every grid move Normal binds must land where it says (w7qd)
scroll-rows and page-scroll derive their landing from their argument as
move-selection does; any bound command that moves the cursor with no
stated landing fails; the numeric floor is gone. Reversed Ctrl-d/u and
PgUp/PgDn, and a dropped jump table with $ sent to column 0, both fail.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 22:07:02 -07:00
Ed Langley 1a6b877482 Merge pull request #194 from fiddlerwoaroof/fix/umi4-edit-seed
improvise-umi4: an editor opens on the value, not on its rounded Display
2026-09-22 22:06:30 -07:00
Edward Langley f24882766e Merge remote-tracking branch 'gh/main' into fix/help-keys-checks
# Conflicts:
#	context/test-evidence/clients.md
2026-09-22 21:57:17 -07:00
Edward Langley 48f52e7da1 Merge remote-tracking branch 'gh/main' into wire2/2wu3
# Conflicts:
#	context/test-evidence/clients.md
2026-09-22 21:56:55 -07:00
Edward Langley 44a183aa26 Merge remote-tracking branch 'gh/main' into fix/6oy4-float-roundtrip
# Conflicts:
#	context/test-evidence/clients.md
2026-09-22 21:54:12 -07:00
Edward Langley 0680f65da8 Merge remote-tracking branch 'gh/main' into fix/umi4-edit-seed
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:52:26 -07:00
Edward Langley 141e41ec4b improvise-umi4: an editor opens on the value, not on its rounded Display
CellValue's Display writes a non-whole number {:.4}, and every editor seed
was that string, so i then Enter on an untouched 1.23456789 stored 1.2346,
in the TUI (pivot and records) and in the browser, which seeds from the
projected edit_text. CellValue::edit_text is now the one home of the seed:
the shortest decimal that reads back as the same f64 (non-finite values
take the core.md §16 token). display_value_in's two arms and
cell_display's records arm all go through it; the now-orphaned
GridLayout::resolve_display is removed.

Text that reads as a number (007) is exact in the seed but still
reclassified by typed(); filed as improvise-ixxx.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:49:20 -07:00
Edward Langley 327fa176ad Evidence for 6562 and v29m
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:43:35 -07:00
Edward Langley 62dba66232 Merge gh/main into fix/6p8q-records-yank-paste
Resolves the append-at-EOF conflict in context/test-evidence/command.md,
keeping gh/main's entries and then this branch's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:43:13 -07:00
Edward Langley 3c6001ac7b Merge remote-tracking branch 'gh/main' into fix/6oy4-float-roundtrip
# Conflicts:
#	context/test-evidence/command.md
#	context/test-evidence/protocol.md
2026-09-22 21:41:49 -07:00
Edward Langley 384e04acbc The inline editor commits by record identity, not viewport position (improvise-2wu3)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:37:56 -07:00
Edward Langley c5e72e889f Merge remote-tracking branch 'gh/main' into fix/help-keys-checks
# Conflicts:
#	context/test-evidence/protocol.md
2026-09-22 21:37:17 -07:00
Edward Langley da8c67109a CellDisplay equality is the wire's, so a NaN cell quiesces (improvise-4w09); docs and evidence for both 2026-09-22 21:37:12 -07:00
Edward Langley 0ed2e41c32 Merge gh/main into fix/6p8q-records-yank-paste
The TUI's records yank now reads records_value unfiltered, the value the browser
yanks since improvise-dsxq; the blank case is filed as improvise-s1lx.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:28:30 -07:00
Edward Langley ef7775efc0 Merge remote-tracking branch 'gh/main' into wire2/j4qt
# Conflicts:
#	context/test-evidence/clients.md
2026-09-22 21:28:26 -07:00
Ed Langley c28b81a6de Merge pull request #173 from fiddlerwoaroof/fix/df2w-i0v9-browser-page
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
df2w: an open, empty formula panel is drawn in the browser
2026-09-22 21:25:46 -07:00
Edward Langley 5ea122b3ea improvise-6p8q/l4mp: context reasoning and test evidence
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:24:54 -07:00
Edward Langley 949810cd30 Merge remote-tracking branch 'gh/main' into fix/help-keys-checks 2026-09-22 21:19:50 -07:00
Edward Langley 3cf3718764 Evidence for wk18, 3t5x and jhdf
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:15:17 -07:00
Edward Langley 3039ce8e58 Merge gh/main into fix/df2w-i0v9-browser-page
Conflict only in context/test-evidence/clients.md, both appended at EOF:
kept df2w's two entries and ktlk's section, whole.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:12:41 -07:00
Edward Langley a77e3a9146 A read-only tab refuses edits instead of painting what it cannot save (improvise-j4qt)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:12:24 -07:00
Edward Langley 5d1bf75271 Merge remote-tracking branch 'gh/main' into fix/dsxq-yank-value-proj
# Conflicts:
#	context/test-evidence/clients.md
2026-09-22 21:11:31 -07:00
Ed Langley a120540e32 Merge pull request #169 from fiddlerwoaroof/fix/ktlk-editor-one-home
CI / Build all bundles (push) Has been cancelled
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Browser JS unit tests (node --test) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled
CI / Lint the harness crate (push) Has been cancelled
CI / Browser harness (OPFS contract + shell) (push) Has been cancelled
CI / Deploy to GitHub Pages (push) Has been cancelled
The editor overlay and Enter read one buffer (improvise-ktlk)
2026-09-22 21:08:56 -07:00
Edward Langley c88b4b5d69 docs: CellDisplay::value and the dsxq evidence
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:08:20 -07:00
Edward Langley 97d17b60f2 Merge remote-tracking branch 'gh/main' into fix/help-keys-checks 2026-09-22 21:05:21 -07:00
Edward Langley a06f50560d The forward-step test pins its key through resolve, not by name
composed() dispatches on the command name and ignores the event, so the
',' it was given pinned nothing: rebinding view-forward to '~' left the
test green (improvise-3ij0). resolve() looks the key up in the shared
keymap first, so the same rival now fails at that assertion.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 21:04:03 -07:00
Edward Langley 942211b921 df2w: an open, empty formula panel is drawn
views/formulas were empty-means-closed, so an open panel over a document
with no formulas built the same RenderModel as a closed one. Option<Vec<_>>
carries both facts in one field; the page hides on null.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 20:54:26 -07:00
Edward Langley bdf9bff472 The editor overlay and Enter read one buffer (improvise-ktlk)
PR #162 (improvise-xg1z) made render.rs paint edit_seed when Editing
arrived without its edit buffer, but commit and append-char still read
the absent buffer as the empty string. So the overlay showed the value,
Enter composed ClearCell, and a typed character replaced the text
instead of extending it: two derivations of what is in the editor.

The fold now seeds the missing buffer (ClientState::settle_editor, next
to settle_mode), and render.rs reads the buffer through read_buffer, as
commit does. edit_seed goes back to pub(crate) and no renderer calls it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 20:53:51 -07:00
Edward Langley 334e218c7b s8h7: i on an aggregate names drill, in help and in the browser
In the TUI, EditOrDrill hands an aggregated cell to DrillIntoCell, so `i`
on an aggregate is drill by another door. The help page called it only
"Edit current cell", and the browser refused it in words unrelated to `>`'s,
so a user could not tell the two were one missing feature.

- help.rs: the edit-or-drill row says it drills into an aggregate.
- help.rs: Normal-mode Enter was captioned "Edit current cell (same as i)"
  but is bound to enter-advance alone, which only moves down; the row moves
  to Cursor movement and says so.
- resolve.rs: the Aggregate arm now reads "an aggregate is edited by
  drilling in — " + not_yet("drill"), the same sentence `>` uses.
- context/code/clients.md and the evidence entry reverse the recorded
  "different sentences on purpose" rationale, which was false about the TUI.
- harness/tests/drill.spec.js follows the new wording.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-09-22 20:53:18 -07:00
Edward Langley 812f555958 Merge remote-tracking branch 'gh/main' into fix/hs0u-lut7-browser-prefix
# Conflicts:
#	context/test-evidence/clients.md
2026-09-22 20:19:38 -07:00
Edward Langley 6596de6140 The prefix guard refuses a request rather than counting it answered
ChatGPT Codex on PR #161, and the finding was correct. The structural
guard counted `requested(name).is_some()` as "answered", but `chord`
maps `Step::Request(_)` to `None` — so a future prefix binding for
`save` would have PASSED the guard while the request was discarded at
runtime. That is the exact shape improvise-lut7 was filed about: a green
derivation over a broken thing.

The guard now refuses a request-backed prefix name. `prefixed` is split
so the binding step is `chord(state, event, Option<&Binding>)`, which
lets a test drive a binding the shared table does not contain — the
regression the review asked for. It shows the outcome is the disarm
ALONE, and asserts `requested("save").is_some()` first so the guard's
refusal clause is known to be able to fire.

Two faults injected. Binding `save` behind `g` in `default_keymaps` —
the scenario the review described — fails the guard, naming `save`. And
making `chord` return `KeyOutcome::Request(r)` (the review's other
suggested option) fails the new test, because the disarm is then lost:
that is the evidence the alternative trades one stranding for another.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 20:14:22 -07:00
Edward Langley b680523296 An editing overlay with no buffer shows the cell (improvise-xg1z)
`render_model` built `editing` as
`is_editing().then(|| buffers.get("edit").cloned().unwrap_or_default())`,
so an `AppMode::Editing` that arrived without its `SetBuffer` painted
`Some("")` — a blank editor drawn over a cell that still holds a value.
The page opens an `<input>` seeded from that string, so the next Enter
commits the empty string and the value is gone. An ABSENT buffer and an
EMPTY one are two different facts, and `unwrap_or_default` collapsed them.

Reachable because the pair is two `Command`s rather than one: this
client's own resolver emits `SetBuffer` then `ChangeMode`, and a
`Snapshot` or a host batch can carry the mode without the buffer.

The fallback is `improvise_protocol::edit_seed`, which is the EXISTING
home of "what an editor opened here starts with" — `seed_at` at the
cursor, the function `enter-advance` already seeds from. A second
reconciliation of the scroll offset inside `render.rs` would have been
gotcha 42's own trap, so this costs one `pub` rather than a copy.

Two tests, and the second is the one that matters for the shape of the
fix: an empty buffer is a state the user typed their way into with
backspace, so refilling it would make the last backspace un-doable. The
over-broad repair — "empty or absent, refill" — fails that test alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 20:06:23 -07:00
Edward Langley 6c46502f52 Resolve prefix chords in the browser (improvise-hs0u, improvise-lut7)
`resolve` answered `Binding::Prefix` with `KeyOutcome::Upstream`
unconditionally, and `Host::handle` answers `ClientFrame::Key` with an
explicit empty vec (gotcha 47) — so `g`, `y` and `Z` were black holes and
everything behind them was dead. `grid_move`'s `GridMove::ToStart(true)`
arm was correct and unreachable, so a browser user could reach the last
row and never get back to the first.

Arming is now a `Command::SetTransientPrefix`, which was already on the
wire and already folded by `reduce_view`; nothing composed it. The second
key is resolved in the sub-keymap through `Keymaps::prefix_keymap`,
mirroring `App::dispatch_key` line for line — including that the prefix
is spent whether or not the second key matched, because leaving it armed
strands every key after it in a sub-keymap the user has forgotten about.

Every outcome of a chord therefore carries the disarm, so the second key
can never be `Upstream`: there would be nowhere to put it. The two names
that reach no composition, `wq` and `toggle-col-group-under-cursor`, are
answered by `declined` instead (improvise-rmvx: silence was worse), and
`every_name_bound_behind_a_prefix_is_answered_by_this_client` makes that
structural — a future prefix-bound name nothing answers fails the build.

`p` could never do anything because nothing could put a value in
`ViewState::yanked` (improvise-lut7) — a *state* reason no name-level
derivation can see, which is why paste appeared on neither the declined
list nor the host-needed one. `yank` now composes, off the `edit_text`
the host already projects, which is the value `YankCell` evaluates.

The three prefixes leave `the_command_names_that_still_need_the_host_are_these`.

Not predicted by either ticket: `tests/lockstep.rs` — the instrument for
"host and client agree on a keypress" — passed unchanged through all of
this, because no chord had ever been driven through it. Every key it
pressed was a single key. `the_g_chord_returns_to_the_first_row_on_both_ends`
closes that gap and compares both ends after *each* press.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:59:58 -07:00
Edward Langley e645ec883d fix(tui): , walks the view history forward (improvise-htl6)
`view-forward` was bound in NO keymap, in either front end, from the day it was
written. The TUI reached it as `:view-forward`; a browser could not reach it at
all, because `declined()` refuses to enter CommandMode at the door. It survived
because pressing `<` twice is the only way to observe the forward stack, and
until PR #115 no client maintained one.

THE KEY IS `,`, AND `>` IS WHY THE OBVIOUS ANSWER IS WRONG. `<` is `view-back`
and `>` is `drill-into-cell`, so the pair that LOOKS symmetric is not one: `>`
moves within the grid, `<` moves between views. Binding the forward half to
anything reading as `>`'s partner would deepen the confusion that hid the gap.
`,` is the unshifted twin of `<`, so both halves of the history sit on one
physical key, and it carries no direction-in-the-grid meaning.

IT IS DELIBERATELY NOT A CTRL- KEY. Ctrl-R, the universal "forward through a
history" spelling, is browser reload. The keymap is SHARED between the two front
ends, so a key chosen for the terminal is one the browser must live with, and
improvise-clso is already open for exactly this against Ctrl-S.

The absence assertion PR #117 placed in
`the_forward_step_mirrors_its_cmd_and_no_key_reaches_it` is INVERTED rather than
deleted: renamed `..._and_one_key_reaches_it`, it now asserts the binding exists,
keeps `view-back` as the control, and pins the key itself so the binding cannot
drift while the presence check stays happy.

Reasoning: context/code/tui.md.
Evidence: context/test-evidence/{command,clients}.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-22 19:25:18 -07:00