Skip to content

Commit fc65038

Browse files
fix(app-shell): a Studio switch inside the autosave debounce never writes the previous item's buffer into the one just opened (objectui#11232) (#11265)
Fixes #11232 Clause-②: no objectui#11203 (the package-switch member, closed by triage as folded into this card) rides here with its three pins. Implemented on `claude/issue-11232-autosave-send-bound-to-target`, dispatched by the `domain:ui` seat 1 PM loop, session `https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ`. Triage `5915804318` is the ruling executed; claim `5918635427`. ## What changed | where | before | now | |:--|:--|:--| | `useDraftAutoSave` | the timer was keyed on the dirty flag, the blocked flag and the snapshot; the save it called was read through a ref that already addressed the newly opened item, while the buffer still held the previous item's document | takes `target`, the item its `save` addresses (a primitive `type:name`, AGENTS.md #10). A dirty period is bound to the item it began on. A period that began on another item is never sent, by the timer or by `flush`, and it ends when the caller's dirty flag falls (each caller's load clears it as it installs the new buffer). A save's claim (objectui#11204) reads moved once the target has changed | | the four callers | none | pass `flow:NAME` (Automations), the page leaf key (Interfaces page inspector), `object:NAME` (Data), `app:NAME` (Interfaces nav) | | where `StudioDesignSurface` renders `InterfacesPillar` | not keyed: a package switch kept the route mounted, and with it the nav editor's dirty flag, its open editing and the open page | `key={packageId}`: one reset of the whole pillar on a package switch (triage's preferred shape for objectui#11203) | No other line of `DataPillar` changes: its hook call gains the required `target`, and its switch (the control) is untouched. ## The pending edit is dropped, not sent to its own item A switch inside the debounce drops the previous item's unsent edit. It is never written to the newly opened item, and it is not re-targeted to its own item either. Why this side of the ruling's choice: - It is what already happens on every pillar whenever the new item loads inside the debounce: the load installs the new buffer and clears the dirty flag, which cancels the timer. The defect was only the slow-load path. Dropping adds no new write; re-targeting would add a save nobody sends today. - It is what the Data pillar's switch has always done (its synchronous clear), the card's named control. - It matches the confirmed discard of a package switch (objectui#11203), where the author said "discard". - PR objectui#11230's sent-snapshot claim: a save that lands after a switch reads moved, so it never clears the flag of the item opened since. objectui#11189's nav-flush rule ("Done" sends a pending edit at once) is a close of the SAME item's editor and is unchanged; `flush` now also refuses a period that began on another item. ## Which fix covers which member (the claim asked) - Automations flow switch, page-inspector page switch: the hook (ablation leg A). - A save in flight across a page switch: the hook's claim binding (leg C). The save itself always addressed its own item (the save call captured it). - Data object switch: the control, green at base and at HEAD. - The nav autosave on a package switch (objectui#11203): the key (leg B). The hook alone cannot reach it: the nav editor's dirty flag and open editing are the pillar's state, so after a confirmed discard a flag still stood over the next package's app, and no edit after the switch could start a period the hook would send. The page inspector's package-switch state, which triage asked measured in the same pass, is the key's too. ## Premises re-measured at `origin/main` `02a22957c0` (after PR objectui#11259) The objectui#11204 harness reading still holds on the tip. Pin file first, fix absent (the red run): - Automations: edit flow `notify_owner`'s start label to 'Kick-off', open `nightly_digest` (load held) inside the debounce: **1 save, of `nightly_digest`, carrying `notify_owner`'s document** (label 'Notify owner', start 'Kick-off'). - Page inspector: edit page `home`'s label to 'Welcome', open `landing` (load held): **1 save, of `landing`, carrying `home`'s document** (label 'Welcome', the hello block). - Package switch, confirmed discard, fast load: **1 save of app `beta_app` to package `com.beta.app`**, B's own navigation, unedited (objectui#11203's reading). With B's app load held: **1 save of `acme_app` to `com.acme.app`**, the edit the author had just confirmed discarding. - Page inspector on a package switch: page `home` of package A stays open on package B, and an edit then saves page `home` with package `com.beta.app`. - Data (the control) and the cancelled discard: green. PR objectui#11259 did not close any member: its edits (the load effect's first-leaf pick, the `?surface=` restore, `StudioNavItemInspector`) do not touch the hook or the switch paths. ## Pins New file `StudioDesignSurface.autosaveSwitch-11232.test.tsx`: the real pillars and the real registered `FlowPreview` / `FlowInspector` / `PagePreview` / `PageBlockInspector` / `PageDefaultInspector`, a server double that records every save with its package and can hold an item's load or a save. - Automations: a flow switch inside the debounce sends nothing; the edit after the new flow's load saves to it (the control). - Page inspector: the same for a page switch. - Page inspector: a save in flight across a page switch lands on its own page, and an edit typed on the previous page's buffer before the next page's load lands is not sent there. - Data: an object switch inside the debounce sends nothing (the control, as it always has), and an edit after the load saves to it. - objectui#11203, through the real surface and its `PackageSwitcher`: a confirmed discard sends nothing to the new package and closes editing; an edit after the switch saves normally, to `beta_app` in `com.beta.app`; the same with B's app load held; a cancelled discard keeps package A and its edit, which then saves to `acme_app` in `com.acme.app`. - Page inspector on a package switch: the new package's page opens and an edit saves to it, in the new package. The objectui#11189 / objectui#11204 / objectui#11167 / objectui#11196 autosave pins are unmodified and green (see the directory run below). ## Reverse verification and ablations (each on a committed HEAD; mutation and restore proven on disk) Every leg ran `pnpm exec vitest run` on the pin file (8 cases) at HEAD `3cd7a994d1`. The pins import the pillar source directly (a relative import, no `dist/`), so no build leg applies. Legs A to C went through objectstack's `scripts/ablation-replace.mjs` in WRAP mode (anchor x1 to x0, blob moved, restore proven: blob == HEAD `f8072729aaf6` and `git diff HEAD` empty). The base leg swapped in the base file under a trap, with the same two restore proofs. | leg | mutation | predicted | observed | |:--|:--|:--|:--| | base | `StudioDesignSurface.tsx` at `02a22957c0` (blob `a2259cfc6561`, verified on disk) | red except the two controls | 6 failed / 2 passed: the Data control and the cancelled discard green. The in-flight pin reads 2 saves, `home` 'Welcome' then `landing` 'Typed during load'; the package-switch page pin finds `home`'s hello block still open on package B | | A | the timer's `owned()` check made a no-op | red: Automations, page switch, in-flight | 3 failed / 5 passed, exactly those | | B | `key={packageId}` deleted | red: the confirmed-discard pin (editing stays open over a standing flag), the package-switch page pin | 3 failed / 5 passed: those two AND the slow-load discard pin, which read 1 save of `acme_app` to `com.acme.app` (the discarded edit). Direction differs from the prediction: an instrumented re-run read `target` still `app:acme_app` when the debounce fired, because the new package's app name had not committed yet (see the harness note under Acceptance notes). So the key, not the hook, holds that pin | | C | the claim's target comparison removed | red: the in-flight pin only | 1 failed / 7 passed, exactly that | ## Tests and gates at `3cd7a994d1` Every run below is at `3cd7a994d1`, from the worktree root, with root-relative paths. Pass counts are read from vitest's own summary lines. - Build closure: `turbo run build --filter='@object-ui/app-shell^...' --concurrency=2`, 28 of 28 tasks, exit 0 (under the shared verify lock, run at `e4594fdc66`, whose source is byte-identical to HEAD's; only the pin file moved since). - `pnpm --filter @object-ui/app-shell type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`): exit 0. `tsc -p tsconfig.test.json --listFilesOnly` lists the new pin file once. - The pin file: 8 of 8 passed. - `packages/app-shell/src/views/studio-design/`, every test file (82, the tree listing and `git ls-files` agree), in three chunks under the lock: 29 files / 137 tests, 25 / 131, 28 / 207, so 82 files / 475 tests passed, 0 failed. It holds every suite that names `useDraftAutoSave`, `AutomationsPillar`, `InterfacesPillar` or `PackageSwitcher` (`git grep` finds none outside it), and the objectui#11189 / #11204 / #11167 / #11196 autosave pins unmodified. - The surface's other renderers: `StudioRoute.test.tsx`, `StudioRoute.landingI18n.test.tsx`, `App.uploadAltitude-10131.test.tsx` and `studio-locale.i18n.test.tsx`: 4 files / 30 tests passed. - eslint on the 2 changed source and test files: 0 errors. `StudioDesignSurface.tsx` has 18 warnings, the same rules and counts as the base version (linted as `git show BASE:path` through `--stdin`). The pin file has none. No type-aware lint is configured, so the diff cannot move another file's verdict. - Gates, each exit 0: `check-changeset-presence`, `check:control-bytes`, `check:new-line-citations` (0 new), `check:changeset-claims` (the one pending changeset naming this file, `6681-declared-lazy-marketplace-routes.md`, was read: it says this file statically imports `SuggestedBindingsPanel`, and that still holds), `check:pending-changeset-literals`, `changeset:check`, `check:test-path-roots`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, `check:metadata-write-doors`, and `check-governed-queue-guard --test` (NOT GOVERNED, 3 paths). - NOT MEASURED: CI (the full farm, `pnpm lint`, e2e) and a browser run. ## Acceptance notes - **The dropped edit, on every switch.** An edit made less than 1.5 s before any switch (flow, page, object or package) is dropped, as above. On a package switch this now includes a page-inspector edit: before, it happened to be sent to its own page in its own package (the page save's callback still held the previous package); now the remount drops it. Page edits are not held by the surface's leave guard (only nav edits are), so no prompt announces it. Read at source, not a new class: the pillar-tab switch unmounts the pillar and drops such an edit the same way today. - **An in-flight save's own UI effects are not bound to its item.** A save of the previous item that lands after a switch still sets the "unpublished draft" badge, and a failure its error, on the item opened since. Nothing is written; read at source, not measured. - **Harness boundary.** `pastDebounce` waits inside one `act()` scope, which holds React renders from async continuations until the scope ends while real timers fire. So in leg B the switched-to app's name (set after the app list's await) was not yet committed when the debounce fired, and the hook still read the previous app as the target; that interleaving is also the real one when the new package's app list is slow, and the key is what holds it. Not measured in a browser. - Out of scope, measured on this branch and reported to the seat, not fixed here: the previous item's buffer stays on screen and editable under the newly opened item while its load is in flight, and a save from there writes it into the new item (page inspector autosave, the Automations enable toggle, the Data pillar autosave); and a package switch keeps the Automations pillar's open flow of the previous package, which an edit then saves into the new package. --- _Generated by [Claude Code](https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f61dab1 commit fc65038

3 files changed

Lines changed: 633 additions & 7 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): switching Studio to another flow, page or package no longer saves the previous item's unsaved edit into the one just opened
6+
7+
Studio's editors save drafts on their own, 1.5 s after the last edit. If the
8+
author opened another flow or page inside that wait, and the new item took
9+
longer to load than was left of it, the pending save went out addressed to the
10+
item just opened, carrying the previous item's whole document. The newly opened
11+
item's draft was overwritten with another item's content. This was measured on
12+
a writable package in the Automations pillar (one flow saved with another
13+
flow's steps) and in the Interfaces pillar's page settings (one page saved with
14+
another page's label and blocks).
15+
16+
An unsaved edit now belongs to the item it was made on. When the author opens
17+
another item before it is saved, nothing is sent to the item just opened, and
18+
the pending edit is dropped, which is what the Data pillar has always done and
19+
what every editor already did whenever the next item loaded quickly. An edit
20+
made after the new item has loaded saves to it as usual. This applies to every
21+
editor that uses Studio's shared draft autosave: the Automations and Data
22+
pillars, the Interfaces page inspector and the Interfaces navigation editor.
23+
24+
Switching package in the Interfaces pillar now starts it afresh on the new
25+
package. Before, an unsaved navigation edit the author chose to discard kept
26+
the editor marked as having unsaved changes, so the next package's navigation
27+
was saved as a draft although nobody edited it, and the previous package's
28+
open page stayed open and saved into the next package.

0 commit comments

Comments
 (0)