Repository navigation
feat(app-shell): the Studio Changes badge and the item header follow the copilot's drafts as they land (objectui#12725, part) - #12952
Merged
objectstack-fleet[bot] merged 4 commits intoOct 18, 2026
Conversation
…opilot's drafts as they land (objectui#12725) ChatPane announces each authoring tool result on the assistant bus the moment it lands, keyed by message and position, instead of when its turn ends; the Studio badge's ledger re-reads on that pulse. The pillars' item headers read the same ledger: 「草稿 · N 项未发布」 while the open item is a pending draft, N the badge's number. Claude-Session: https://claude.ai/code/session_01Cbdek8SKSHxddGqTPZXdqX Co-authored-by: Claude <noreply@anthropic.com>
…our it is (objectui#12725) Claude-Session: https://claude.ai/code/session_01Cbdek8SKSHxddGqTPZXdqX Co-authored-by: Claude <noreply@anthropic.com>
… item header line (objectui#12725) Claude-Session: https://claude.ai/code/session_01Cbdek8SKSHxddGqTPZXdqX Co-authored-by: Claude <noreply@anthropic.com>
…aft-badge-marks Claude-Session: https://claude.ai/code/session_01Cbdek8SKSHxddGqTPZXdqX Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contributor
Author
席位核对 · ACCEPT ·
|
objectstack-fleet
Bot
deleted the
claude/issue-12725-draft-badge-marks
branch
October 18, 2026 10:12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #12725
Clause-②: no
What changes
Phase 5 of objectui#12725 takes two remainder items of the card: item 5 (the 变更 badge counts the copilot's drafts the moment they land; the item header shows 「草稿 · N 项未发布」) and item 3's marks (the 草稿 chip and the 来源 line), which were measured first and are not drawn (below).
draftedresult lands, not when the turn ends.ChatPane(the chat host the Studio copilot mounts) announces each tool result that carries an authoring envelope (draftReview, or a 确认修改 replay'sreplayOutcome) on the assistant bus (emitMetadataRefresh) the moment it appears in the stream. Each result is announced once, keyed by its message and its position in that message; the results already in the thread when the pane mounts are history and are not announced. No reload, no navigation: the badge's own ledger (usePendingDrafts,GET /meta/_drafts) already re-read on that pulse.studioItemDraftStatus.ts, provided by the surface to its pillars), so a draft the copilot writes to the open item shows as it lands, and the line goes when a publish or a discard takes the item off the ledger. N is the badge's number: publishing is package-level, so the line says this item is one of N unpublished changes, and its tooltip says that. The pillar's own reading of its item (taken at load) counts only while the ledger is behind a Studio save (objectui#11787'sbehindSave); where the ledger could not be read, the header keeps the old chip with no count.engine.studio.itemDraftPending,engine.studio.itemDraftPendingTitle.The measured chain (before this change, at
32b492b6)StudioDesignSurfaceholdsusePendingDrafts({ packageId }), whosefetchPendingDraftsreadsGET /api/v1/meta/_drafts?packageId=…, the route the framework'slistDraftsserves. The count is the row count (shownPendingCount, at least one whilebehindSave). The cloud envelope'sdraftStateis read from the samelistDraftsserver-side (cloudf9e45145,readDraftStateinchange-posture.ts).publishNonce; on a Studio draft save (onDraftSaved→refresh({ afterSave: true })); and on the assistant bus's pulse, whichusePendingDraftssubscribes to (subscribeMetadataRefresh).ChatPane, in the effect commented "objectui#5801 — when a turn that STAGED or PUBLISHED something finishes": it returned early whileisLoading, and read the last assistant message only. So adraftedresult reached the badge only when the stream finished, after the agent's whole narration.StudioChatDock→StudioCopilotConversation→ChatPane;ChatPaneowns the stream (useObjectChat) and is the only host-side holder of the tool results.ChatDock.tsxis the console's own dock, not Studio's. The callback the chat already exposes to its host,ChatbotEnhanced'sonDraftArtifacts, is not enough: it fires only when the cumulativetype:nameset grows (a second change to the same item, or one after a publish, never fires it), andmapMessagessuppressesdraftReviewon a confirm-replay (replayOutcome ? undefined : detectDraftResult(result)), so the 确认修改 path, the one cloud#2924's draft posture goes through, never reaches it. The change therefore lives inChatPane's own effect (AiChatPage.tsx);ChatbotEnhanced.tsxandmapMessages.tsare untouched.hasDraft, taken once when the item loads and set by its own save, and not re-read on the pulse; anobjectleaf on Interfaces carries none. A copilot draft to the open item never marked its header (live reading below).Item 3, the 草稿 chip and the 来源 line: measured, not drawn
No declared key carries per-field draft provenance (who drafted it, from which surface, when), so nothing is drawn for item 3, and nothing is inferred from timing or from "the last tool call touched this field".
be076dad28a0):ListDraftsResponseSchema(packages/spec/src/api/protocol.zod.ts) declares, per draft row,type,name,member,label,packageId,updatedAtandupdatedBy. Those are per ITEM, not per field.SysMetadataRepository.listDraftsfillsupdatedByfromrow.updated_by ?? row.created_by, the acting user; the copilot stages with that same user asactor(cloudstageDraftpassesinput.actor, which the tools set fromexec?.actor?.id), so a copilot draft and the user's own hand edit carry the sameupdatedBy.sys_metadata'ssourcecolumn (filesystem | database | api | migration) is storage origin, andlistDraftsdoes not project it.f9e45145,applyDraftanddescribeLandedChangeinmetadata-tools.ts):status,type,name,packageId,summary,changedKeys,note, thenposture,draftState(pending,itemsastype/name,others),existingRowsandappliedCard.changedKeysnames the keys one call wrote, in the conversation only: it carries no time, is not stored with the draft, and is not true any more once the user or a later call edits the same key. Drawing the chip from it is exactly the "last tool call touched this field" inference the dispatch rules out.Tests
StudioDesignSurface.copilotDraftBadge-12725.test.tsx(5): the real surface, top bar,usePendingDrafts, right slot and Studio copilot (StudioChatDock→StudioCopilotConversation→ChatPane), the chat package's ownuiMessagesToChatMessagesfed the producer's envelope shapes; the transport plays a scripted stream. (1) adraftedapply_editresult re-reads the badge to 2 while the turn is still running, with the same surface tree and location; (2) a 确认修改 replay (replay_…id) does the same; (3) each result is announced once, later chunks and the turn's end send no second pulse; (4) CONTROL: a read tool's result sends no pulse; (5) a drafted result already in the thread at mount is history, not announced.StudioDesignSurface.itemDraftLine-12725.test.tsx(4): the Data pillar's header shows the line with the badge's number as the copilot's draft lands; gone after a publish (the surface's owndoPublish); gone after a discard takes the item off the ledger; CONTROL: another item's draft is counted by the badge and marks no header.__tests__/studioItemDraftStatus-12725.test.ts(7): the derivation rules one by one (listed, stored-type fold, absent, a stale own reading, thebehindSavebridge, no ledger, no item).32b492b6sources checked out over the committed tests, restored bygit checkout HEAD, blobs equal to HEAD andgit diff HEADempty): 7 red, 2 green (the two CONTROL cases). Red: the three landing cases (no pulse while the turn runs), the history case (the base announced a hydrated thread's last authoring turn on mount: 1 pulse), and the three header-line cases (the header never shows the copilot's draft).111d6816, through objectstack'sscripts/ablation-replace.mjsin wrap mode; no dist step, the tests import app-shell src relatively): A1, ChatPane'sif (landed) emitMetadataRefresh();replaced byvoid landed;: 3 red (the three landing cases), the CONTROL and the history case green; restored, blob == HEAD (ad1d0a6041b1) andgit diff HEADempty. A2, the header line'sif (!listed && !(ownDraft && pending.behindSave)) return undefined;replaced byif (!ownDraft) return undefined;(the pillar's own reading only, the old source): 6 red (the three header-line cases, three derivation cases), 5 green; restored, blob == HEAD (6c981c2758f8) andgit diff HEADempty.vitest run packages/app-shell/(the package script's command) at111d6816, in 8--shardruns under the verify lock (one run exceeds the 10-minute foreground cap), each EXIT 0: 1568 files run (1567 passed, 1 skipped) of 1568 tracked test files; 14740 tests passed, 9 skipped. After mergingorigin/main(0707b4ae, which brought objectui#12724'smapMessagesandChatbotEnhancedchanges) ata2326866: the new files plusconsole/ai/__tests__,studio-design/__tests__,layout,previewand the six surface files that read the badge or the status chip, 176 files / 1118 tests, EXIT 0. The whole package was not re-run after the merge; that run is CI's.Gates
Local, exit codes captured before any pipe.
pnpm --filter '@object-ui/app-shell^...' build: VERDICT command-exit 0, at111d6816and again ata2326866.pnpm --filter @object-ui/app-shell type-check(tsc --noEmit && tsc -p tsconfig.test.json): EXIT 0, 0error TS, atb151ed07and ata2326866; the test project lists all three new test files (--listFilesOnly).eslinton the seven changed files: 0 errors; warnings per file unchanged against the base (AiChatPage.tsx34,StudioDesignSurface.tsx13,i18n.ts0); the new files 0.a2326866:check:i18n-keys,check:i18n-drift,check:i18n-designer-parity,check:new-line-citations(0 new),check:control-bytes,check:test-path-roots,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:changeset-claims,changeset:check,scripts/check-changeset-presence.mjs(7 source files of 1 released package, 1 changeset). EXIT 0 at111d6816:check:i18n-dead-keys(report-only; the two new keys are not among its candidates),check:pending-changeset-literals,check:spec-symbols,check:phantom-deps,check:self-import,check:esm-specifiers,check:unreferenced-sources,check:handler-key-reads,check:metadata-write-doors,check:comment-mask-corpus.Live reading
Evidence branch
claude/issue-12725e-evidence(917af6f8),evidence/12725e/: screenshots at 1010 and 1280 px, before (32b492b6) and after (111d6816), the DOM readings as JSON, and the driver. Published backend 18.0.0-next.3 (showcase); the copilot's chat stream and the_draftsledger are played by the driver (the community backend serves no agent): the tool answersdraftedwith cloud#2924's envelope shape, then the agent narrates for 20 s.No reload and no navigation in any run. At 1010 px the header chip folds to its pencil glyph (objectui#12611), its text in the tooltip.
Acceptance notes
AiChatPage.tsx"only for labels it passes"; it landed as PR feat(ai-build): the plan and result cards draw the v2 rows cloud declares, and step names read as sentences (objectui#12724) #12947 before this branch mergedmain, and touched no line of this file. This change is the authoring-emit effect only.MetadataProviderre-reads its loaded types, the Studio rails re-read); a turn with several authoring calls sends several, spaced by the agent's own calls.publishedresult (cloud'smaybeAutoPublish, which promotes every pending draft of the package) sends no pulse, so the badge keeps the swept drafts; at 1010 px the copilot's chat panel overflows its 300 px slot once the thread holds a message. Both are in the report.Generated by Claude Code