Skip to content

fix(plugin-chatbot,app-shell): keep an assistant answer's chart after a reload - #27396

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-1up1lx
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-1up1lx

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #25908

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:AI 助手回答里的图表,刷新页面后不再消失;从服务器历史和本地缓存恢复时都会按原位置画回来。
  • 为什么改:直播流里的图表是一次性的,服务器不存它;服务器存的是 visualize_data 工具结果,里面的 chart 字段就是同一份图表描述,之前没人读它。
  • 风险与回滚:只影响聊天消息的恢复路径;没有 chart 字段的旧结果照旧不画图;回滚即 revert 本 PR。
  • 先看:ui/packages/plugin-chatbot/src/mapMessages.ts(唯一读 chart 的地方与防重复画图规则)、ui/packages/app-shell/src/hooks/useChatConversation.ts(缓存写法)、ui/packages/plugin-chatbot/src/useObjectChat.ts(API 模式重建)。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-chatbot/src/__tests__/mapMessages.persistedChart-25908.test.ts packages/plugin-chatbot/src/__tests__/useObjectChat.persistedChart-25908.test.tsx packages/app-shell/src/console/ai/__tests__/AiChatPage.persistedChart-25908.test.ts,预期三个文件全绿。
  • 需要你做的:无,席位落地。

What changes

