Evidence, docs and the bundle surface for the mouse editor's seed and first edit (improvise-ugj4, improvise-jibp)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Edward Langley
2026-09-23 08:52:26 -07:00
parent fa0af07988
commit 35013b6bda
5 changed files with 90 additions and 24 deletions
+11 -6
View File
@@ -1573,8 +1573,10 @@ Four smaller decisions that would be re-derived wrongly:
Since improvise-n6t8 it shares `#bottom-dock` with the toast, which stacks
**above** it, so the prompt is neither covered nor displaced.
`RenderCell::edit_text` is the stored value, not the formatted one: seeding an
editor from `text` and committing turns `1200` into the string `"1,200"`. The
An editor is seeded from `edit_seed(target)`, the stored value the target
captured, never from `text`: seeding from the formatted text and committing
turns `1200` into the string `"1,200"`. `RenderCell` carries no `edit_text`
since improvise-ugj4, because a paint can be stale while an editor is open. The
`numeric`/`error` flags are the server's answers, echoed — the client must not
re-derive them from the text, which is how the two ends drift.
@@ -1617,7 +1619,7 @@ The browser half of improvise-cr3, kept as thin as it can be made, now talking
to either of two peers (improvise-djm). **The boundary is JSON strings in and
out, so this file holds no protocol knowledge at all**: it never inspects a
`Command`, never builds a `CellId`, never decides whether a cell is editable or
whether a typed string is a number. Five calls are the whole harness:
whether a typed string is a number. These calls are the whole harness:
| call | what it is |
|---|---|
@@ -1625,6 +1627,8 @@ whether a typed string is a number. Five calls are the whole harness:
| `client.on_frame(json)` | fold one frame, in arrival order |
| `client.render_model()` | everything to paint, already decided |
| `client.edit_target(r, c)` | what an editor opened here edits (the record, the version seen, and the seed it opened on): an opaque string the page holds, or `null` |
| `client.edit_seed(target)` | the text the target's editor opens on: the seed it captured, never the paint (improvise-ugj4) |
| `client.edited_target(target)` | the same target marked edited, so retyping the seed still writes (improvise-jibp) |
| `client.typed_cell(target, s)` | the frame for "the user typed s into that cell", by identity (improvise-2wu3), or none if `s` is still the target's seed (improvise-ixxx) |
| `client.on_key_event(k,c,a,s,m)` | the frame for a keypress, folded locally first where this client may answer it |
@@ -1832,9 +1836,10 @@ Neither arm leaves one armed.
- **`editable` is the server's answer relayed through Rust**: a stored cell. An
aggregate is a drill target and drill is not on the wire yet, so it is simply
not offered.
- The inline editor seeds from `RenderCell::edit_text` — the STORED value, not
the drawn one: `td.textContent` has been through the number format, so a stored
1200 reads `"1,200"` and committing that stores the *string* `"1,200"`. Enter
- The inline editor seeds from `client.edit_seed(target)` — the STORED value the
target captured, not the drawn one: `td.textContent` has been through the number
format, so a stored 1200 reads `"1,200"` and committing that stores the *string*
`"1,200"`. Enter
removes the focused `<input>`, which fires `blur`, which calls `close` again:
harmless, but only by accident.
- **The title bar is three fields and one joining.** `doc_name`, `doc_dirty` and
+13 -9
View File
@@ -699,7 +699,9 @@ for that, beside `typed()`. Every commit path reaches it:
- **Browser mouse.** The seed is a field of #188's `EditTarget` (`opened`),
captured by `edit_target_at` from the cache's `edit_text` beside the record
and its version. `typed_target_frame` starts with the `untouched` check, and
the page passes no seed (`typed_cell(target, text)`).
the page passes no seed (`typed_cell(target, text)`). The `<input>` shows
`edit_seed(target)`, so what the page shows and what it compares against are
one capture (improvise-ugj4).
**The seed is captured at open, never recomputed at commit.** Where the editor
opened is where `OpenedEditor` is written. For the TUI that is `EditOrDrill`
@@ -742,14 +744,16 @@ deliberately do **not** mark it:
The comparison stays exact: whitespace is content, and nothing trims.
**The mouse editor tracks no `touched`.** Its edits happen in a DOM `<input>`
that Rust never sees, so on that path `untouched` compares content alone, and
the 2054 case survives there (improvise-jibp). The fix belongs with
`EditTarget`, for example an `input` listener that reports the first edit. A
second gap sits on the same path. The `<input>` is still seeded from the
**painted** `editText`, while `EditTarget` captures the **cache**. So a paint
that went stale while another editor was open shows old text against a newer
seed and version (improvise-ugj4).
**The mouse editor reports its first edit.** Its edits happen in a DOM
`<input>` that Rust never sees, so the page's `input` listener, registered
`{ once: true }`, swaps its target for `edited_target(target)`, which is
`EditTarget::edited`: the same capture with `touched` set (improvise-jibp).
Before that, the mouse path compared content alone, so the 2054 case survived
there. The `<input>` used to be seeded from the **painted** `editText` while
`EditTarget` captured the **cache**. `repaint` returns early while an editor is
open, so a paint that went stale then showed old text against a newer seed and
version (improvise-ugj4). The page now seeds from `edit_seed(target)`, and
`RenderCell` no longer carries `edit_text`, so the paint cannot be asked.
This does **not** change classification. `007` typed fresh is still
`Number(7)`, and `a_touched_browser_commit_still_writes` pins that.
+44 -5
View File
@@ -4933,11 +4933,50 @@ sends no write, and a touched one does". It wraps `Worker.prototype.postMessage`
opens the editor on cell 4, and presses `Enter`. No `SetCell` may be posted.
Then it types `1` and presses `Enter`, and exactly one must be.
**Known gap, filed as improvise-ugj4.** The `<input>` is still seeded from the
painted `editText`, while `EditTarget` captures the cache. A paint made stale
while another editor was open (`repaint` returns early while `editing` is set)
therefore shows old text against a newer seed and version.
**improvise-jibp** is the other residue: the mouse editor has no `touched` flag.
Both gaps this entry used to list are closed: the stale-paint seed
(improvise-ugj4) and the missing `touched` flag (improvise-jibp). See the
entries that follow.
### `a_mouse_target_marked_edited_writes_its_own_seed`
**improvise-jibp**, the Rust half. A target on the seed `"1200"` commits
`"1200"` and composes nothing: that is the control. `edited()` must leave the
seed where it was, and the same text committed through the edited target must
compose a write. **Rival:** `edited()` leaving `touched` false fails this test
and `the_page_opens_its_editor_on_what_the_target_captured`, and nothing else.
### `the_page_opens_its_editor_on_what_the_target_captured`
**improvise-ugj4 and improvise-jibp**, through the wasm-bindgen surface the
page calls. The cell is drawn `"1,200.50"` and stored `"1200.5"`, and
`edit_seed` must return `"1200.5"`. A projected write then moves the cell to
`"777"`. A target taken afterwards seeds `"777"`, and the first target still
seeds `"1200.5"`, because a target is one capture. `typed_cell(later, "777")`
composes nothing, and the same text through `edited_target(later)` composes a
write. **Rival:** as above, `edited()` leaving `touched` false fails here. I
wrote no rival for an `edit_seed` that re-reads the cache. `EditTarget` names a
record, not a position, so that rival would need a lookup the client does not
have.
### a mouse editor typed back to its own seed still writes — `harness/tests/contract.spec.js`
**improvise-jibp**, the page half. The editor opens on cell 4, and the test
types the seed over the selection and presses Enter. Exactly one `SetCell` must
be posted. Before the fix the page sent `typed_cell(target, seed)` with an
untouched target, which composes nothing. **Rival:** delete the page's `input`
listener. This test fails, and the rest of `contract.spec.js` passes (16).
### an editor opened after an unpainted write shows and commits what it captured — `harness/tests/contract.spec.js`
**improvise-ugj4.** The test opens an editor on cell 0, so `repaint` stops.
While it is open, it delivers a synthetic frame that moves cell 4 to `777`. It
builds that frame from the last `Cells` entry the page actually received for
cell 4, changing only `text`, `edit_text` and `value`, so the record and the
version are real. Then it opens cell 4. The `<input>` must show `777`, and
Enter with no edit must post no `SetCell`. **Rival:** seed the `<input>` from the
paint (`drawn.replaceAll(",", "")`). This test fails on the value, and so does
"an untouched mouse editor sends no write, and a touched one does", because the
drawn text is not the seed. The other 15 pass.
### `a_browser_editor_typed_back_to_its_seed_still_writes`
+18 -4
View File
@@ -313,11 +313,25 @@ mod tests {
],
});
let first = client.edit_target(0, 0).unwrap().expect("a record cell");
assert_eq!(client.edit_seed(&first).unwrap(), "1200.5", "not the drawn text");
client.fold_frame(ServerFrame::Commands(vec![Command::Project(at("777", "777"))]));
assert_eq!(
client.edit_seed(&first).unwrap(),
"1200.5",
"not the drawn text"
);
client.fold_frame(ServerFrame::Commands(vec![Command::Project(at(
"777", "777",
))]));
let later = client.edit_target(0, 0).unwrap().expect("a record cell");
assert_eq!(client.edit_seed(&later).unwrap(), "777", "not the cache at open");
assert_eq!(client.edit_seed(&first).unwrap(), "1200.5", "a target is one capture");
assert_eq!(
client.edit_seed(&later).unwrap(),
"777",
"not the cache at open"
);
assert_eq!(
client.edit_seed(&first).unwrap(),
"1200.5",
"a target is one capture"
);
assert!(client.typed_cell(&later, "777").unwrap().is_none());
let edited = client.edited_target(&later).unwrap();
+4
View File
@@ -80,9 +80,13 @@ test("the page still reaches the client through a handful of calls", () => {
"click_page_item",
"click_tile",
"click_view",
// improvise-ugj4: the editor shows the seed its target captured, not the paint.
"edit_seed",
// improvise-2wu3/0j0f: what an editor edits, captured when it opens, so
// its commit names that record at that version and not a position.
"edit_target",
// improvise-jibp: the first edit of the <input> marks the target touched.
"edited_target",
"on_frame",
"on_key_event",
"render_model",