Skip to content

fix(app-shell): Studio re-reads the open object, its extension and its form view on the in-tab copilot pulse - #32930

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-ahsrk0
Nov 13, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-ahsrk0

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #32610

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:Studio 数据支柱在同一标签页的副驾「存为草稿」或「全部撤销」之后,会按服务端草稿重新读取当前打开的对象、它的基础对象扩展和表单视图,字段属性栏、表单画布和头部读数不用刷新页面就跟上。
  • 为什么改:之前副驾改完,属性栏没有「草稿」标记、帮助文字为空、画布看不到新字段,用户以为没改成功,会重复下指令,做出重复草稿。
  • 风险与回滚:作者有未保存的编辑或保存还在进行时,重读会推迟到保存落地后再做,不会覆盖作者的改动;回滚就是撤掉这个 PR,行为回到「刷新页面才更新」。
  • 先看:StudioDesignSurface.tsx 的对象加载和扩展加载两个 effect(加载键多了重读计数、refreshOnly 保留选中字段);useFormViewDraft.ts 的加载依赖;DataPillar.copilotDraftMarks-12725.test.tsx 新增的 describe。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/DataPillar.copilotDraftMarks-12725.test.tsx,预期 10 个测试全过,其中 4 个是本卡新增。
  • 需要你做的:无,席位落地。
  • 席位意见:席位(domain:ui#4)审过。第一版的表单视图在冲突后选「重新载入」时会被当成副驾重读而跳过安装,作者看不到已保存版本;返工一轮改为只在「仅重读计数变化」时才视为重读,并加了对照测试与消融。数据支柱两处加载本就按完整加载键判断,没有这个问题。只改 @object-ui/app-shell,不碰服务端,不加新机制(studio: refetch drafts on focus and poll while the tab is visible, so changes made outside the tab show without a reload — ruled C, phase 1 (phase 2 is #33023) #32592 的待裁问题不受影响)。CI 全绿后按队列落地。

What changes

The in-tab copilot already announces both its draft write and its undo on the assistant bus (emitMetadataRefresh, sent by ChatPane), and the Studio ledger and the Data rail already re-read on that pulse. The Data pillar's open-object load did not: it was keyed on object, publish nonce and conflict-reload nonce only, so the field inspector, the Form canvas and the header's own reading kept the pre-write buffer until a reload. This PR connects the existing pulse to the three loads that ignored it, and adds no new mechanism (no focus refetch, polling or server push; #32592's open decision is untouched).

  • StudioDesignSurface.tsx, Data pillar: useMetadataRefreshNonce(objBuffersHeld) where objBuffersHeld = dirty || extDirty || saving !== false. The object load and the base-object extension load add that nonce to their load key and their dependencies. A load whose only change is the nonce is refreshOnly: it keeps the selected field and the shown extension in place while it reads, and replaces only what they show. After a refresh lands, one effect drops the selected field only if the re-read no longer has it (a discard took it away). An edit begun while the re-read was in flight keeps its buffer, and its save stays pinned to the read it was made on, so the version guard asks the author about a conflict instead of the re-read overwriting the edit.
  • useFormViewDraft.ts: the same pulse, held by the form view buffer's own dirty || saving, re-reads the form view the Form tab edits. As in the Data pillar's loads, only a run whose pulse alone moved is held: the load compares a base key (view, package, publish nonce, reload nonce, customizing), so a conflict's "reload", a publish or a new view replaces the buffer whatever it holds.
  • studioItemDraftStatus.ts: docblock only. It said the pillar's own reading is never re-read on the pulse; it now says the Data pillar re-reads it (held while the author has unsent work) and other pillars read their item once.
  • The header draft count (card step 5): it already reads the _drafts ledger, which re-reads on the pulse. It cleared after an undo on main at unit level too (as domain:ui#2 measured), so it gets a regression pin here and no code change. The staging residue in the card stays NOT MEASURED (an older console build, or read-after-write lag on _drafts).

Tests

Four new pins, each a top-level describe in the component's existing harness:

  • DataPillar.copilotDraftMarks-12725.test.tsx: (1) the open field properties re-read on the pulse (the copilot chip, the copilot's help text, a field the same draft added shows on the Form canvas, and the rail stays open); a re-read that no longer has the open field closes its properties; (3) CONTROL: a pulse during an unsent save is held, so nothing is re-read or replaced until the save lands, then it is re-read (deferred, not dropped); CONTROL: a pulse re-reads the open object only, never another object in the rail.
  • StudioDesignSurface.itemDraftLine-12725.test.tsx: (2) with the object opened WITH a draft, the header's draft line ("Draft · 2 unpublished") goes once the undo empties the ledger, and the pillar re-reads its own reading. The file's docblock and one comment that said the pillar never re-reads on the pulse are corrected.
  • DataPillar.baseObjectExtension-28575.test.tsx: an extension draft the copilot writes at the pair is re-read on the pulse and its field shows in the Form.
  • DataPillar.formViewLayout-30057.test.tsx: a form view draft the copilot writes is re-read on the pulse and the canvas shows its sections; CONTROL: a dirty form view buffer refused as stale (409) is replaced by the saved version when the author picks "reload" (the pulse hold never holds a reload).

The three DataPillar harnesses now import @objectstack/lint at module top (the check:test-source-alias convention for edited files that reach its lazy import).

Verification

Tests at the final head c69a2ed
  • The four edited harnesses plus __tests__/studioItemDraftStatus-12725.test.ts: pnpm --dir ui exec vitest run --maxWorkers=2 on the five files, under os-verify-lock: VERDICT command-exit 0, Test Files 5 passed (5), Tests 47 passed (47).
  • Every test file that imports StudioDesignSurface or useFormViewDraft (197 files, after merging main at ba321c27 and rebuilding @objectstack/spec and @object-ui/i18n): VERDICT command-exit 0, Test Files 197 passed (197), Tests 1336 passed (1336). The pins added after this run are covered by the five-file run above.
  • pnpm --filter @object-ui/app-shell run type-check (both tsc programs, the test one included): VERDICT command-exit 0.
Ablation (each leg through scripts/ablation-replace.mjs, mutation proven on disk, restored to the HEAD blob)
  • A1, the object load's key without the refresh nonce: DataPillar.copilotDraftMarks-12725 + StudioDesignSurface.itemDraftLine-12725 go red, 5 failed: the inspector re-read pin, the closed-properties pin, both CONTROLs (they wait for the re-read) and the header undo pin.
  • A2, the pulse no longer held by the buffers (useMetadataRefreshNonce(false)): exactly the held-save CONTROL goes red (1 failed, 9 passed).
  • A3, the extension load's key without the refresh nonce: the extension pin goes red (1 failed, 15 passed).
  • A4, the form view load without the refresh nonce in its dependencies: the form view pin goes red (1 failed, 7 passed).
  • A5, the form view's refreshOnly made coarse again (any re-run of the same view): the conflict-reload CONTROL goes red (1 failed, 8 passed).
  • Restore: git diff HEAD empty for both files; StudioDesignSurface.tsx hashes to its HEAD blob 236cc6f6, useFormViewDraft.ts to d345a289.
Derived gates (dispatch-gates --commands, run at the final head, stdin closed)
  • node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
  • node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
  • node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
  • node scripts/check-closing-keyword-parity.mjs :: exit 0
  • node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
  • node scripts/check-comment-mask-corpus.mjs :: exit 0
  • node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
  • node scripts/check-empty-changeset.mjs --self-test :: exit 0
  • node scripts/check-sdui-manifest.mjs :: exit 0
  • node scripts/check-sdui-manifest.mjs --self-test :: exit 0
  • node scripts/ci/snapshot-next.mjs --self-test :: exit 0
  • node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
  • node scripts/release-pending-publish.mjs --self-test :: exit 0
  • pnpm --dir ui exec vitest run scripts/ :: exit 0
  • pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
  • pnpm check:changeset-gate-self-tests :: exit 0
  • pnpm check:closed-ceremony-text :: exit 0
  • pnpm check:cross-package-test-inputs :: exit 0
  • pnpm check:dispatcher-error-vocabulary :: exit 0
  • pnpm check:driver-memory-census :: exit 0
  • pnpm check:engine-double-contract :: exit 0
  • pnpm check:gitlink-declared :: exit 0
  • pnpm check:nul-bytes :: exit 0
  • pnpm check:published-files :: exit 0
  • pnpm check:query-options-erasure :: exit 0
  • pnpm check:refd-timer-probe :: exit 0
  • pnpm check:registry-log-declared :: exit 0
  • pnpm check:rest-log-spy-declared :: exit 0
  • pnpm check:skill-identifier-liveness :: exit 0
  • pnpm check:test-source-alias :: exit 0
  • pnpm check:tier-file-adoption :: exit 0
  • pnpm check:type-check-coverage :: exit 0
  • pnpm check:type-check-debt :: exit 0
  • pnpm check:undeclared-dep-imports :: exit 0
  • pnpm check:watch-hint-literal :: exit 0
  • pnpm check:where-matcher :: exit 0

dispatch-gates --ran: 36 derived families accounted for, 36 run, 0 NOT-MEASURED (a derived zero: all 36 recorded an exit code). Every family CI runs is the PR's check-runs. pnpm check:new-line-citations (ui): 0 new citations, exit 0.

Acceptance notes

  • A package publish bumps publishNonce and also emits the pulse, so the open object's load runs once for the publish and the pulse-keyed run cancels the first in-flight read. One extra read request, no behaviour change.
  • The PR carries no assignee and requests no review, per the dispatch order: the seat lands it.

…orm view on the in-tab copilot pulse

The open object, its base-object extension and the Form tab's form view now
re-read on the assistant bus's metadata-refresh pulse, held while any of
those buffers carries an unsent edit or a save in flight. A re-read on the
pulse alone keeps the selected field open and drops it only when the re-read
no longer has it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibzf9ca7cfZHfuRz8ek
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibzf9ca7cfZHfuRz8ek
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 12, 2026
…ld by its buffer

A conflict's reload, a publish or a new view replaces the form view buffer
whatever it holds; the dirty hold applies only to a re-read whose pulse alone
moved, as in the Data pillar's object and extension loads.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibzf9ca7cfZHfuRz8ek
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-tesla · session_01L9qibvZYR36KAJUv44oXZw

席位核对 · ACCEPT · 2026-11-13T00:16Z

Head-sha: c69a2ed253f5acb315240b311a40532322f22719

  1. Shape. Draft PR to main, Fixes #32610. 8 files, +377/−27 = 404 lines ≤ 3000; check-governed-merges --pr 32930: NOT governed. Changeset .changeset/32610-copilot-draft-reread.md (@object-ui/app-shell patch, Clause-②: no, Metadata-protocol: none); no export, no i18n key; contract review not owed. Files within claim 6837487182 plus the docblock-only amendment in 6839325775 (studioItemDraftStatus.ts). Body patched by the seat: seat-opinion line, signed trailer.
  2. Premise, measured. Held on main 5c0f1134 (claim); the card thread re-measured the hunk on post-fix(app-shell): the permission-set pages read the confirm label, clone source, ledger row and Add refusal by the holder they show #32800 main before applying it. The header-count half did not reproduce at unit level and is pinned only; the staging residue stays NOT MEASURED.
  3. Review. Round 1 found one blocking gap (6839325775): the form view's pulse-only test was "same view", so a conflict "reload" with a dirty buffer skipped the install. Fixed at c69a2ed2: a base key of view, package, publish nonce, reload nonce and customizing, plus the installed pulse; only a pulse-alone change is a re-read. CONTROL pin added (a 409 "reload" over a dirty buffer installs the saved version); ablation A5 turns it red. The Data pillar's two loads already compared full base keys.
  4. Gates. Check-runs on this head: all success or skipped, 0 failure, at landing. Card thread at c69a2ed2: 5 harnesses 47/47, app-shell type-check 0, dispatch-gates 36/36 exit 0 (0 NOT-MEASURED), new-line-citations 0; ablations A1–A5 red then restored to the HEAD blobs. Reports 6839290509 and 6839762708.
  5. Accepted deviations. (a) A package publish re-reads the open object twice (publish nonce, then pulse); one extra read, no behaviour change. (b) The three DataPillar harnesses import @objectstack/lint at module top (check:test-source-alias convention).
  6. Out of scope / landing. None filed. Ordinary queue landing: pr_ready + automerge_enable now, on the standing landing go-ahead for pm-dispatch-reviewed PRs (maintainer, 2026-10-30 and 2026-11-06). On MERGED: confirm Studio: after the copilot saves a draft, the open field properties rail, the form canvas and the header draft count keep the old state until a full page reload #32610 closed, remove pm:dispatched.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant