Skip to content

feat(app-shell,plugin-chatbot,i18n): launchers mark a proposed plan awaiting approval (objectui#11666) - #11671

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11666-launcher-pending-blueprint
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11666-launcher-pending-blueprint

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

  • The reading comes from the plan card itself. ChatbotEnhanced (@object-ui/plugin-chatbot) takes a new optional prop, onPlanApprovalPendingChange. It reports whether the thread's NEWEST proposed plan is still pending according to resolveProposalCardState, 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.
  • The chat pane publishes it on the assistant bus. ChatPane (app-shell) publishes the reading per conversation and per signed-in user. The new bus functions are publishPlanApprovalPending and usePlanApprovalPending. They are kept off the useAssistant snapshot, like the bus's two event channels, so a plan's state does not re-render every snapshot consumer. Deleting a conversation from the /ai sidebar (useConversationList.remove) drops its reading.
  • Both launchers read the bus. These are ConsoleChatbotFab (app pages and Home) and ChatDockLauncher (Studio's collapsed dock). While a reading is pending they render a small amber dot. Its text, the new key console.ai.dock.planAwaitingApproval, is wired as the button's accessible description through aria-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 no aria-describedby.
  • i18n: the key is added to all ten built-in packs.
  • Docs: a paragraph in packages/plugin-chatbot/README.md documents the prop.

Why it is built this way (measured, not assumed)

  • The chat is not mounted when a launcher shows. ChatDockPanel renders only while dock.expanded. ConsoleLayout renders the FAB only while !dock.expanded, and Studio renders ChatDockLauncher only 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.
  • What useChatConversation holds is a cache mirror, not the plan's state. Its CachedProposedPlan is the serialization mirror for the localStorage message cache. The plan's state is derived only inside ChatbotEnhanced, from builtPlanIds and approvedPlanIds. Deriving it again in app-shell would have been a second producer of the same fact, which objectui#7254 ruled out. Hence the prop.
  • The spec declares no plan status. @objectstack/spec 17.6.0 has no blueprint_proposed, awaiting_confirmation, approved, rejected or superseded status. Its dist carries only the HITL pending_approval envelope. The console's declared states are ProposalCardState (pending, in-progress, settled, failed), and the marker keys on pending.
  • Keyed by conversation and owner. Opening a chat on another thread does not clear a plan waiting in this one. Because the SPA keeps running across a sign-out (AuthProvider does not reload), a reading is shown only to the user it was read for.

What it does not see (stated, not papered over)

  • A page reload starts the bus empty. A waiting plan shows no marker until a chat on that conversation is opened again. The durable copy is the server conversation. Reading it from the launcher would need the conversation's scope (agent resolution), its messages, and the chat's plan-state derivation, which is the chat graph this card keeps out of the launcher. See Acceptance notes.
  • A decision made in another tab or on another device is not seen until this tab's chat re-reads that thread.
  • "Reject" has no declared state. The plan card has no reject action: "Adjust" only focuses the composer. A plan the user declines in words keeps its "Build it", and the marker mirrors the card. Keying on a guess at the user's words is what the dispatch ruled out. A rejection that leads to a newer proposal does move the marker, onto that proposal.

Widened public surfaces (Clause-②)

  1. @object-ui/plugin-chatbot: the exported ChatbotEnhancedProps type 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 to ChatbotEnhanced.
  2. @object-ui/i18n: one new key, console.ai.dock.planAwaitingApproval, in every built-in pack. The en pack is exported from the package entry, so the key also joins the translation-key type derived from it.

@object-ui/app-shell adds no export. publishPlanApprovalPending and usePlanApprovalPending ship inside dist/ but are not on the package entry, and the exported assistantBus object and AssistantSnapshot type are unchanged. Changeset: .changeset/11666-launcher-plan-awaiting-approval.md, minor for all three packages.

Evidence

All readings below were taken on the merged head f678767 (the branch after one merge commit of origin/main at a600924) 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=verbose over the three files, on f678767: 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 an awaiting_confirmation apply (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 no aria-describedby with no reading; the marker plus accessible description A proposed plan is waiting for your approval with 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 real ChatPane and 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 /ai sidebar delete dropping it.
  • Affected suites. pnpm exec vitest run --maxWorkers=2 over packages/app-shell/src/layout/, packages/app-shell/src/assistant/, the wiring pin, packages/plugin-chatbot/src/ and packages/i18n/src/ printed Test Files 185 passed (185) and Tests 2235 passed | 13 skipped (2248), identical on f678767 and on the pre-merge 4828725.

  • Type-check, lint, build.

    • The dependency closure was built first: 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-check and @object-ui/app-shell type-check: exit 0 each, on f678767.
    • pnpm --filter @object-ui/app-shell build: exit 0, dist completeness: 1 package(s) complete (988 emitted files verified).
    • lint for app-shell, plugin-chatbot and i18n: exit 0 on 4828725, with 0 errors each. Per-file eslint --format json on the six edited source files gives identical warning counts to the base f9f4a62, and zero findings on the three new test files.
  • Eager-closure budget (node scripts/check-eager-closure-budget.mjs, after a console vite build):

    • base f9f4a62: Console eager closure is 3319.2 KB gzipped across 331 of 2451 chunks (budget: 3330.4 KB, headroom: 11.2 KB). The report's eagerGzipBytes is 3398847.
    • this branch before the merge, 19865e0: 3399427 bytes, +580, over the same 331 of 2451 chunks.
    • merged head f678767: Console eager closure is 3319.7 KB gzipped across 331 of 2451 chunks (budget: 3330.4 KB, headroom: 10.6 KB), 3399421 bytes.
    • Where the +580 sits: the assistantBus chunk +192, the entry index chunk +282, plugin-chatbot +96, and i18n-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.tsx imports react, ../assistant/assistantBus.js and @object-ui/i18n. assistantBus.ts imports react and @object-ui/auth. In the built console, the assistantBus chunk's imports are rolldown-runtime, vendor-react and infrastructure, the chunk that carries @object-ui/auth and was already in the eager set at base. It has no edge to the plugin-chatbot chunk.

    • Measured beside it, not caused by it: plugin-chatbot was already an eager chunk at base (162.5 KB gzipped), reached through ConsoleLayout's static ChatDock import and useAiSurfaceEnabled's useAgents.
  • Ablation, one leg per added behaviour. Run on the committed 19865e0 through objectstack's scripts/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:

    leg mutation pin file reading
    producer report constant false producer pin Tests 5 failed - 2 passed (7)
    publisher ChatPane publishes pending: false wiring pin Tests 4 failed (4)
    FAB marker {planPending ? ( to {false ? ( launcher pin Tests 4 failed - 18 passed (22) (the four FAB cases)
    dock-launcher marker same, in ChatDockLauncher launcher pin Tests 4 failed - 18 passed (22) (the four launcher cases)
    owner filter o === owner to o === o launcher pin Tests 2 failed - 20 passed (22) (both other-user cases)
    delete drops reading publish line deleted wiring pin Tests 1 failed - 3 passed (4)

    Every restore was proven by blob hash: each of the six files' on-disk hash equals its HEAD blob, and git diff HEAD was empty afterwards.

  • Repo check:* gates touched by the diff. All exit 0 on f678767: 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-sources and self-import. check-changeset-presence and check-changeset-no-major also 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 reports 0 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's propose_blueprint) is not in objectstack main (git grep blueprint_proposed origin/main returns 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):

  • Two unbuilt plan cards in one thread both keep a live "Build it". The older, superseded card is not made inert, the way only the latest "Open in Builder" hand-off stays actionable (objectui#2458 UX#5). Observed in the producer pin's superseded case. Carrier: none.
  • 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: the plugin-chatbot chunk is in the eager closure at base. It is a stale comment, not touched here. Carrier: none.
  • The marker's durability after a reload is the open question in the report. The candidate is a server-declared "awaiting approval" fact on the conversation, which the launcher host would read with the console's existing client.

Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL


Generated by Claude Code

claude added 3 commits October 6, 2026 02:56
…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>
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3319.7 KB 3330.4 KB
Main entry chunk (gzip) 152.6 KB 350 KB
Entry file index-DXjkX9qS.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.22KB 6.37KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 574.72KB 137.94KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 232.57KB 64.51KB
fields (index.js) 262.75KB 66.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.18KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.17KB 15.46KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.81KB 47.14KB
plugin-dashboard (index.js) 143.75KB 38.87KB
plugin-designer (index.js) 231.41KB 48.84KB
plugin-detail (index.js) 247.23KB 65.04KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.48KB 65.51KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.72KB 29.10KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.80KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT: PR objectui#11671, head f678767. It lands on an at-tier contract-review PASS on this head and all-green checks

domain:ui execution seat 1 @ objectui · session_015W8GBu6sBiqus2L2xjMsAL (os-steve) · 2026-10-06T04:18Z. Reviewed against GitHub and origin/main, not against the report's prose (report 6009207558 on objectui#11666).

  • Shape.
    • The PR is a draft against main, and its assignee is os-steve. Its first line is Fixes #11666, the only line with a closing keyword next to an issue number. Clause-②: yes is on its own line.
    • needs:contract-review is on the PR, and the isolated at-tier review is owed on this head.
    • The diff is 21 files, +743/−4: source +192/−4, three new pin files, ten locale packs +11, the plugin-chatbot README +8, and the changeset.
  • The file surface, extended, accepted by the seat.
    • The claim named the launchers, "the light signal path" (examples assistantBus.ts, useChatConversation.ts), the locale packs and tests. The measured path runs through three more files: plugin-chatbot/src/ChatbotEnhanced.tsx (the only producer of the plan state), console/ai/AiChatPage.tsx (ChatPane, the host that publishes) and hooks/useConversationList.ts (one line, so a deleted thread drops its reading).
    • The dev declared the breach in the report rather than stopping. The seat accepts it.
      • The dispatch allowed a new prop where the card "cannot be met without it".
      • The plan state is derived only inside ChatbotEnhanced (resolveProposalCardState over builtPlanIds and approvedPlanIds), so re-deriving it in app-shell would be a second producer (objectui#7254).
      • useChatConversation's proposedPlan turned out to be a cache mirror, not the state, and is untouched.
    • AiChatPage.tsx is also objectui#11667's surface (in flight). Whichever lands second merges main and resolves it in a merge commit.
  • Published surface, as the dev names it.
    • ChatbotEnhancedProps.onPlanApprovalPendingChange?: (pending: boolean) => void is optional, on @object-ui/plugin-chatbot.
    • One i18n key, console.ai.dock.planAwaitingApproval, is added in all ten packs.
    • The new assistantBus functions are off app-shell's entry.
    • The contract review judges each of these.
  • The mechanism, read in the diff.
    • ChatbotEnhanced reads the newest plan card through the same resolveProposalCardState the card renders, and reports pending through the prop, once on mount and on each change.
    • ChatPane publishes it on a module store in assistantBus, keyed by conversation and signed-in user, and kept off the snapshot bus. The launchers read it through usePlanApprovalPending() and import no chat code.
    • Opening a chat clears nothing, and opening another thread clears nothing. A deleted thread drops its reading.
  • Eager closure. It grows by +580 gzipped bytes (3398847 → 3399427; 3399421 on the merged head), across the same 331 of 2451 chunks, leaving 10.6 KB of headroom. The bus's new useAuth import lands in the @object-ui/auth chunk, which was already eager at base.
  • Ablation. 6 legs, all red, each restore proven:
    • producer forced false (5 red);
    • host never publishes (4);
    • FAB marker off (4);
    • dock marker off (4);
    • owner filter off (2);
    • sidebar delete not dropping (1).
  • Live: NOT MEASURED. The propose_blueprint producer is not in objectstack main, so no blueprint can reach "awaiting approval" locally. The three pin files drive the real ChatPane → bus → real FAB path instead, including unmount, reopen, another thread and sidebar delete. This is stated in the PR.

The dev's two open questions, answered by the seat (technical scope; neither adds a capability):

  1. A reload, or a decision made in another tab or device: A, this card's in-document reading.
    • The card's ask is the user who closed the chat, and that holds: the reading outlives the chat's unmount.
    • A reload starts empty until a chat on that thread is opened, and the PR states this as a known limit.
    • Reload fidelity needs the AI service to declare "awaiting approval" on the conversation (the dev's option C). That is a contract change on a service outside objectui main, and so its own card if the maintainer wants it. ⛔ Not a client-side persisted guess (option B).
  2. "Disappears on reject": A, the marker mirrors the card.
    • The plan card has no reject action or state today ("Adjust" focuses the composer).
    • A newer plan supersedes the older one, and an approval or a build that ran ends the reading. All three are pinned.
    • An explicit Reject/Dismiss on the plan card would change that card's contract, so it would be its own product card, ⛔ not this one.

Left as noted, not filed (from the report):

  • Two unbuilt plan cards in one thread both keep a live "Build it". The superseded card is not made inert.
  • ConsoleLayout.tsx's comment says the chat chunk graph downloads only on first hover or click. The console build has the plugin-chatbot chunk in the eager closure at base, so the comment is stale.

Landing: after a conforming at-tier PASS on this head and all-green checks, needs:contract-review comes off, then ready, then auto-merge into the merge queue.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f678767fcc0b8901c8f5218b3b55b17e17a2fd7d
Local-runs: none

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 main at a600924 (the merge-base is main itself); the head's check-runs; the three touched packages' exports maps and entry files at the head; the unpacked @objectstack/spec 17.6.0 tarball. The seat's ACCEPT was judged, not adopted. Not read: the dispatch order.

Checks on the head: 43 check-runs, 40 success, 3 skipped (Test (coverage), dependabot, the coverage-shard matrix placeholder), 0 failure. Ten were in_progress at first read (the eight Test shards, Type Check, Spec Main Shape Gate); this record was rendered after all 43 completed. README Export Check is green here, which covers the dev's NOT MEASURED check:readme-exports.

① Derived judgments

  1. @object-ui/plugin-chatbot: ChatbotEnhancedProps gains onPlanApprovalPendingChange, optional, a callback taking one boolean and returning nothing. Published, widening, right. ChatbotEnhancedProps is re-exported by name from src/index.tsx, the one entry that exports["."] maps to dist/index.d.ts; so this is an accept-set widening of a published type, additive and optional, with no existing member changed. The behaviour matches the declaration: the reading is computed from the same producer the plan card renders (resolveProposalCardState with the same four inputs, replayOutcomeByProposalId, builtPlanIds, confirmedChangeIds, approvedPlanIds), the newest-plan scan uses the same predicate the card path uses (tool.proposedPlan || isUnstructuredBuildProposal(tool)), and the callback fires once on mount and on each change of the boolean through a ref, so a fresh function identity does not re-announce. Declared in the PR body, the changeset and the README.

  2. @object-ui/i18n: new key console.ai.dock.planAwaitingApproval in all ten packs. Published, widening, right; the dev's statement of the doors is incomplete. The entry re-exports en (export { default as en } from './locales/en.js') and TranslationKeys (typeof en in locales/en.ts, re-exported through i18n.ts), so the key joins the en object's type and the key type, as the dev states. Two further published doors that neither the PR body nor the changeset names: exports["./locales"] (builtInLocales and the ten named packs) and exports["./locales/*"] (each pack on its own). Every one of the ten packs is therefore a published surface in its own right, and each gains the key. The changeset's "every built-in locale pack gains one key" covers the fact, and the level is unchanged (minor). The nine non-en values are translations, not English copies, pinned per pack. Additive only: i18n-drift reads 1 key added, 0 removed.

  3. @object-ui/app-shell: publishPlanApprovalPending and usePlanApprovalPending are new named exports of src/assistant/assistantBus.ts. Not published, right as the dev states it. The package exports map has . (to dist/index.d.ts) and ./styles.css only; no subpath, no wildcard. src/index.ts carries no export *; its re-export from ./assistant/assistantBus.js is a named list (assistantBus, useAssistant, useRegisterAssistantEditor, requestAssistantOpen; types AssistantSnapshot, AssistantEditorContext, AssistantEditorField), and the two new functions are not on it. They are unreachable from the entry type graph and unaddressable through exports: shipped bytes, not a published accept-set. The exported assistantBus object and AssistantSnapshot type are unchanged against main; the new store is a separate module-level map with its own listener set, kept off the snapshot.

  4. @object-ui/app-shell: behaviour on published components, no type change. ConsoleLayout (exported) renders ConsoleChatbotFab while !dock.expanded; AiChatPage (exported) hosts ChatPane. The FAB and the Studio ChatDockLauncher now render an amber dot plus a visually hidden span while a reading is pending and set aria-describedby to it; otherwise nothing, and no attribute. ConsoleChatbotFabProps, ChatDockLauncherProps, ChatPane's props and useConversationList's options are unchanged. useConversationList.remove additionally publishes pending: false for the deleted id and now keys on userId as well. Right: no published type moves.

  5. @object-ui/app-shell dependencies: assistantBus.ts newly imports useAuth from @object-ui/auth, already a declared dependencies entry of app-shell; no new dependency or peer. useAuth outside a provider returns a safe default (user: null), so the launchers do not throw in a provider-less host, and the owner key '' then matches a reading published with userId: undefined. Right.

  6. Contract vocabulary: no @objectstack/spec field is invented or read. The 17.6.0 tarball names no plan status (blueprint_proposed, awaiting_confirmation, approved, rejected and superseded are absent; pending_approval appears once, in dist/contracts/index.d.ts, the HITL envelope). The marker keys on the console's own ProposalCardState (pending). No error code, no env var, no governed text (.claude/**, docs/adr/**, skills/**, AGENTS.md), no schema file. Right.

② Semver level

  • .changeset/11666-launcher-plan-awaiting-approval.md declares minor for @object-ui/app-shell, @object-ui/plugin-chatbot and @object-ui/i18n; all three are in the fixed group. It matches the diff: two additive widenings of published accept-sets (① 1, ① 2) and one source change with new user-visible behaviour and no export change (① 3, ① 4). minor is the right level for each; no major (Changeset Bump Policy green), no skip-changeset; Changeset Declaration, Changeset Claim Re-read and Changeset Fixed Group Check are green on the head. The changeset body carries its own Clause-②: yes (widening) paragraph naming both surfaces.
  • Clause-②: yes: on its own line, the second line of the PR body, and on the claim comment. Right: the diff widens two published accept-sets. The claim anticipated only the i18n key and asked the dev to state every other surface; the dev stated the prop. Nothing in the diff narrows, removes or retypes an existing member.

③ Boundary flags

Dev deviations (os-dev-report 6009207558):

  1. File-surface breach, declared rather than stopped on. packages/plugin-chatbot/src/ChatbotEnhanced.tsx (plus README and test), packages/app-shell/src/console/ai/AiChatPage.tsx (plus test) and packages/app-shell/src/hooks/useConversationList.ts lie outside the claim's named surface, which said "stop on breach". Answered, inside the seat's remit. The breach is substantive: it crosses into a third published package and is the source of the second widening. But the card carries no maintainer ruling on the surface (the file surface is the claim's own instruction, which the dispatching seat may waive, and did in ACCEPT 6009241489); the claim itself foresaw an unlisted surface and asked for it to be stated; and the alternative the dev rejected (deriving the state again in app-shell from the localStorage mirror in useChatConversation) would be a second producer of the fact that resolveProposalCardState was made the one producer of (the code comment at the card's own call cites objectui#7254). The widening is declared and judged in ① 1. Noted, not a contract matter: AiChatPage.tsx is also objectui#11667's in-flight surface, which the claim's serial-constraints line did not cover; it is not a SINGLE_CLAIM_PATHS path, so ordinary concurrency applies and the later lander merges main.
  2. One prop beyond the i18n keys. Judged in ① 1 and ②. Answered.
  3. "Disappears on reject" met only as far as a declared state exists. See open question 2. Answered.
  4. "A newer proposal" read as the newest plan superseding the older ones. Inside the card's own word "superseded"; the superseded-plan case and the newest-plan case are pinned in the producer pin. Answered.
  5. A reload starts the bus empty. See open question 1. Answered.
  6. Pre-PR merge of main at a600924. Procedural; the head is that merge commit and its merge-base with main is main itself, so the net diff is the 21 files judged here. Answered.
  7. Lock queue returned 99 four times, re-queued, nothing narrowed. Procedural. Answered.

Open questions, answered by the seat in 6009241489, judged here:

  • Q1, reload and cross-device durability, answered A (this card's in-document reading). Stays inside the card. The card's ask is "the launcher the user returns to" after they "closed the chat"; that case holds (the reading outlives ChatPane's unmount, pinned end to end through the real pane and the real FAB). The card names no reload case, and its own pointer to "the state" (useChatConversation.ts caching proposedPlan) was measured as a serialization mirror, not the state. Of the three options only A is inside this card: B is a drifting second copy of a fact that has one producer; C is a contract field on an objectstack service whose propose_blueprint producer the dev found absent from objectstack main, so it cannot be this repo's card. The residual is a disclosed narrowing of "Done when" bullet 1 to the in-document case (after a reload, or for a decision made in another tab or device, no marker shows until a chat on that thread re-reads), stated in the PR body, the changeset and the code comment; the PR's acceptance notes carry the C candidate for the maintainer. No maintainer decision is needed to land this head; whether reload fidelity is wanted is the maintainer's to raise as its own card.
  • Q2, "disappears on reject" with no reject state, answered A (the marker mirrors the card). Stays inside the card. The plan card's action set at the head is approve, adjust, building and built (proposed-plan-approve, proposed-plan-adjust, proposed-plan-building, proposed-plan-built); ProposalCardState is pending | in-progress | settled | failed; there is no rejected state to follow, and the card's rule is "the marker follows the plan's state". An explicit Reject would change the plan card's own contract, so it is its own card; C (reading a later user message as a rejection) is a string guess and is rightly refused. A rejection that produces a newer proposal moves the marker onto that proposal, pinned.

Two readings of the card worth naming, both holding:

  • "An accessible name in all locales" is delivered as the button's accessible description (aria-describedby pointing at the translated text), the button's name staying "Open assistant". The text assistive tech reaches is translated in all ten packs and pinned per pack and per launcher (toHaveAccessibleDescription); a name on a presentational child of a button would reach no assistive tech. Inside the card's intent.
  • "Live console: NOT MEASURED": no local stack can propose a blueprint, since the producer is not in objectstack main. The three pin files drive the real ChatPane, the bus and the real FAB end to end, and the eager-closure gate (Bundle Analysis; performance-budget comment 6009224402 reads 3319.7 KB of 3330.4 KB over 331 chunks) is green on the head, so the card's "the FAB still imports no chat code" is read off the check, not the prose.

Nothing escalates to the maintainer on this head.

Implemented-by: claude/issue-11666-launcher-pending-blueprint
Reviewed-by: session_015W8GBu6sBiqus2L2xjMsAL

VERDICT: PASS

Read at 2026-10-06T04:34Z, after the last of the head's in-progress check-runs completed.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

needs:contract-review removed: a conforming PASS is on record for this head.

  • Record: 6009402061 (## Contract review, Served-tier: CONTRACT_REVIEW_TIER, Local-runs: none, VERDICT PASS).
  • Judged head: f678767fcc0b8901c8f5218b3b55b17e17a2fd7d, the PR's head at this write.
  • Independence pair: Implemented-by: claude/issue-11666-launcher-pending-blueprint, Reviewed-by: session_015W8GBu6sBiqus2L2xjMsAL. That is an isolated reviewer at the review tier, adopted by the dispatching seat.
  • The record's one note, carried: @object-ui/i18n's exports["./locales"] and exports["./locales/*"] also publish each pack, which the PR body names only as "every built-in pack". That is an incomplete statement, not a FAIL, and the minor level is unchanged.
  • Checks on that head: 40 success and 3 expected skips (dependabot, the two coverage jobs), and 0 UNRECOGNISED / NOT APPLICABLE lines.
  • Governed: objectui check-governed-queue-guard.mjs --test answers NOT GOVERNED over all 21 paths.

The seat now marks this ready and arms auto-merge through the merge queue.

domain:ui execution seat 1 @ objectui · session_015W8GBu6sBiqus2L2xjMsAL · 2026-10-06T04:35Z.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 04:36
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 04:36
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit 848ba0e Oct 6, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11666-launcher-pending-blueprint branch October 6, 2026 04:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants