Repository navigation
feat(app-shell,plugin-chatbot,i18n): launchers mark a proposed plan awaiting approval (objectui#11666) - #11671
Conversation
…waiting approval (objectui#11666) ChatbotEnhanced reports whether the thread's newest proposed plan still awaits the user (the plan card's own resolveProposalCardState reading) through a new optional onPlanApprovalPendingChange prop. ChatPane publishes it on the assistant bus per conversation and signed-in user; the console FAB and the ChatDock launcher read the bus and show a marker with a translated accessible description. Opening a chat clears nothing; the plan leaving "awaiting" does, and deleting the thread drops it. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…11666) Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
✅ 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
|
ACCEPT: PR objectui#11671, head
|
Contract reviewServed-tier: Inputs: card objectui#11666 (body, claim comment 6008002333, os-dev-report 6009207558); PR objectui#11671 (body, its 21-file list, comments 6009224402 and the seat's ACCEPT 6009241489; zero review comments, zero reviews); the net diff of the head against Checks on the head: 43 check-runs, 40 success, 3 skipped ( ① Derived judgments
② Semver level
③ Boundary flagsDev deviations (os-dev-report 6009207558):
Open questions, answered by the seat in 6009241489, judged here:
Two readings of the card worth naming, both holding:
Nothing escalates to the maintainer on this head. Implemented-by: VERDICT: PASS Read at 2026-10-06T04:34Z, after the last of the head's in-progress check-runs completed. |
|
The seat now marks this ready and arms auto-merge through the merge queue.
Generated by Claude Code |
Fixes #11666
Clause-②: yes
The console chat launchers now show a marker while a proposed plan awaits the user's approval (objectui#2458 item 6). Before this, a user who closed the chat with a blueprint still waiting got no sign of it from the launcher they came back to.
What changed
ChatbotEnhanced(@object-ui/plugin-chatbot) takes a new optional prop,onPlanApprovalPendingChange. It reports whether the thread's NEWEST proposed plan is stillpendingaccording toresolveProposalCardState, the one producer the card's header and body already read (objectui#7254). So the reading is true exactly when the newest plan card offers "Build it". It fires once on mount and again on every change. An approval (the optimistic "Building…" flip) or a build that ran ends it. A newer proposal supersedes the plan before it.ChatPane(app-shell) publishes the reading per conversation and per signed-in user. The new bus functions arepublishPlanApprovalPendingandusePlanApprovalPending. They are kept off theuseAssistantsnapshot, like the bus's two event channels, so a plan's state does not re-render every snapshot consumer. Deleting a conversation from the/aisidebar (useConversationList.remove) drops its reading.ConsoleChatbotFab(app pages and Home) andChatDockLauncher(Studio's collapsed dock). While a reading is pending they render a small amber dot. Its text, the new keyconsole.ai.dock.planAwaitingApproval, is wired as the button's accessible description througharia-describedby, since a button's children are presentational and a name on the dot would reach no assistive tech. With no reading pending they render nothing, and the button carries noaria-describedby.packages/plugin-chatbot/README.mddocuments the prop.Why it is built this way (measured, not assumed)
ChatDockPanelrenders only whiledock.expanded.ConsoleLayoutrenders the FAB only while!dock.expanded, and Studio rendersChatDockLauncheronly when collapsed on desktop. So no chat code is mounted to ask at the moment the marker is visible, and the reading has to outlive the chat. The bus is a module singleton that only a mounted chat writes. Every change to a plan's state in the document happens inside a mounted chat: the approval click, a newer proposal, and the build that runs.useChatConversationholds is a cache mirror, not the plan's state. ItsCachedProposedPlanis the serialization mirror for the localStorage message cache. The plan's state is derived only insideChatbotEnhanced, frombuiltPlanIdsandapprovedPlanIds. Deriving it again in app-shell would have been a second producer of the same fact, which objectui#7254 ruled out. Hence the prop.@objectstack/spec17.6.0 has noblueprint_proposed,awaiting_confirmation, approved, rejected or superseded status. Its dist carries only the HITLpending_approvalenvelope. The console's declared states areProposalCardState(pending,in-progress,settled,failed), and the marker keys onpending.AuthProviderdoes not reload), a reading is shown only to the user it was read for.What it does not see (stated, not papered over)
Widened public surfaces (Clause-②)
@object-ui/plugin-chatbot: the exportedChatbotEnhancedPropstype gains one optional member,onPlanApprovalPendingChange, a callback taking one boolean and returning nothing. The card cannot be met without it: the only producer of the plan's state is internal toChatbotEnhanced.@object-ui/i18n: one new key,console.ai.dock.planAwaitingApproval, in every built-in pack. Theenpack is exported from the package entry, so the key also joins the translation-key type derived from it.@object-ui/app-shelladds no export.publishPlanApprovalPendingandusePlanApprovalPendingship insidedist/but are not on the package entry, and the exportedassistantBusobject andAssistantSnapshottype are unchanged. Changeset:.changeset/11666-launcher-plan-awaiting-approval.md,minorfor all three packages.Evidence
All readings below were taken on the merged head
f678767(the branch after one merge commit oforigin/mainata600924) unless a line names another sha. Each gate's own verdict line is quoted.Pins (3 new files, 33 tests).
pnpm exec vitest run --reporter=verboseover the three files, onf678767:Test Files 3 passed (3),Tests 33 passed (33).ChatbotEnhanced.planApprovalPending-11666.test.tsx(plugin-chatbot) pins the producer through the real mapper (uiMessagesToChatMessages) and real envelopes. It covers: pending while "Build it" shows; false with no plan; false on the approve click; still true after anawaiting_confirmationapply (objectui#8343) and false once the build ran; following the newest plan; an older superseded plan not keeping it alive; and one announcement per change.launcherPlanApprovalMarker-11666.test.tsx(app-shell layout) pins both launchers in both states: no marker and noaria-describedbywith no reading; the marker plus accessible descriptionA proposed plan is waiting for your approvalwith one; the zh pack's text under a zh UI; cleared when the reading turns false; not cleared by another thread's reading; and never shown for another user's reading. It also checks that every one of the ten packs names the key, and that no non-en pack copies the English text.AiChatPage.planApprovalReachesLaunchers-11666.test.tsx(app-shell) renders the realChatPaneand the real FAB. It covers: the marker outliving the pane's unmount; a reopen clearing nothing until the chat reads false; another thread not clearing it; and a/aisidebar delete dropping it.Affected suites.
pnpm exec vitest run --maxWorkers=2overpackages/app-shell/src/layout/,packages/app-shell/src/assistant/, the wiring pin,packages/plugin-chatbot/src/andpackages/i18n/src/printedTest Files 185 passed (185)andTests 2235 passed | 13 skipped (2248), identical onf678767and on the pre-merge4828725.Type-check, lint, build.
turbo run build --filter='@object-ui/app-shell^...' --concurrency=2, 29 packages, exit 0.pnpm --filter @object-ui/i18n type-check,@object-ui/plugin-chatbot type-checkand@object-ui/app-shell type-check: exit 0 each, onf678767.pnpm --filter @object-ui/app-shell build: exit 0,dist completeness: 1 package(s) complete (988 emitted files verified).lintfor app-shell, plugin-chatbot and i18n: exit 0 on4828725, with0 errorseach. Per-file eslint--format jsonon the six edited source files gives identical warning counts to the basef9f4a62, and zero findings on the three new test files.Eager-closure budget (
node scripts/check-eager-closure-budget.mjs, after a consolevite build):f9f4a62:Console eager closure is 3319.2 KB gzipped across 331 of 2451 chunks (budget: 3330.4 KB, headroom: 11.2 KB). The report'seagerGzipBytesis 3398847.19865e0: 3399427 bytes, +580, over the same 331 of 2451 chunks.f678767:Console eager closure is 3319.7 KB gzipped across 331 of 2451 chunks (budget: 3330.4 KB, headroom: 10.6 KB), 3399421 bytes.assistantBuschunk +192, the entryindexchunk +282,plugin-chatbot+96, andi18n-locale-en+21 (that chunk's headroom moves from 5.5 KB to 5.4 KB). No chunk joins or leaves the eager set.The FAB still imports no chat code.
ConsoleChatbotFab.tsximportsreact,../assistant/assistantBus.jsand@object-ui/i18n.assistantBus.tsimportsreactand@object-ui/auth. In the built console, theassistantBuschunk's imports arerolldown-runtime,vendor-reactandinfrastructure, the chunk that carries@object-ui/authand was already in the eager set at base. It has no edge to theplugin-chatbotchunk.plugin-chatbotwas already an eager chunk at base (162.5 KB gzipped), reached throughConsoleLayout's staticChatDockimport anduseAiSurfaceEnabled'suseAgents.Ablation, one leg per added behaviour. Run on the committed
19865e0through objectstack'sscripts/ablation-replace.mjs, which mutates through an anchor that must hit exactly once, proves the write on disk (anchor count and blob hash), runs the pin and restores. Every leg went red in the expected direction:falseTests 5 failed - 2 passed (7)ChatPanepublishespending: falseTests 4 failed (4){planPending ? (to{false ? (Tests 4 failed - 18 passed (22)(the four FAB cases)ChatDockLauncherTests 4 failed - 18 passed (22)(the four launcher cases)o === ownertoo === oTests 2 failed - 20 passed (22)(both other-user cases)Tests 1 failed - 3 passed (4)Every restore was proven by blob hash: each of the six files' on-disk hash equals its
HEADblob, andgit diff HEADwas empty afterwards.Repo
check:*gates touched by the diff. All exit 0 onf678767:i18n-keys,i18n-drift(0 en value(s) changed (1 key(s) added, 0 removed),i18n-dead-keys(the new key is not listed),control-bytes,new-line-citations(0 new citation(s)),vi-mock-specifiers,vi-mock-inherit,vi-mock-override-shape,test-path-roots,changeset-claims,pending-changeset-literals,phantom-deps,side-effects-array,unreferenced-sourcesandself-import.check-changeset-presenceandcheck-changeset-no-majoralso exit 0.NOT MEASURED:
check:readme-exports. Reason: it reds on packages unbuilt in this worktree (type entry ./dist/index.d.ts is not on disk), a prerequisite and not a judgment. It reports0 wrong-path, 0 fabricated, and the README edit adds prose only, no code block.NOT MEASURED: live console. Reason: the producer of
blueprint_proposed(the build agent'spropose_blueprint) is not in objectstackmain(git grep blueprint_proposed origin/mainreturns nothing there), so no local stack can propose a blueprint. The unit pins are the binding reading.CI runs the full farm.
Acceptance notes
Observations, not filed (no public-door measurement):
ConsoleLayout.tsx's import comment says the chat chunk graph (plugin-chatbot and the rest) "only downloads on first hover/click". The build reads otherwise: theplugin-chatbotchunk is in the eager closure at base. It is a stale comment, not touched here. Carrier: none.Session:
https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsALGenerated by Claude Code