refused_here (from #286) ran before commit() consulted the capture and,
for the commit keys, judged the cell under the cursor. After a reframe
left the cursor on an aggregate, a captured editor's Enter was declined
on every retry. The commit-key guard now applies only to an editor that
captured nothing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
#274's capture read the cell through the cursor's offsets and stuck, so a
scrolling Enter captured the old window's cell even with every reply
delivered (improvise-7mro's horizontal wrap: Jan/Revenue for Feb/Revenue).
#286's cell_at_cursor reads through projected_at, so the capture waits
until the host projects the cell. Taken by merge, not by port: the matrix
test's held cases need #282/#277's waits too.
Resolutions: committable_cell_key takes #286's uneditable(); the two unit
CellId::Coordinate patterns become struct patterns; evidence is appended
on both sides.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
An awaited seed waits for a cell the host projected (id present), never the
blank a relabel leaves between a frame and its cells. The advance's reopen
keeps #282's shape: on this base the welded clear-buffer empties it anyway.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
cell_at_cursor and seed_at read through projected_at, and cell_at (the
cursor-offset read) is gone. A cell of the grid outside the window the
cache holds is unprojected: i and an advance open an editor awaiting its
seed, which the fold settles once the host projects the cell, and a
commit, x, yy or p on it waits out loud rather than composing anything.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
Carries #268 onto #258's new head, which now sits on #247's re-merge
(12f206fb) and so on #239's spanned_values and &Expansion signatures.
Code auto-merged: GridLayout::evaluator / GridEval stays the only public
path to a FoldMemo, and prune_empty still shares one memo. The one
conflict was benches/grid_projection.rs's main(): both new sections kept,
the pruned block kept directly after the folded chain it calls "the same".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
Carries #258 onto #247's re-merged head, which now has #239's
spanned_values/spanned_positions and the &Expansion fold signatures.
Code auto-merged: FoldMemo::new(agg, covers) still owns both, and
fold_stored reads through spanned_values. The only conflicts were the
append-at-EOF evidence files (command.md, core.md); both sides kept.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
Answers the ChatGPT Codex P2 on ba3d2875. Grids looked the view up through
resolve_view, whose read-side fallback to the default accepted SetCell
{ view: "Nope" } wherever Default drew the record. The view is now looked
up strictly, and a stored record is gated too, so a missing view is
RecordGone at the basis check for every record.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
Both PRs added ClientState::notify, so the merge failed with E0592. #266's
is kept: it queues the sentence through fold_outgoing, which runs
settle_status, so the owner is still recorded. #279's direct-write notify
is deleted.
#266's rebase() rebuilt the view without settle_status, so a relay that
moved the view off the editor left the message owned, and an equal
editor reopened afterwards took it back. rebase() now settles it, pinned
by a_rebase_off_the_editor_ends_the_message_standing (red before).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
From PR #280's review.
improvise-s2gb: every_silent_binding_is_on_the_list walked Normal and
search mode only, so a black hole bound in FormulaPanel (the reviewer's
Z -> commit-export) passed the whole suite. The modes are now derived:
reachable_modes starts from both halves of Normal (the grid, not a key,
picks RecordsNormal, e5pf) and closes over the modes a pressed binding
lands in. Doors `declined` shuts (the three stranding prompts, the
import wizard, records editing, search now that `/` declines) fall out
with no list to maintain. A floor assertion names seven modes the walk
must reach, so it cannot pass by shrinking. furnished_to_type seeds the
mode's own minibuffer as well as the search query.
`heard` counts a SetStatus as a decline only when it says something: a
resolver answering SetStatus("") left the user seeing nothing and passed
this guard, caught only incidentally by the Ctrl-Q tests.
Merges #275's new head (improvise-4k1j) and fixes the fmt gate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
Enter or Tab on a typed-into editor over a fold or a formula's cell went
Upstream, which nothing answers. refused_here declines the step, which
takes the whole binding with it, exactly as the TUI's failed
committable_cell_key does: status said, buffer and mode kept.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
App::grids() memoises the named view's layout for as long as it borrows the
App: once for a whole frame check, once in model_effects. A frame of n writes
to another view's empty records now derives 1 + 2n grids instead of about 5n.
Empty-record ClearCell on 18k cells with a formula, M4 Max: 1.44s -> 0.66s.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
declined_at indexed the cache with the cursor's offsets, which the
client's own outgoing frame moves before the host's projection arrives.
A wrapping Down read the old window's top row and closed an editor on
Revenue/Feb with Profit's refusal. projected_at reads against
AxisFrame::first, so a stale cache answers None instead.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
renamed_away() is a Step::Declined, like still_waiting(), so the Enter
binding's welded clear-buffer no longer empties the editor the user must
Esc away from. Shared by the value and rename paths. Red first:
a_retry_whose_cell_is_gone_keeps_the_typed_text failed with Some("").
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
improvise-4k1j, from PR #275's review. hidden_row_item refused on a fold
only when the NEAREST group header was collapsed. Under two row
categories that header can be the inner one's: with Region's Coast
collapsed and Product's Kit expanded, the client composed
HideItem{Region, "Coast"} and a twin host wrote a group name into the
document, where the host refuses.
The frame cannot answer this on gh/main: FrameGroup names no level and
an inner GroupEnd reads as an outer one. So FrameEntry::Item now carries
the layout's own DataItem::collapsed flag (a fold, or under one, at any
level) and AxisFrame::folds reads it, the same bit
GridLayout::row_folds_a_group reads. FrameEntry::item builds an
unfolded one, as AxisEntry::item does.
hiding_agrees_with_the_host_on_every_row_of_nested_groups presses H on
all 25 rows of four nested grouped grids, in two windows each. It was
red on the nearest-header check at rows 4 and 5, as the review found.
Also fixes the fmt gate the review found red: fmt-workspaces.sh is a
check, not a formatter, and its diff output was misread as success.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
8phw: when A's Enter crossed B's HideItem, the LayoutMoved refusal replayed
the editor over South and settle_opened recaptured South/Cost, so the
retry wrote there. #251's Opened::Retry/Gone now covers values:
written_to() recognises SetCell/ClearCell as well as a rename, and Retry
and Gone carry the whole OpenedTarget, which the recapture must match.
A Gone value editor declines with RECORD_RENAMED_AWAY. seen stays
rename-only.
Codex P2 on #274: commit picked value/rename/create from the cell under
the cursor before it consulted the capture, so a reframe that left no
cell there sent a captured editor's Enter upstream. commit now
dispatches on opened.target(); only an uncaptured editor is dispatched by
the cursor. A records grid's measure cell is not captured as a value,
since the browser still declines it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
The two generator strategies re-parsed the grammar inside their
prop_map, once per generated case: the same defect improvise-8shu fixed
in improvise-io. A OnceLock takes the lib test binary from ~0.9s to
~0.06s on the same machine and load.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
`fold_frame` wrote the rejection sentence straight into `view.status_msg`,
so it was in neither `settled` nor `in_flight`, and the xd3q rebase erased
it whenever a relay queued behind a frame still in flight: an in-flight
frame's own `SetStatus` was replayed over it.
`ClientState::notify` queues the sentence as a view-only entry at the tail
of `in_flight`, where `fold_outgoing` puts a frame the host never answers.
Frames sent before it replay first, relays queue ahead of it, frames sent
after it may replace it, and it settles when everything ahead is answered.
Writing it into `settled` too was tried as a rival and fails the same way.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
OpenedCoordinate becomes #274's OpenedCell, now carrying #251's seen
(filled for a rename only) inside Opened::Target. Retry/Gone keep #251's
rename-only semantics in this commit; the value-cell retry (improvise-8phw)
follows. #221's superseded "a retry after a refusal is a force" paragraph
is dropped in favour of #251's rule.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
reopened_at opens provisionally where the landing cell is not projected
yet; ClientState::settle_declined_editor closes an untouched editor once
the host projects that cell as one i declines. Both ask one
resolve::declined_at. A touched editor is kept.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
`i`/Enter opened an editor on a NonItem cell and the refusal came only at
commit, under a minibuffer that hides the status. `EditOrDrill` now asks
`cursor_cell_id` after the formula check, as the browser already does.
Three rivals each passed the whole workspace before this: NonItem
writable in `value_cell_key`, a Record in `committable_cell_key`, and
`None` from the browser's open-editor arm. Each now fails its own test.
The commit guard is reached through `:enter-mode editing`, which opens
an editor past `EditOrDrill`.
command.md also records improvise-4daa's argument beside gk51's.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
`first_non_item` asked `effective_item_names(cat).contains(name)` per
coordinate per projected cell, cloning every item name each time, and the
TUI projects the whole grid every frame. `is_item` asks `contains_key`,
plus the formula targets for a measure category, and is pinned equal to
the list it replaces. #252's write path already goes through
`first_non_item`, so both callers get it.
Measured on an Apple M4 Max, interleaved 5x, minimum per build: pivot
18k cells 193 ms -> 136 ms (base 131 ms); records with a 2000-item label
145 ms -> 2.8 ms (base 1.9 ms).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
improvise-oyaa. every_silent_binding_is_on_the_list presses every
binding the Normal and search-mode keymaps reach (parents unshadowed,
prefixes expanded) on the furnished fixture, and requires each silent
one to be on NEEDS_THE_HOST. The two earlier guards could not see an
unlisted binding that no help row names: dropping force-quit (Ctrl-Q)
from the list failed nothing. Now it fails this test.
every_binding_reaches_parents_and_prefixes checks the walk itself
against a hand-built keymap, because no silent binding in today's maps
sits in a parent or behind a prefix, so a walk that skipped either
passed the first test unchanged.
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
Enter in Editing/RecordsEditing is [commit-cell-edit, clear-buffer edit].
The "still waiting" answer came back from composed() as an ordinary
Local step, so the sequence ran the welded clear after it and emptied
the editor it had left open, on the value path and on #221's rename
path alike.
The two cell commits are now answered by committed(), ahead of
composed(), and still_waiting() is a Step::Declined: the existing
channel by which a step stops a sequence. Under #227 (no weld; the
commit clears at its head) the binding is one command and the waiting
return precedes that clear, so the fix fits both.
Stacked on #274.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
improvise-fo0w. Three black-hole keys closed over vocabulary that
already exists, each mirroring its Cmd line for line with a lockstep case:
- enter-advance (Normal Enter) resolves as cursor arithmetic over
GridExtent; the host's bottom-right AbortChain is Ignored here, which
loses nothing because Enter is never bound inside a Sequence.
- hide-selected-row-item (H) resolves to ModelCommand::HideItem over the
outer row category and the selected tuple's first label, and refuses
on a fold with the host's own FOLD_REFUSAL, now in improvise-protocol.
- search (/) and search-navigate (n) decline out loud as a pair: n needs
the whole model, and / alone would open a box that finds nothing.
NEEDS_THE_HOST drops to exit-search-mode, force-quit and
toggle-group-under-cursor. Tests that used Enter or "Start search" as
their upstream example move to Ctrl-Q and "Exit search mode", including
shell.spec.js's not-sent case.
Also improvise-0629: the help render test now asserts every ex row is
drawn dim, not just that its description is marked.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
The advance no longer reaches the commit door on a fold or a formula's
cell, so the two older advance tests stopped seeing it (measured: the
cell_key rival passes them). Two tests seat the editor there directly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
An empty record exists only as a layout position, so resolve it against the
layout of the view the command names -- the one grid_in projects for that
session -- rather than the host's own layout.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
EnterEditAtCursor asks core::uneditable, the commit door's own question,
now shared with committable_cell_key. The browser's advance asks the
projected id at the landing cell; lockstep pins both ends on a fold and
on a formula's cell.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
resolve.rs::written read its operands at Enter: the record and measure
from the cell under the cursor, and the basis from claim(), i.e. the
version current at commit. So a HideItem from another client that
renumbered the rows under an open editor sent the write to whichever
cell the cursor then named (8xq0), and a write by another client to the
record while the editor was open was silently overwritten instead of
refused RecordMoved (xz2z, 0j0f's rule on the keyboard door).
The fix reuses #221's capture rather than adding a second one.
ClientState::opened becomes OpenedCell { record, target, version,
document_generation }, where OpenedTarget is Value { measure } or
Coordinate { category }. settle_opened captures a Record cell as a
Value target, and written builds the RecordCell and the claim_as_read
basis from it. With no capture while the host has yet to answer this
client's own last write, the commit declines with the rename's "still
waiting" status.
Red first: both round-trip probes failed on gh/main 202e7eac and on
#221's head 39821d5f. Unit-test fixtures that set Editing by assignment
now open the editor through the fold, which is where the capture is
taken.
Stacked on #221 (fix/fdk1-coordinate-rename).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
pb30: gz puts the cursor on the collapsed group through landing and
place_cursor, and their column arms were untested. Three rivals passed
all 1948 tests: the arm swapped, landing returning None for columns, and
landing reading col_tuple(0). A new test uses two column levels
(Region over Month) with the cursor at (1, 7), South/May. It requires
the cursor to land on South's Q2 at (1, 5). Each rival fails it and
nothing else. The col_tuple(0) rival needs the two column levels, since
with one level the prefix is empty everywhere.
0r01 (Codex's P2 on #263): innermost_group built open_runs for the
whole axis, cloning a level stack per row, to answer for one row on
every z. open_runs is now a lazy iterator, still the one reader, and
the lookup takes .nth(idx). a_lookup_does_not_read_past_its_row gives
it an unreadable header after row 0 (level usize::MAX). The eager rival
panics with capacity overflow.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze