Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

fix(plugin-chatbot): render an answer's text and charts in stream order (objectui#13492) - #13500

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-49319q
Oct 25, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-49319q

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #13492

Clause-②: no
Metadata-protocol: none

What changed

An assistant answer now renders its text and its charts in the order the stream produced them. A chart the model drew before writing its analysis shows first, so an open-ended analysis in the console's assistant panel puts its chart on the first screen (the acceptance objectstack-ai/cloud#3072 sets). Before, mapMessages joined every text part into one content and lifted every data-chart part into one trailing charts[], and ChatbotEnhanced rendered the whole text first, so the chart always landed below every paragraph.

  • mapMessages.ts: extractCharts now records, on each chart, where it arrived among the text parts: textOffset = the length of the text that extractText joined before it. Tool, step and reasoning parts add nothing. Only when content IS that join; a legacy string content is not built from the parts, so its charts carry no offset.
  • ChatbotEnhanced.tsx: a new optional ChatChart.textOffset. layoutAnswer splits content at the offsets into text runs and chart runs; each text run renders through its own MessageResponse (so long-answer folding and record links work in every run), and each chart run through the same chart markup as before, now in AnswerCharts. The streaming caret rides the last text run, where it sat before. Charts render in the same tree position whether or not prose has arrived, so a chart drawn before the first word keeps its mounted node when the text streams in.
  • Backward-compatible: content and charts keep their shape. A chart with no textOffset (a host-built message, the legacy string-content shape) or one past the end of the text trails all of it, which is exactly the old layout; a message with no charts renders the same DOM as before. textOffset is a placement key and never reaches the chart schema handed to SchemaRenderer.

The card's acceptance, item by item

Acceptance Pinned by
parts data-chart, text: the chart renders before the text data-chart then text: the chart renders before the analysis
parts text, data-chart, text: the three in that order text, data-chart, text: the three render in that order
a re-hydrated stored message keeps the order useObjectChat.chartOrderRehydrate-13492.test.tsx (API mode, stored message with parts through the hook and into the bubble) and re-mapping a restored message (same parts) yields the same placement
long-answer folding (objectui#13032) does not regress objectui#13032 does not regress: the long analysis after a chart still folds once the stream ends, plus the unchanged chatAnswerLayout-13032.test.tsx
record links (objectui#13037) do not regress objectui#13037 does not regress: a record link in the text after a chart opens through the host opener, plus the unchanged chatRecordLink-13037.test.tsx
staging re-test with 「帮我分析一下客户」 NOT MEASURED: staging reaches this through cloud's objectui pin; re-run after the pin moves past this merge

New files: mapMessages.chartOrder-13492.test.ts (6 tests), ChatbotEnhanced.chartOrder-13492.test.tsx (7, one a CONTROL for the old layout), useObjectChat.chartOrderRehydrate-13492.test.tsx (2). mapMessages.test.ts gains the textOffset in the one test that pins a chart's exact shape.

Verification (local, head cb0af96)

All runs from the repo root under the shared verify lock.

  • pnpm exec vitest run --maxWorkers=2 packages/plugin-chatbot/: Test Files 82 passed (82), Tests 787 passed (787).
  • The 54 test files in packages/app-shell/src and apps/console/src that import @object-ui/plugin-chatbot (resolved to its src by the root config's alias): Test Files 54 passed (54), Tests 582 passed (582).
  • pnpm turbo run type-check --filter=@object-ui/plugin-chatbot (builds the dependency closure, then tsc --noEmit && tsc -p tsconfig.test.json): exit 0 after one fix to the new re-hydrate test (it now hands the hook's messages through toRuntimeMessages, as hosts do).
  • eslint --no-inline-config on the six touched source and test files: 0 errors; the 11 warnings are all on lines this PR does not touch.
  • node scripts/check-changeset-presence.mjs and node scripts/check-changeset-no-major.mjs: pass. pnpm check:new-line-citations: 0 new citations. check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:test-path-roots, check:control-bytes, check:ui-root-lookups, check:unreferenced-sources: all exit 0.
  • Reverse verification, both through ablation-replace.mjs with the fix committed and restore proven (blob equals HEAD, git diff HEAD empty):
    • renderer: layoutAnswer ignores textOffset (every chart trails the text) ⇒ 6 of the 9 render and re-hydrate tests turn red; the record-link test, the old-layout CONTROL and the mapper tests stay green, as they should.
    • mapper: the textOffset spread deleted ⇒ 13 tests red across the four files, including the existing exact-shape test; the legacy no-offset test stays green.
  • NOT MEASURED locally, left to CI: the sharded full pnpm test, repo-wide pnpm lint, builds and the remaining check:* gates.

Acceptance notes

  • No reload path in this repository restores a chart today, so on reload the chart is missing, not misplaced. The server keeps a conversation as Vercel ModelMessages (IAIConversationService.addMessage in @objectstack/spec's contracts/ai-service.ts), which have no data parts. On the way back, app-shell's hydratedMessagesToChatMessages builds content and tool invocations only, and sanitizeChatMessagesForCache writes text and tool parts only. Read from code, not measured on staging; outside this card's file surface (packages/plugin-chatbot/src/**). A stored message that does carry its data-chart parts keeps its order (pinned above). Whether cloud's visualize_data result carries enough to re-derive the chart on reload is a cloud-side reading this session cannot reach.

  • In an answer that interleaves text and charts, each text run folds on its own (a run taller than two viewport heights folds; a short intro above the chart does not). An answer with no chart, or with its charts after all the text, is one run and folds exactly as before.

  • Docs: neither packages/plugin-chatbot/README.md nor content/docs/plugins/plugin-chatbot.mdx describes where charts render, so nothing there went stale; textOffset is documented on the ChatChart type.

  • Adds no product-specific widget, dialog, hook or string (see AGENTS.md › Product-neutral renderer)

Card thread session: session_01VWbYRWtBm3f8J2T8HAt97o, claimed by the domain:ui seat 4 (claim comment 6393183623).

🤖 Generated with Claude Code

https://claude.ai/code/session_01VWbYRWtBm3f8J2T8HAt97o


Generated by Claude Code

mapMessages joined every text part into one `content` and lifted every
`data-chart` part into one trailing `charts[]`, so a chart the model drew
before writing its analysis still rendered below all of it. Each chart now
records where it arrived among the text parts (`textOffset`), and the bubble
splits the answer at those points. A message without offsets keeps the old
layout.

Claude-Session: https://claude.ai/code/session_01VWbYRWtBm3f8J2T8HAt97o
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 85 chunks) 3039.1 KB 3496.0 KB
Main entry chunk (gzip) 80.8 KB 350 KB
Entry file index-BzjtUHw-.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 (buildIdentity.js) 0.80KB 0.49KB
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 21.41KB 7.90KB
app-shell (runtime-config.js) 24.52KB 8.48KB
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) 45.59KB 12.03KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 20.67KB 6.12KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.74KB 3.94KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.27KB 1.86KB
auth (createAuthClient.js) 42.84KB 11.66KB
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) 8.84KB 3.61KB
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.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
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) 615.22KB 150.62KB
core (index.js) 10.51KB 4.16KB
create-plugin (index.js) 27.92KB 9.50KB
data-objectstack (index.js) 257.46KB 72.08KB
fields (index.js) 296.40KB 75.98KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
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.52KB 2.39KB
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) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.17KB 11.96KB
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) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (ServerReadScope.js) 4.03KB 1.82KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.99KB 0.43KB
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.01KB 3.91KB
plugin-calendar (index.js) 55.31KB 16.16KB
plugin-charts (index.js) 88.30KB 24.33KB
plugin-chatbot (core.js) 5.22KB 2.18KB
plugin-chatbot (index.js) 238.98KB 56.69KB
plugin-dashboard (index.js) 147.15KB 39.95KB
plugin-designer (index.js) 232.83KB 50.01KB
plugin-detail (index.js) 268.92KB 71.56KB
plugin-editor (index.js) 2.21KB 1.04KB
plugin-form (index.js) 185.46KB 48.48KB
plugin-gantt (index.js) 178.19KB 45.06KB
plugin-grid (index.js) 277.83KB 77.44KB
plugin-kanban (index.js) 64.27KB 19.81KB
plugin-list (index.js) 149.51KB 39.15KB
plugin-map (index.js) 27.54KB 9.10KB
plugin-markdown (index.js) 13.79KB 4.79KB
plugin-report (index.js) 46.42KB 13.06KB
plugin-timeline (index.js) 39.74KB 12.00KB
plugin-tree (index.js) 15.25KB 5.38KB
plugin-view (index.js) 93.37KB 24.00KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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) 129.13KB 42.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.71KB 2.11KB
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.07KB 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.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
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 (product-blocks.js) 0.20KB 0.19KB
types (record-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.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.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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 #13500 @ cb0af9656b697e19145c7d2bc3c8f773dc6300eb · domain:ui#4 · 2026-10-25T04:27Z

Reviewer of record: session_01XWGTsmqrtrt1Zqh1ioWqsT (seat 4). Implemented-by: session_01VWbYRWtBm3f8J2T8HAt97o on claude/project-thread-49319q. Card objectui#13492 (p1), claim 6393183623, report 6393359237.

  1. Shape: draft, base main, Fixes #13492. Seven files (+548 −55), all under packages/plugin-chatbot/src/** plus one changeset, inside the claim.
  2. Card against diff:
    • Mapper: extractCharts now counts the text parts it walks past and stamps each chart with textOffset. That count matches extractText's join('') exactly, so an offset is an index into content. It is set only when content is built from the parts (anchored: typeof msg.content !== 'string'); a legacy string content gets no offset.
    • Render: layoutAnswer splits content at the offsets. It clamps offsets to [0, content.length], uses a stable sort, merges charts at the same offset into one block, and drops whitespace-only slices. A chart with no offset trails all the text, which is the old layout byte for byte, so a host that builds ChatMessage itself is unaffected.
    • Backward compatibility: content and charts keep their shapes; textOffset is optional and never reaches the <chart> schema. The claim's backward-compatibility line holds.
    • Non-regression: the streaming cursor rides the last text run. RecordLinkContext wraps every run (chat: a record link in an assistant answer opens that record inside the console (today every link is an external-link dialog and a new tab) #13037). Each run is its own MessageResponse, so a long answer folds per run once streaming ends (chat: an assistant answer in the side panel — headings at chat size, wide tables fit the narrow panel, long answers fold, the scroll-to-bottom button never covers text #13032); both are pinned in ChatbotEnhanced.chartOrder-13492. The tool-call placeholder branch is reachable only when there is no visible prose, as before.
    • The card's 验收: chart then text renders the chart first, and text / chart / text keeps that order (render and mapper pins). The re-hydrate pin runs a stored API-mode message with parts through useObjectChat into the bubble. Ablations (renderer ignores the offset; mapper drops it) turned 6 and 13 tests red respectively.
  3. Gates on this head: 33 check-runs success and 3 skipped (coverage jobs, dependabot). mergeable_state: behind only because main moved; the merge queue rebuilds on current main. Locally: plugin-chatbot 82 files / 787 tests, and the 54 app-shell + console files importing it (582 tests), are green. Type-check and eslint pass (0 errors).
  4. Accepted deviations: none.
  5. Out of scope:
    • Charts after a reload, filed separately. The dev read that no objectui reload path restores a chart at all. The server persists ModelMessage with no data parts; app-shell's hydratedMessagesToChatMessages and the chat cache sanitizer keep only text and tool parts. So after a reload the chart is missing, not misplaced. The card's re-hydrate line is met for messages that keep their parts.
    • Staging re-test: NOT MEASURED. It runs after cloud's pin moves past this merge, on cloud#3072.
  6. Landing: not governed. Changeset: @object-ui/plugin-chatbot patch, Clause-② no / Metadata-protocol none; it matches the diff. The maintainer's standing landing word was given in this seat's project thread at 2026-10-25T01:48Z, so the seat lands it.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 25, 2026 04:28
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 25, 2026 04:28
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 25, 2026
Merged via the queue into main with commit 76c560e Oct 25, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-49319q branch October 25, 2026 04:37
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-chatbot: 助手回答里的图表总排在全部文字之后,模型先出图也到不了首屏(cloud#3072)

2 participants