An assistant answer that drew a chart came back as text only after a reload. The live stream draws the chart from a transient data-chart part that the server never stores. What the server stores is the visualize_data tool result, one JSON string whose chart key holds the same descriptor the part carried (#26744, comment 6457382770). Nothing read that key back.

  • One reader (plugin-chatbot mapMessages.ts, chartFromToolResult). It reads chart off a visualize_data result through the existing parseResultEnvelope, and narrows it with the same chartFromDescriptor the data-chart part goes through, now shared by both. The top-level rows (a 20-row narration preview) are never drawn. A result stored before the producer wrote chart gives no chart, with no rebuild from its other keys.
  • Where it sits. extractCharts reads tool results only for a message that has no data-chart part. A live message carries both, so it still draws the chart once. A stored message carries only the result, so each chart takes the offset of its tool part among the text parts.
  • Hydration (app-shell hydratedMessagesToChatMessages) sets charts from chartsFromParts(message.parts). That walk covers both the server's visualize_data results and the cache's data-chart parts, and the offsets index the same text join the hydration makes content. The share page (SharedRecordPage) renders these messages directly, so it gets the chart too, with no change to its file.
  • API-mode round trip (useObjectChat aiInitialMessages). The rebuild used to write one text part and then the tools, so a hydrated charts key was dropped. It now writes chartAnswerParts(content, charts): the text cut at each chart's offset, with a data-chart part between the runs. The mapper reads that back as the same content and the same charts at the same offsets.
  • Cache (app-shell sanitizeChatMessagesForCache). It writes the same chartAnswerParts in place of the single text part, so a cache-fallback reload keeps the chart and its place. A tool-output arm would not have kept the place, because a tool part carries no position in the cache.
  • Where the fix sits, and why. The order offered two places: the rebuild emits data-chart parts, or mapMessages re-derives charts from the tool output. This PR takes the first for the round trip and the cache, because the live path already draws from data-chart parts and only position-encoded parts keep textOffset through a rebuild that joins text. The one reader of chart sits in mapMessages, and the hydration mapper reaches it through chartsFromParts.
  • Tool name. @objectstack/spec declares visualize_data only as a member of PLATFORM_TOOLS_BY_PACKAGE['service-ai'], with no constant of its own. mapMessages keeps one literal, VISUALIZE_DATA_TOOL (a module export, not a package export), and a test pins it to that registry.
  • New exports (Clause-②: yes (widening), claim 6487631480): chartsFromParts, chartAnswerParts and the ChartAnswerPart type, on @object-ui/plugin-chatbot and @object-ui/plugin-chatbot/core. app-shell is private and needs to call the one reader, so these exports are how it does.

Tests

  • plugin-chatbot mapMessages.persistedChart-25908.test.ts. A stored result yields the same chart the live part draws (title, series labels, all 30 rows rather than the 20-row preview), at the offset of its tool part. A bare string result is read too. A result without chart gives no chart and does not throw, and neither does a non-JSON one. Only visualize_data is read, and its name is pinned to the spec registry. A live message with both the part and the result draws one chart. chartAnswerParts maps back to the same message, and a chart without an offset follows the text.
  • plugin-chatbot useObjectChat.persistedChart-25908.test.tsx. In API mode, a hydrated answer keeps one chart at its offset through the store, and the bubble draws it once, between the two text runs.
  • app-shell AiChatPage.persistedChart-25908.test.ts. Server history restores the chart between the text it sat between, and a result without chart restores text and tool with no chart. A live answer is cached and restored with its chart in place, and a restored server answer survives being cached again.

Acceptance notes

  • The reach has never been measured in a running console, and the producer change (cloud abffd69) runs on hosted deploys only (@objectstack/service-ai 10.3.0 on npm predates it). The proof here is fixture-level, against the result shape cloud: state the persisted visualize_data result shape after cloud#3292 (chart descriptor key, row cap), so ui can redraw charts after a reload #26744 read on cloud main a32c80c.
  • The fixtures use the AI SDK text output wrapper ({ type: 'text', value }), the form a string tool result takes, and a bare string. A JSON-wrapped output ({ type: 'json', value }) with no status key resolves to the wrapper in the shared parseResultEnvelope, so it would give no chart. The producer returns a string, so that shape is not reached, and this PR does not change the shared parser.
  • Local mode (no api) keeps dropping hydrated render-only keys in normalizeMessages, charts and buildProgress alike, as before. AiChatPage runs local mode only with no chat backend, which has no stored conversations.

Verification

All readings below were taken on head e1f1c06a, branched from origin/main c4c515ad. The four commits main gained since touch none of this PR's files.

New pins and ablations
  • The three new files: Test Files 3 passed (3), Tests 14 passed (14).
  • Each ablation goes through scripts/ablation-replace.mjs, which confirms the mutation landed, restores the file and reads it back equal to the HEAD blob with an empty git diff HEAD. Every leg is a source-level mutation, because ui/vitest.config.mts aliases @object-ui/plugin-chatbot and its /core entry to src.
    • Reader (chartFromToolResult returns undefined): 3 tests fail, the two result-reading pins in mapMessages.persistedChart-25908 and the server-history pin in AiChatPage.persistedChart-25908.
    • API-mode rebuild (chartAnswerParts handed no charts in aiInitialMessages): 2 tests fail, both in useObjectChat.persistedChart-25908.
    • Cache writer (sanitizeChatMessagesForCache handed no charts): 2 tests fail, both cache-fallback pins in AiChatPage.persistedChart-25908.
Package suites and typecheck
  • pnpm --dir ui exec vitest run packages/plugin-chatbot/: Test Files 88 passed (88), Tests 828 passed (828).
  • pnpm --dir ui exec vitest run packages/app-shell/src/console/ai/ packages/app-shell/src/hooks/ packages/app-shell/src/layout/: Test Files 245 passed (245), Tests 1703 passed (1703).
  • The three SharedRecordPage test files in ui/apps/console/src/pages/: Test Files 3 passed (3), Tests 19 passed (19).
  • pnpm --filter @object-ui/plugin-chatbot run type-check && pnpm --filter @object-ui/app-shell run type-check: exit 0.
  • eslint --no-inline-config on the nine touched ts/tsx files: 0 errors. The warnings that remain sit on lines this PR does not touch.
  • pnpm --dir ui run check:new-line-citations: 0 new citations.
Derived gates (dispatch-gates --commands, reconciled with --ran: 35 derived, 35 run, 0 NOT-MEASURED)
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0

Families CI owns (the required checks) are cited from this PR's check runs at hand-back; they were not re-run locally.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No packages/spec change.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (eslint on the touched files: 0 errors; the repo-wide run is CI's.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: not applicable.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English, apart from the 维护者速读 block the dispatch order asks for.

Generated by Claude Code

… a reload

Read the chart a visualize_data result persisted under its chart key,
at the offset of its tool part, and carry hydrated charts back through
the API-mode store rebuild and the message cache as data-chart parts.

Claude-Session: https://claude.ai/code/session_01JQwxbYXpAEUq8s7bsevHLW
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 29, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#5 · os-warren · session_01JQwxbXGpVJHZTXXerMoY4C

Contract review

Served-tier: TIER_DEFAULT
Head-sha: e1f1c06a18895a98b4c4da7db1b168b5137db37b
Local-runs: none

The face is a declaration: Clause-②: yes (widening) appears on the claim (6487631480), the PR body and the changeset. The diff touches no published schema and no governed path.

① Derived judgments

  • Public surface grows by three names. The root entry (index.tsx) and the ./core entry (core.ts) of @object-ui/plugin-chatbot add chartsFromParts(parts), chartAnswerParts(content, charts) and the type ChartAnswerPart. That is additive and nothing is removed or renamed, so it is a widening. Correct.
  • Reachable types. Both functions take or return ChatChart and AnyPart, which already reach the entries through existing exports. ChartAnswerPart is new and exported. No other new type becomes reachable. Correct.
  • Not on any entry. VISUALIZE_DATA_TOOL is a module-level export of mapMessages.ts that neither entry re-exports. It is not public surface. Correct.
  • Acceptance set. No spec or metadata key changed, so Metadata-protocol: none is correct. The chart key read here is cloud's persisted tool result (cloud: state the persisted visualize_data result shape after cloud#3292 (chart descriptor key, row cap), so ui can redraw charts after a reload #26744, comment 6457382770), not an objectstack contract. The consumer reads exactly that one path, and it renders no chart when the key is absent.

② Semver level

Additive exports on a published package with no protocol kind ship as patch, under AGENTS.md's "a non-protocol change, widening or breaking, ships patch". The changeset .changeset/25908-chart-after-reload.md declares '@object-ui/plugin-chatbot': patch, and its body has both line-initial declarations. They match.

③ Boundary flags

  • The dev raised one flag: the Clause-② mismatch with the first claim. The superseding claim 6487631480 answered it.
  • The dev raised no open_questions beyond the seat-pinned literal visualize_data. Spec declares that name only inside PLATFORM_TOOLS_BY_PACKAGE['service-ai'], and a test pins the literal against that registry.
  • The PR author is objectstack-fleet[bot], through the relay.

Implemented-by: claude/project-thread-1up1lx
Reviewed-by: session_01JQwxbXGpVJHZTXXerMoY4C

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#5 · os-warren · session_01JQwxbXGpVJHZTXXerMoY4C

席位核对

ACCEPT · seat domain:ui#5 · session_01JQwxbXGpVJHZTXXerMoY4C · head e1f1c06a · 2026-10-29T05:10Z

席位意见:以前 AI 助手回答里的图表一刷新就没了,因为直播流里的图表只是临时数据,服务器从不保存。服务器保存的是 visualize_data 工具结果,里面的 chart 字段就是同一份图表描述,只是一直没人读。现在 plugin-chatbot 里有唯一一个读取点:消息里没有直播图表时,才从工具结果的 chart 恢复,所以在线回答不会画两次。服务器历史、本地缓存和 API 模式重建这三条路径都会把图表放回原来的文字位置。

  1. Shape: Fixes #25908. 10 files, +540/−34.
    • mapMessages.ts: chartFromDescriptor is extracted from extractCharts. It adds chartFromToolResult (the one reader of chart), chartsFromParts and chartAnswerParts.
    • useObjectChat.ts: the rebuild emits data-chart parts between text runs.
    • app-shell: AiChatPage.tsx hydration sets charts, and useChatConversation.ts caches charts as parts.
    • The two plugin-chatbot entries export the new functions.
    • Three new test files.
    • The changeset is a patch for @object-ui/plugin-chatbot. app-shell is private: true.
  2. Premise measured: the order's read points held. The seat read the diff and checked the following.
    • hydratedMessagesToChatMessages joins text parts with '', so chartsFromParts's offsets index the same content.
    • normalizeMessages gives content: msg.content || '', so chartAnswerParts always gets a string.
    • A live message carries both the part and the result, and parts take precedence (fromParts.length > 0 ? fromParts : fromResults).
    • A result without chart yields no chart.
    • The top-level rows are never read.
    • The second caller, SharedRecordPage.tsx, inherits the fix through the same mapper.
  3. Gates / check-runs: PR check-runs on e1f1c06a: 44 success, 11 skipped (path-filtered), 0 failure. mergeable_state is clean.
    • Dev: 35 of 35 derived gates exit 0. The new pins pass 14/14. plugin-chatbot passes 828/828 and app-shell ai/hooks/layout passes 1703/1703. Typecheck is green.
    • Ablation: the reader, the rebuild and the cache each turn their pins red, and all were restored.
  4. Accepted deviations: Clause-② changed from the first claim's no to yes (widening), measured by the dev, with the superseding claim 6487631480. The declaration-face contract review record 6488421537 (TIER_DEFAULT, PASS on e1f1c06a) is on this PR. The tool name is one literal, pinned by a test against spec's PLATFORM_TOOLS_BY_PACKAGE, because spec has no per-tool constant.
  5. Out of scope:
    • The proof is fixture-level only, because the producer's chart key runs on hosted deploys and is unreleased on npm (@objectstack/service-ai 10.3.0).
    • Results persisted before cloud abffd69 stay chartless by design.
    • Skills-docs: none: a console behaviour fix, with no authoring surface.
  6. Landing: not Tier H (no governed path). The Clause-② declaration review PASS is on record for this head, so the PR is ready for landing (ready + auto-merge).

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 05:11
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 9f76037 Oct 29, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-1up1lx branch October 29, 2026 05:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-chatbot / app-shell: an assistant answer's chart is gone after a reload — no hydration path keeps data-chart parts

2 participants