Repository navigation
fix(plugin-chatbot): plan card actions wait until no turn is in flight (objectui#10925) - #10952
Conversation
The proposed-plan card renders as soon as propose_blueprint returns, but the proposing turn keeps streaming (a todo_write, the closing prose) while the server stores each step. Build it clicked in that window sent the next turn before the previous one was stored, and the stored history interleaved the two turns. Build it, Adjust and the one-click answer chips, on the structured and the fallback plan card, are now disabled while isLoading is true: the signal the composer already uses, which falls only once the response stream has been read to its end. A message's own streaming flag is derived from it for the trailing assistant message only, so it would miss the submitted phase and a newer turn streaming below an older card. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1
|
changeset-claim-re-read
|
✅ 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
|
Contract reviewServed-tier: Inputs: card objectui#10925 (body and its 3 comments: serial note 5866915376, claim 5867755959, os-dev-report 5868137217); PR #10952 body, file list and ① Derived judgmentsThe diff:
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #10925
Clause-②: no
Dispatched by the
domain:uiseat 4 PM. Sessionhttps://claude.ai/code/session_015AUunPkX7UTkCH9e7AdZo1, branchclaude/issue-10925-plan-card-wait-stream, base5c94589.What changed
The proposed-plan card's actions are disabled while
isLoadingis true. That covers Build it, Adjust and the one-click answer chips, on the structured plan card and on the fallback confirm card (apropose_blueprintwhose result did not parse into a plan). The buttons carry the nativedisabledattribute and thedisabled:cursor-not-allowed disabled:opacity-50styling the builder-handoff button already uses. No new copy.isLoadingis the signal that already turns the composer's send into a stop button. TheisLoadingprop's JSDoc now says that it also holds the plan card.The diff is one source file (
packages/plugin-chatbot/src/ChatbotEnhanced.tsx), one new test and onepatchchangeset.AiChatPage.tsxis not touched, because it already passesisLoadingfromuseObjectChat.The signal, measured
The dispatch assumed the owning message's
streamingflag is the "turn still streaming" signal, to be verified against persistence of the turn's tail.useObjectChatsetsisLoadingfrom the AI SDK status (submittedorstreaming).uiMessagesToChatMessagessetsstreamingfrom that same value, on the trailing assistant message only. In the installedai@7.0.65,makeRequestsets the status back toreadyonly afterconsumeStreamhas read the response to its end.origin/main2611cdd,AIService.streamChatWithToolsawaits the store of each tool result before the next round. It awaits the store of the final reply beforeyield finishPart(result).ObjectQLConversationService.addMessageawaitsengine.insert, and the agent chat route streams that generator. So neither signal drops before the proposing turn's tail is persisted, and no timer is involved.isLoading, notmessage.streaming. The per-message flag is a subset ofisLoading, so it covers the proposing turn the same way. It misses two windows that overlap turns in the same way. One is thesubmittedphase before the first chunk, when no message carries the flag. The other is a newer turn streaming below an older plan card. Case 5 below pins both windows.Tests
All runs below are on HEAD
847cb69, with a clean tree.New pin
ChatbotEnhanced.planActionsWaitForTurn-10925.test.tsxbeside the component has five cases. Each feeds the wire shape through the real mapper, with the flagsuseObjectChatderives from a status.streaming: Build it and Adjust are disabled. A click sends nothing and does not flip the card to Building.streaming, thenready: both are enabled, and Build it sendsplanApproveMessageexactly once.submittedand thenstreaming(its own message is not streaming): disabled. Afterready: enabled.node ../objectstack/scripts/ablation-replace.mjsreplacedconst planActionsLocked = isLoading;withconst planActionsLocked = false;(anchor 1 to 0, blob1aa9b4bd0f9etoae9939a163a9). Result:Tests 5 failed (5), each attoBeDisabled()("Received element is not disabled"). Restore: blob equal to HEAD1aa9b4bd0f9e, andgit diff HEADempty. The test imports the component from source by a relative path, so the ablation needs no build and nodist/leg.pnpm exec vitest run --maxWorkers=2 packages/plugin-chatbot/gaveTest Files 52 passed (52),Tests 553 passed (553).git ls-files packages/plugin-chatbotlists 52 test files.pnpm --filter @object-ui/plugin-chatbot type-checkexited 0. It ran after the dependency closure was built withpnpm --workspace-concurrency=2 --filter '@object-ui/plugin-chatbot^...' build, with 9 projects in scope.tsc -p tsconfig.test.json --listFileslists the new test file.eslint --no-inline-config --format jsonover the 2 touched.ts/.tsxfiles. The JSON output lists 2 files.ChatbotEnhanced.tsxhas 0 errors and 10 warnings at HEAD, and 0 errors and 10 warnings at base5c94589, from the same three rules. The new test has 0 and 0. Population: thefiles: ['**/*.{ts,tsx}']blocks ofeslint.config.jscover both files. Invariance:eslint.config.jsenables no type-aware linting (noproject,projectServiceor type-checked preset), so this diff cannot move the verdict on any untouched file. The repo-widepnpm lintbelongs to CI.check-changeset-presence(1 changeset for 1 released package),check-changeset-no-major,check-changeset-overwrite,check-changeset-claims,check-changeset-fixed,check-pending-changeset-literals,check-new-cross-file-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-lint-coverage,check-i18n-call-site-keys,check-phantom-dependencies,check-package-self-import,check-unreferenced-sources,check-handler-key-read-sites,check-type-check-coverage.check-governed-queue-guard --teston the three paths reports NOT GOVERNED.isLoadingJSDoc. The one plan-card consumer test in app-shell (AiChatPage.planCardLocale.test.tsx) renders withisLoading: false.Acceptance notes
useObjectChatmaps from.proposed-changes) sendschangesConfirmMessagefrom a button that stays live while its proposing turn streams. A one-off probe, not kept, rendered anupdate_metadatachanges_proposedresult on the streaming tail withisLoadingtrue:proposed-changes-confirmwas enabled, and a click sent the message. The same guard would close it. It was not observed through a public door, so it is not filed. Carrier: none.check-changeset-claims(report-only) names.changeset/6687-chatbot-surface-authorable.mdas describingChatbotEnhanced.tsx. I read it: its paragraph is about thesurfaceprop andisPlainSurface, which this diff does not touch, so it stays true.Generated by Claude Code