context/code/protocol.md gains section 10 and tui.md's colour section now
describes face_style; the pointers to axis_fg, minibuf_fg and AxisColor
elsewhere follow. Evidence for the new tests.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
Review of #307 at 7b9e868b: the duplicate-step evidence and argument still
said 12, tui.md bounded a collision tuple by twelve, and the coupling
inventory named no beads. The page-bar entry now notes its unclipped
set_string can overwrite the right border.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
Every wizard write goes through Panel, which clips to the panel and saturates
its height arithmetic, so no step panics on a small terminal. The bottom row
truncates in cells, so a wrapped message with a multibyte character is drawn
whole. A scrolling list's range is named on its header row, where no item can
cover it. Tests: a multibyte message at every width, wrap bytes vs cells, a
no-panic sweep of every step at 20-100 x 3-20, the first item filling the list.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
When source records collide on every coordinate, the import wizard now asks:
sum them, keep them separate with a generated occurrence category, or go
back. Collisions are found before the build by the same placement pass the
build uses, and are carried to the TUI as structured data (WizardCache) and
listed in full, scrollable. At the 12-category cap, "keep separate" is shown
disabled with its reason and remedy.
Every wizard warning and message now wraps to the panel instead of being
truncated, and long lists scroll.
Headless paths no longer sum silently: `import --no-wizard` takes
`--on-duplicate refuse|sum|separate` (default refuse), and `:import-json`
refuses and points at `:import`.
Reasoning: context/code/formula-and-io.md, "Colliding records are a
question, not a default"; context/code/tui.md; context/code/cli.md §11.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
Grid: App::cmd_context counted visible_cols from term_width while
draw_content gave the grid term_width minus the 32-column side panel, so
with any panel open the cursor was scrolled past the last drawn column.
viewport::grid_width, reached through App::grid_width, is now the one
answer: draw_content splits on it and the keypress path counts on it.
Tile bar: the scroll measured from the end of the prefix while the draw
began one column later, two once scrolled (the left arrow), so the
selected tile could be dropped. tiles_left(start) is now where both the
scroll (first_tile) and the draw begin.
The grid defect dates from b5418f2e; it became the launch state when
50e4a372 opened the category panel by default, which is why it read as
a regression. Mixed-width columns still lose the cursor with no panel
open: improvise-gtn7.2. Reasoning: context/code/tui.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
The toast hid every status after six seconds, so a refusal the user was
still fixing vanished with its prompt open: su4u's bug on a timer. The
browser now asks the TUI's question. EditorSession moves into
improvise-protocol, ClientState records which editor a status was
written in, and RenderModel::status_stands tells the toast to hold it
until that editor closes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
#257 restored a stale message whenever the mode after a key equalled the
mode before it. Editing and RecordsEditing commit with CommitAndAdvance,
which re-opens an equal AppMode on the next cell. So the message followed
the editor from cell to cell until Esc. The reviewer's repro was R, o, 5,
Tab: "Added new record row", or o's "That record row is already here"
refusal, survived the Tab.
press now compares an EditorSession, made of the things an opening sets:
- the whole AppMode, not its variant;
- the cursor cell, which is a cell editor's target;
- the editor's seed, which EnterEditAtCursor re-seeds from the cell, so a
commit that re-opens on the same cell (Tab in the last column) is new.
A generation counter was rejected: editors are opened by more than one
effect, and a counter in each is a hand-kept list. The view was left out:
no minibuffer key can switch it and stay.
Tests:
- four press scenarios: the two records repros, Enter in a plain grid,
and Tab in the last column;
- a direct check that ItemAdd on another category is another editor.
Rivals: mode-only (#257 as pushed), no cell, no seed and discriminant
each fail a distinct step.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
#233 clears the status message at the start of every TUI keypress. In a
text-entry mode the message sits beside the prompt, and a formula that
does not parse keeps the editor open with the error. So the error
vanished on the first correcting key.
press now remembers the mode and the message. It clears the message as
before and dispatches the key. It puts the stale message back only if
the key left the user in the same text-entry mode and wrote no message
of its own. Editing keys keep the refusal on screen, a key's own message
still wins, and leaving the minibuffer (a successful commit or Esc)
drops it.
The mode check is observed, not predicted: which keys commit is the
keymap's knowledge, and the TUI loop does not copy it.
Rivals, each injected into press and restored: never restoring (#233),
restoring over a key's own message, and restoring whatever the mode each
fail a distinct step of the test.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
improvise-cq3d: nothing in production cleared status_msg, and draw_status
prints it instead of the derived hint, so after the first message of a
session the TUI never drew the key hint again, in any mode.
run_tui now sends each key through draw.rs::press, which applies
SetStatus("") and then dispatches: a message a key produces is drawn after
that key and expires at the next one, vim-style, so an error is never
wiped before it is drawn. The expiry lives in the TUI front end rather than
App::dispatch_key, which the lockstep harness shares with the browser. The
browser already shows messages transiently (a TOAST_MS toast) beside an
always-drawn #hint. Reasoning: context/code/tui.md.
The test drives press: a stale message must give way to TileSelect's hint,
a message must survive its own key, and give way at the next. Red at base
at the first step; clearing after dispatch fails the second.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
`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
Two hand-written key tables were drawn BESIDE derived ones, in a different
notation, and one of them was false.
The tile bar's non-TileSelect hint read " Ctrl+^v<-> to move tiles" while
Ctrl-Left/Right/Up/Down are bound in Normal to enter-tile-select -- pressing one
ENTERS tile mode and moves nothing. It was the only persistent on-screen
documentation of those keys, and it spelt Ctrl+ where key_label, and therefore
every derived hint and help row, spells Ctrl-.
The fix DELETES both of the tile bar's hints rather than rewriting one, which
departs from the ticket's acceptance criteria and does so on the ticket's own
evidence. rr8n proposed rewriting the string and suppressing the STATUS BAR's
hint in TileSelect. But the derived status line is a strict superset of the tile
bar's literal -- it carries Space, h/l and Tab, which the tile bar's omits, as
the ticket's own finding 2 records -- and it cannot go stale, being read off the
keymap. Suppressing it would have deleted information and kept the table that had
just been shown to rot. In Normal the derived line already says T:tiles, so only
the Ctrl-arrow spelling is lost, and it was never documented correctly.
The wizard goes the other way and there the ticket is right. Its overlay draws
correct per-step hints; the status bar drew one static line under it, wrong on
four of eight steps, including telling the user Esc cancels on Done where Esc
goes BACK. That line is derived too, but from the wrong input -- the wizard's
real keys are resolved per step inside HandleWizardKey. So draw_status suppresses
its hint fallback for that one mode.
The rule, stated in context/code/tui.md: keep whichever table is derived from the
thing it describes.
Five tests. Four faults injected, each caught by a distinct test; the sharpest is
suppressing the hint fallback unconditionally, which passes both tests aimed at
the suppression and is caught only by the counterweight asserting TileSelect
still advertises its keys. Two things that let the false hint survive are
recorded: every_hint_key_resolves_in_its_mode cannot see a literal in a widget,
and the widget's own characterization test renders at 80 columns, where the hint
does not fit and is never drawn.
Evidence: context/test-evidence/tui.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
`fill_line` composed the title bar's two segments by concatenating them and
letting ratatui clip. ratatui clips the RIGHT, and the right segment is
" ?:help :q quit " -- the only thing on screen telling a stuck user how to get
help or get out. The left segment is decoration whose length is unbounded, so
the affordance was the first thing sacrificed: measured through a TestBackend, a
document named "Quarterly Regional Sales Analysis" stored as
quarterly-regional-sales-2026.improv loses ":q quit" at 100 columns.
Two smaller faults in the same three lines. The padding was `left.len()` --
BYTES -- and the title's own format string spells a "·", two bytes and one cell,
so every document's right segment sat one cell in from the edge it is anchored
to, before any non-ASCII name made it worse. And there was no truncation at all,
so the failure was a mid-token clip of "?:" rather than a graceful drop.
`Keeps` makes the priority a per-call-site decision rather than a global rule,
because it genuinely differs: the title bar keeps the RIGHT (the hatch outranks
the name), while `minibuffer_line` and `draw_status` keep the LEFT, which is what
they already did -- there the left is the user's own typing plus the cursor, or
the status message, and right priority would let a long message eat the text
being edited. Truncation goes through ui::text::truncate, so widths are display
cells throughout and an overflow is an ellipsis.
Five tests, each rendered through a real TestBackend because the loss happens in
ratatui's clip and a helper's return value cannot see it. Three faults injected,
each caught by a distinct test; the reasoning and the injections are in
context/test-evidence/tui.md and context/code/tui.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
`context/code/tui.md` claimed `mode_style`'s default-chrome arm spells out
fourteen `AppMode` variants. It spells thirteen — Normal, RecordsNormal,
FormulaEdit, FormulaPanel, CategoryPanel, CategoryAdd, ItemAdd, ViewPanel,
ImportWizard, ExportPrompt, SavePrompt, Help, Quit — counted off `src/draw.rs`
rather than taken on report.
The match itself is sound and exhaustive; only the sentence about it was wrong.
The arithmetic that makes the corrected count checkable now sits beside it:
13 + Editing/RecordsEditing + CommandMode + TileSelect = 17, which is
`AppMode`'s variant count.
Nothing in this repository checks a count written in prose, which is exactly why
it survived — the same failure mode gotcha 38's derivation-count correction and
gotcha 44's stale gap list record.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
The repo map promises of `AppMode` that "add a variant and the compiler names
every one you must fill in". That was true of the five enum methods and false
of `src/draw.rs::mode_style`, which swept eleven variants into a `_ =>`
wildcard — so `SavePrompt` took `ExportPrompt`'s DarkGray by accident rather
than by decision, and the next person adding a variant would have trusted the
sentence.
Of the ticket's two options, the first: `mode_style` is now exhaustive and
stays in `draw.rs`, because `Style` is a ratatui type (gotcha 26). The second —
a semantic colour method on `AppMode` in `improvise-protocol`, the
`AxisColor`/`MinibufColor` shape — is NOT taken here, and deliberately: the
ticket says to decide it together with improvise-k099, which asks the same
question of `hint_text`/`label` and is not in this unit. The repo map's promise
needed exhaustiveness; the semantic enum is a separate, larger answer and stays
open.
No colour changes. The fourteen default-chrome variants are spelled out with
the DarkGray they already had, so this is the wildcard becoming a list.
The guard is the compiler, not a test — a test enumerating today's variants
would not fail when a fifteenth is added. Verified by deleting the
`AppMode::Help` arm:
error[E0004]: non-exhaustive patterns: `&AppMode::Help` not covered
Also removes the doc comment calling the wildcard "a known defect", which this
commit makes false, and updates `context/code/tui.md`, which said the same.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
improvise-9c8c. The two files this unit owns, written before any comment is
capped so nothing is capped without somewhere to land.
context/code/tui.md carries the grid widget's module doc whole (it is the
best-written statement of the no-&Model/no-&View rule in the repo, better
than the repo map's own), the two semantic-to-terminal colour mappings and
why mode_style is NOT that shape (improvise-zx8t), the wizard's
set_bottom_line and bytes-vs-cells arguments, and the per-frame cache
building that is the temporary half of improvise-35e.
context/test-evidence/tui.md carries the injected-fault records, keyed by
test name per design-principles §7 — including THE_MEMBERSHIP_CORRECT_SPANS,
whose assert_ne! is a live tripwire that starts failing the day
improvise-gd83 is fixed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>