main
18 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
c8d715346f |
fix(web): a narrow window must not widen the document (improvise-tg27)
The report was "a narrow window clips grid columns without usable horizontal
scroll", and the ticket suspected a flex `min-width: auto` trap swallowing
`#grid`'s overflow. It was not that: `#grid` has `overflow: auto`, which
already zeroes a flex item's automatic minimum. What could not shrink was the
app bar.
Measured at
|
||
|
|
1f222e6215 |
style: centre the view chip in its reserve, as the mode pill already is
Found by looking at the screenshots rather than by a test, which is the only instrument that reaches this: `#view-name` is now a fixed 16ch, and "Plan" left-aligned in it read as a pill that had failed to shrink rather than as a deliberately reserved slot. Centring is what makes the reserve legible as a reserve. No test moves -- the header spec measures boxes, and text alignment inside a fixed box changes none of them. That is the honest scope of the guard and not a gap in it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
d750b433b8 |
fix: the app bar is reserved, not fitted, and a message is a toast (improvise-n6t8)
The rule is the deliverable: nothing a user expects to be fixed may move.
Four children of <header> were sized by their own content and all four
changed on the SAME event, so the bar reflowed on a mode change with no
message at all: #mode (mode_label, 4-14 chars), #hint (hint_line, one
line per mode), #doc-name (gains and loses " [+]") and #view-name, which
had :empty { display: none } and so appeared out of nothing.
Every child that changes with a keystroke now has an allocated width, and
#hint is the single elastic child, so nothing is positioned by it. The
mode pill's reserve is a FLOOR and deliberately not a cap: a cap absorbs
an over-long label inside a box that never grows, which is a reserve that
hides its own failure. AppMode's exhaustive-match methods make the
compiler demand mode_label for a new variant; nothing checked the CSS
still fit it. harness/tests/header.spec.js is that check.
The spec found what the ticket did not predict: the compounding one was
not the pill. #status was flex: 0 1 auto beside a flex: 1 1 auto hint, so
a longer hint took the shrink out of #status, which WRAPPED to two and
three lines and grew the header by a row -- 52.5px to 71.0px. Reserving
the pill alone would have left that untouched.
ViewState::status_msg is now a toast in a position: fixed dock anchored at
its bottom, so it takes part in no layout and stacks ABOVE the minibuffer:
the prompt is neither covered nor displaced. composeStatus is gone -- the
two facts go to two elements, so a repaint reaches onModel and cannot
address onShell at all. That is gotcha 46's rule made structural rather
than kept by a join's convention; its unit test was replaced, not deleted.
Dismissal keys off message IDENTITY, not a timer. status_msg is STATE and
repaint() re-offers it on every revision, so a setTimeout alone resurrects
the toast on the next keypress. Cost stated rather than hidden: two
identical messages in a row show once, because nothing carries a sequence
and the alternative reading of an ambiguous offer is exactly the bug.
Nine of seventeen modes are reachable in a browser and the spec names them
individually; three injected faults are recorded in the evidence file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
|
||
|
|
a913390322 |
feat: the page bar becomes a control, and [/] reach the modes about the document
Three tickets on one subject: paging.
improvise-rw6l — clicking a page chip opens a menu of that axis's items, with
a down-arrow saying so. Almost nothing had to be derived: `PageAxis::items` was
already projected and `ModelCommand::SetPageSelection` was already on the wire.
The one gap was that `RenderPageAxis` dropped the names, keeping the position
and count improvise-p1ym needed for `[`/`]`. `items` is relayed INSIDE `cycle`
rather than beside it, so there is one predicate and not two: a Filter axis has
no cycle, therefore no arrow, no menu, no items, and composes nothing if a click
reaches it — all four off `PageAxis::cycle`, the one home of gotcha 27's split.
improvise-771e — the browser tile bar said "Quarter Pag". `short_label` is three
characters so the TUI aligns in a fixed-width terminal (gotcha 26); a browser has
no column to fit. `render.rs` asks `plain_label` instead; `short_label` is
untouched. Note what pinned the truncation was `harness/tests/drill.spec.js`,
not `tests/grid_frame_agreement.rs`, which does not mention the tile bar.
improvise-wiew — `[`/`]` move from Normal onto a new `document_keymap` between
the global and the modes whose subject IS the document (Normal, the three
panels, TileSelect; RecordsNormal through Normal). Help and the import wizard
take the global directly. Per the user's ruling: an overlay has a subject of its
own, and these are the first shared keys that MUTATE the document — under the
help overlay that is an invisible change to the grid it is covering.
Boundary crossings to flag, both outside the brief's stated write scope and
neither a wire change:
- `resolve.rs` gains `Click::PageItem` and `lib.rs` a `click_page_item`
binding. Composing the command anywhere else would put a derivation in the
page, which design-principles §5 forbids; `Command`/`ModelCommand` are
untouched.
- `improvise-command/src/command/keymap.rs` gains three tests. The two
existing guards were the mechanism that forced the wiew argument, so they
had to be answered where they live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
|
||
|
|
926396352f |
fix: seat the build stamp in #120's redesigned bar, and stop it faking a ticket id
Three things the merge with #120 (ui/shell-polish) actually required, none of which a clean auto-merge could have told us. MOVED, not preserved. #120 left <header>'s markup alone but gave every child explicit styling and a stated reading order -- "what this is, which document, which view, which mode, what keys, what the peer is doing". A build identifier is "what this is", so the span moves from after #status to beside <h1>. Keeping it at the tail was loyalty to my own first draft: it read as metadata trailing #status, and with #hint's `flex: 1 1 auto` fighting for the slack a 40-char commit SHA there is the thing that crowds the bar. STYLED, because unstyled is illegible-by-default in a design system. It takes `var(--dim)` -- the same token #hint and #status use, so it tracks both themes by construction rather than by a hand-picked colour -- at 0.72rem, and clamps to 14ch. Measured under Chromium on the merged tree: a tag renders whole (scrollWidth == clientWidth, 69px), a 40-char SHA truncates to a 13-character prefix with an ellipsis, and `header` does not overflow in either case (scrollWidth == clientWidth == 1280). Contrast of rgb(107,103,95) on rgb(244,241,234) is ~5.0:1, above AA for small text. Selecting the text still copies the whole SHA -- text-overflow is a rendering clamp, not a truncation. NOT AN ID. `improvise-web.tar.gz` puts the token `improvise-web` in the tree, and AGENTS.md's audit matches `improvise-[a-z0-9]{3,8}` -- so the artifact name resolved to no ticket and read exactly like a dead id, which AGENTS.md calls worse than one. `improvise-installer.sh` escapes only by being ten characters, which is luck rather than a rule, so the exclusion list gains `web` and the paragraph gains the general case. RELEASING.md (which arrived in this same merge, from improvise-gx5d) listed what a tag publishes and was made wrong by omission; it now names the web capture and says plainly that it is not what Pages is serving. Verified on the merged tree: 1631 cargo tests over 37 binaries, node --test 47 -> 50 (so #120's three new shell tests survived the merge rather than being silently dropped), 42 Playwright tests green under Chromium, and the stamp gate re-proved by injecting the missed substitution again -- a header rewrite does not carry the old fault injection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
52f615d14d |
fix(web): an error cell must not lose its colour to the dimming or the selection
Found by reviewing the restyling before pushing it, and demonstrated before it was fixed. `td.error` and `td:not(.editable)` used to set DIFFERENT properties — colour and opacity — so both applied and a dimmed error cell was a dimmed red. The restyling made the dimming a colour too, at the same specificity and below `td.error` in the file, so an error in an aggregate cell rendered plain grey: rgb(107, 103, 95) where the error colour is rgb(168, 50, 45). `td.selected` and `td.typing` did the same by setting `color: var(--fg)`. `#grid td.error` is the only place on this page where colour carries MEANING rather than emphasis, and it is the one a reader takes at face value: a circular reference or a `mixed` aggregate reading as an ordinary number is worse than it reading as nothing. So the dimming moves ABOVE the semantic colours, and a selection is a ring and a tint and never a colour. That is the class of defect a stylesheet has and a program does not — two rules that were independent become one property, and nothing in any language fails. Also: an expanded notice now caps at 40vh and scrolls. `main` is free to shrink to nothing, so on a short viewport opening it could have pushed the grid out entirely. The test is aimed at the STYLESHEET rather than at a document, because the question is which rule wins and a fixture per combination would be three documents testing three documents. It appends a <td> carrying each combination to the real page's real table and compares two controls taken the same way, rather than pinning absolute colours. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
8ad5c0806d |
feat(web): dismissable notice, a raised minibuffer, and a styled shell
Three things the user reported after looking at the deployed page, in the order they cost a visitor something. The deployment notice was ten dense lines of configuration reference — ?doc=, ?seed=, ?ws=, wss:// and reverse proxies — above the tile bar, the category tree and the grid on every visit: about 230px of an 820px window. It is now a <details>, so the collapsing is the BROWSER's (keyboard operable, and correct with app.js absent or still loading) and the only thing the page adds is remembering the answer. It starts COLLAPSED, and collapsing withholds nothing: the one fact a first visitor needs — the document lives in this tab, is saved to this browser's private storage and never leaves the machine — is the <summary>, which never collapses. What a stored value means is a decision and lives in shell.js; touching localStorage is a browser API and stays in app.js, wrapped, with every value that is not one of the two spellings read as "unanswered". The minibuffer was a 14px line in the page's bottom-left corner, up to 800px from the cell being typed into. It is now a raised bar over the grid. The CONTENT is untouched — RenderMinibuffer is built from `state.view.mode.minibuffer()`, the same semantic value the TUI draws — so this is position, weight and contrast, and there is no second derivation of what a prompt says. MinibufColor still resolves here exactly as draw.rs::minibuf_fg resolves it for the terminal. style.css stops being "legibility only": layered surfaces, one accent, a filled badge for the mode, chips for the two axis strips, one selection treatment, tabular numerals in the grid, and a focus ring on the one keyboard surface the page has. The palette is the landing page's, so the two read as one product. Asserted (harness/tests/contract.spec.js, Chromium): the notice starts collapsed and remembers, in BOTH directions across a reload; it opens from the keyboard, which the page's own keydown handler must not claim; the minibuffer is inside the viewport, OVERLAPS #grid — the assertion a static bottom bar cannot satisfy — and is larger than body text. Each was proved discriminating by injecting the fault and watching it fail. NOT asserted, and named as such in context/code/clients.md: colour, contrast, spacing, weight, the grouped and multi-level header DOM, either theme, and the localStorage-throws path. Those are argued and read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
a93d668e5e |
feat(browser): paint the page bar and the totals strip, and delegate the pointer
improvise-p7lh: `GridCache::page_coords` and `GridCache::totals` were projected to every session and folded correctly by every client, and NO front end read either. The shipped front page — `web/demo.improv` is `Quarter: page, Q1` — drew a grid filtered to Q1 with nothing saying so, and `[`/`]` resolve locally off that same `page_coords`, so a user could page through quarters watching every number change with no way to tell which one they were looking at. `RenderModel` gains `page_coords` and `totals`, fed straight off `state.grid`. The Page/Filter split is asked of `PageAxis::cycle` rather than re-derived from `items.is_some()`, which disagrees with it on an empty item list (gotcha 27). The totals are RELAYED, never summed in the client: the sum runs over every row including ones a viewport scrolled past (gotcha 44). `web/tests/render_surface.test.js` gains the direction that would have caught this when improvise-n10 added the fields. The existing guard is closed under what `RenderModel` chose to expose, so a cache field that never reaches it is invisible to it; the new one asks that every `pub` field of every widget cache is read somewhere the browser runs, or is named as debt with a reason. `grid.col_widths` is the one entry, pointing at improvise-pcje. improvise-b8ot: the five clickable surfaces moved from one listener per node, reallocated inside the render loops on every repaint, to one handler per stable delegation root — the user's ruling. The operands are carried on the node as data attributes and read back by a pure `cellAction`/`indexAction` in `web/shell.js`, so the handler is a LOOKUP rather than a second derivation of which data row was clicked: `gridPlan` counts data rows because a group header is not one, and walking the table back up would disagree the moment one appears. The editor's own keydown/blur stay direct — one transient node, one at a time, and `blur` does not bubble. All four documented behaviours a naive delegation loses are preserved and each is now asserted in `harness/tests/contract.spec.js`: mousedown rather than click (asserted by dispatching mousedown ALONE), `preventDefault` and only over a cell, the conditionally skipped repaint (asserted by the editor's survival), and closing a live editor before composing the click. Gates: 1598 passed / 0 failed / 7 ignored over 37 binaries (baseline 1595); node --test 47 over 6 files (baseline 36 over 5); Playwright 47 passed / 4 skipped (baseline 43 / 4); clippy, fmt and --check-evidence clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
628f43121d |
feat(web): the title bar is painted, a click survives an editor, and the twin door is gone
improvise-5jna, improvise-0x9u, improvise-seik — three browser-page defects with one shape: a thing that looks done from every check except running it. improvise-5jna. `doc_name`/`doc_dirty`/`doc_stored_as` were projected by z754 and painted by nothing. `render_surface.test.js` could not see it — it is a SUBSTRING SEARCH over the page's source, so a field named by a function nothing calls reads as painted, which is also how `model.minibuffer` shipped dead. So the guard is a harness assertion that loads the page and reads `#doc-name`, proved by writing it first (`element(s) not found`). The joining is `shell.js::documentTitle`, the TUI's own `name (stored_as) [+]` minus the chrome; the `[+]` is a node test, not a harness one, because this deployment persists on every accepted batch and clears the flag in the same round trip, so the marker is transient. UNPAINTED is now empty. improvise-0x9u. From an EDITING state a cell click did nothing at all: the mousedown moved focus, the input's blur ran `close`, `close` repainted, `replaceChildren` destroyed the `<td>` the mousedown landed on, and no `click` was ever dispatched. The old spec pressed Escape between the two clicks, so it only ever clicked from a non-editing page — this repo's characteristic defect. Worth recording: moving to `mousedown` is NECESSARY AND NOT SUFFICIENT, and the first attempt failed the same assertion with the same symptom. Removing a focused `<input>` fires `blur` SYNCHRONOUSLY, so `close` rebuilt the table from inside itself and detached the `<td>` again. `close` therefore no longer repaints, the `blur` arm checks `closed` first, and the handler `preventDefault()`s so no focus change happens at all — otherwise the default action focuses `#grid` after `input.focus()` and blurs the new editor. `close` restores the DRAWN text, since nothing repaints behind it any more. improvise-seik. `Client::viewport` was a wasm-bindgen door with no caller, kept for a reason `web/app.js` falsified in the same wave. It is one letter from `set_viewport` and fails SILENTLY — identical frame, forgotten window, every cursor key an upstream black hole. Deleted, along with the exported `init()` free function that duplicated `new Client()`. The guard is not the deletion: `wasm_surface.test.js` now reads the generated `.d.ts` and requires every declared method to be one `app.js` calls or to be named in `UNCALLED` with its reason. Proved by injecting a spare `static viewport(...)` into the built `.d.ts` and watching it fail by name. Tests: 1405 Rust (unchanged), 27 node + 4 bundle (was 26 + 3), 24 chromium harness (was 23). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
d1ce2ecbc3 |
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
|
||
|
|
6f9cd6cad5 |
fix(web): the tile bar, the view panel and the formula panel are drawn
A user reported "I can't expand categories, and the Tile + View UIs don't
work". Driving the assembled site under Chromium separates three different
answers, and the useful finding is that two of the three were missing only
their last layer.
Category expand already worked, by keyboard: Shift-C, j, Enter folds
Command::ToggleCatExpand client-side and the items appear. What does not work
is the mouse, because categoryList builds plain <li>s with no listener — the
whole page's pointer model, filed as improvise-7ybz rather than guessed at.
The tile bar and the view panel had the cache, the keymap, the resolver and
the host round trip, and no DOM. Before this commit the browser held five
TileBarCache entries and both ViewListCache views; Shift-T entered TileSelect,
ArrowRight moved tile_cat_idx, and r/c/p really did move a category between
axes — with nothing on screen to aim them at. RenderModel had no field for
either and index.html had no element. The formula panel was the third of
Panel::ALL in the same state, and is fixed by the same mechanism rather than
left arbitrarily behind.
So RenderModel grows `tiles`, `views` and `formulas`. The two panels follow
the help overlay's rule — an empty list is what says a widget is closed, so
the page asks no question about the mode — and the tile bar is drawn always
with only its *selection* conditional, exactly as src/ui/tile_bar.rs makes it
conditional on AppMode::TileSelect.
The view panel's axis line would have been a second derivation, so
ViewContent::axis_summary moves to improvise_protocol::viewmodel as
view_axis_summary and the TUI's own panel now calls it: one home, both ends
(design-principles §5).
Tests: harness/tests/widgets.spec.js, proved by running it against the
unfixed bundle, where the four new assertions fail on zero elements and the
category one passes — the control that says the keyboard really is being
driven. Two draft assertions were wrong and both corrections are recorded:
Axis::short_label is three columns ("Quarter Pag"), and switching views
leaves the panel open as the TUI does.
Not fixed, filed: improvise-7ybz (no pointer affordance anywhere on the page),
improvise-wsaw (app.js has never read category_panel_open, so #categories is
drawn unconditionally and is now inconsistent with the two panels added here).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
301a74821a |
feat(protocol): the grid's frame on the wire, as AxisFrame
`GridCache::row_labels`/`col_labels` were `Vec<String>` built by `GridLayout::row_label`, which slash-joins a multi-level tuple — the exact rendering `two_row_categories_produce_cross_product_labels` asserts must not appear. So the protocol carried a form of the labels the TUI itself considered wrong, and `improvise-wasm-client/src/render.rs` drew precisely that form: the browser showed `Food/Alice` where the TUI showed two header columns, with no groups and no collapse indicators (gotcha 44). Both fields become one `AxisFrame` per axis, carrying the five facts gotcha 44 enumerates: the categories (hence the header levels), the label *tuples* and the inline group headers, the group a position sits in, and each group's collapse state. `record_col` stays off — session-local, keys pending drill edits, correct to keep off. It is one type and not a `row_levels` beside `row_labels`, because two answers to "what are the labels" is the disease (improvise-edp). The rules that used to be written twice are methods on it and are now called from both front ends: `shows_level` (repeat suppression, improvise-jjx's fix), `group_of`, `body_from`, and `header_widths` — the row-header width rule, which existed twice already, inline in `render_grid` and again in `viewport::compute_row_header_width` (improvise-1125). `GridWidget` loses its `&View`: nothing in it reads the view any more. Evidence. `the_whole_frame_is_unchanged` — the exact-buffer characterization over a grid with row groups, column groups, a page axis, multi-level headers and a total row — passes byte for byte, so the TUI is unchanged. The new `tests/grid_frame_agreement.rs` drives the widget into a ratatui buffer and the browser client through a `Snapshot` of the *same* cache, and compares what each draws; re-introducing the slash-join on either side was injected and watched to fail before the test was kept. `tests/web_demo_model.rs::the_web_demo_has_one_category_per_visible_axis` is deleted: it guarded the gap this closes, and a consolidation that keeps its compatibility test is unfinished. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
59cc6720b5 |
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 |
||
|
|
e13635638c |
feat(browser): the browser reads the TUI's keymap, and i opens an editor
Follows the keymap move. `keys.rs`'s hand-written per-mode classifier is gone; `resolve` now looks the key up in `Keymaps::default_keymaps()` — the same table the terminal dispatches against — and what is left here is the narrow residue: turning a bound command NAME into a `Command`, for the subset a client holding no `CmdContext` can compose. That is a better seam than the one it replaces. The drift risk was "which key does what": 170 bindings, no direct test, invisible when wrong. It is now "which command names can I execute", which is a list — and `the_normal_mode_keys_that_still_need_the_host_are_these` becomes `the_command_names_that_still_need_the_host_are_these` to match, so a rebinding no longer invalidates it and a new binding for an already-executable command reaches the browser with no change here. Several gaps close by sharing rather than by code. Search-mode typing resolves locally for the first time (the table binds `append-char ["search"]`, and "search" is `ViewState::search_query` — gotcha 11's pair is mirrored and nothing else is). `Esc` resolves as the `Binding::Sequence` it is, all-or-nothing. A modified character in a text-entry mode now types, because the `AnyChar` fallback being reached for is the host's own — the old "unmodified only" rule was a hedge against a table this client no longer guesses at. And a key nothing binds is `Ignored` rather than forwarded into `Host::handle`'s empty vec. What is newly executable, and so what a user gets back: `edit-or-drill` (`i`/`a`), `commit-cell-edit`/`commit-and-advance-right` (Enter/Tab in `Editing`), `enter-mode` (`?`/F1, Esc), `enter-tile-select` (`T`), `toggle-panel-visibility` (Ctrl-F/C/V), `toggle-panel-and-focus` (F/C/V) and `transpose` (`t`) — four of which an independent browser smoke test had just found inert. `i` on an **aggregate** does not go upstream. Every upstream key is a black hole, and a keypress that visibly does nothing is the worst of the three answers, so it answers with a `SetStatus` naming drill. A view-half command is folded into the asker's own mirror and broadcast to nobody, so that message reaches this user and no one else. The write itself is not composed twice: the keyboard commit calls `socket::typed_cell_command`, the function click-to-edit already used, so Enter and a click cannot disagree about what `""` or `"42"` means. ## `CellDisplay::edit_text`, found by lockstep `tests/lockstep.rs` drove `i` through the host and the client over one grid and they disagreed by a comma: the host seeds its edit buffer from `App::display_value_at_cursor` (the stored `1200`) while the client had only `CellDisplay::text`, which has been through `format_f64` (`1,200`). Committing that stores the *string* `"1,200"` and the number is gone — display rounding fed back into the model, which design-principles §4 forbids outright. So the projection is enriched rather than the client taught to un-format: `CellDisplay::edit_text` is `display_value_of` on the wire, and it repairs **click**-to-edit too, which had been seeding its `<input>` from the drawn text all along. ## Making the edit visible `RenderModel::editing` carries what the user has typed so far, or `None` when no edit is open. The decision — is this an editing mode, and which buffer does it type into — is made in Rust; the page draws the string in the selected cell. Without it the mode indicator would read INSERT while every keystroke vanished into a buffer nobody painted. `web/app.js` and `web/style.css` change accordingly, and are **unrun**: there is no browser here (gotcha 48). `node --check` only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH |
||
|
|
188f3e94ef |
ci(pages): build the wasm bundle and publish web/ to GitHub Pages
improvise-d31's finished artifact depends on improvise-9x6 (the worker-hosted server) through improvise-djm. The *pipeline* does not, and building it now is what stops the delivery half from being a mystery when the worker lands: the swap from websocket transport to worker transport then changes what is bundled, not how it is built and shipped. Two jobs in ci.yml rather than a second workflow. `pages-build` runs on every push and pull request, so a change that breaks `web/build.sh` or the bundle fails on the PR that made it. `pages-deploy` runs only on a push to main and `needs: [test, wasm, pages-build]`, so nothing that failed `cargo test` or the existing wasm32 gate can reach the published site — the deploy depends on those checks instead of repeating them. Deliberately not path-filtered. The bundle is improvise-wasm-client plus improvise-protocol, improvise-core and improvise-formula, so a filter meaning "skip when the bundle cannot have changed" would have to name most of the workspace, and getting it wrong leaves a stale site with nothing saying so. wasm-pack is a pinned release tarball, v0.15.0 — the version `nix develop` now supplies — so the local run that produced 346 KB of wasm (120 KB gzipped) is evidence about the CI run. The devShell was missing wasm-pack entirely: the `wasm` job only runs `cargo check`, which does not need it, so nothing had ever required the one tool that turns a passing check into a loadable page. What this cannot do yet, said on the page itself rather than routed around: web/app.js is a thin client that holds no model, so a copy served from a static host paints an empty grid until it is pointed at an improvise-ws-server with ?ws=. The notice is static HTML, not something app.js decides, because it is true on every load and a page that cannot reach a server cannot be trusted to explain why. README says the same and names the deployed URL. Verified: cargo test --workspace 1134 passed / 0 failed, clippy clean, fmt clean, actionlint clean, ./web/build.sh succeeds and the assemble step produces a 388 KB site whose every reference resolves inside it. NOT verified: the workflow has never run — GitHub Pages is not enabled on the repository (GET /repos/fiddlerwoaroof/improvise/pages is 404), which is improvise-3gy. The deploy job asks configure-pages to enable it; if the token may not, that step is where the run fails and the fix is the repository setting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH |
||
|
|
c72832f542 |
feat(wasm-client,web): the DOM renderer for the browser grid (improvise-cr3)
The last blocker of improvise-1ey. Mostly assembly: `Client` already had the fold, the key classifier and `set_cell`, `CellDisplay::id` already said what a cell is (ADR 002), and `flatten_cat_tree` already owned the category panel's flattening. What was missing was the three joints between them. `render.rs` is the one that matters. The DOM half is JavaScript and JavaScript here is untested, so every question with a *silent* wrong answer is answered in Rust under test and crosses as decided data: `editable` is a fact about the `CellId` variant (Record only — an Aggregate is a drill target and `Drill` is not on the wire yet, and `None` is "no projected cell", not "aggregate"), the selection is placed by subtracting the scroll offset because `GridCache` is viewport-indexed while the cursor is not, and the category panel is `flatten_cat_tree` rather than a second walk of the cache. `socket.rs` frames what `improvise-ws-server` actually speaks, which is not what `wire.rs` speaks: it has `Viewport` (the handshake — a snapshot built at extent zero is an empty grid), it batches `Commands`, and it has no `Key` variant. The two enums are duplicated because a native crate cannot be a wasm dependency; `improvise-ws-server` is a dev-dependency so the compatibility is checked through its own types, and a companion test proves the check is discriminating on the variant names that would drift. `web/` is the shell, and it is thin on purpose: four string calls in and out, no protocol knowledge, no framework. Gates: 1047 passing (was 1031), clippy and fmt clean, and `cargo check -p improvise-wasm-client --target wasm32-unknown-unknown` clean. Verified end to end headlessly — the wasm-pack artifact driven from node over a real websocket to a real `improvise-ws-server`: 1,200 read, "98765" typed, "98,765" projected back. The DOM painting itself is unverified; there is no headless browser here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH |