Repository navigation
fix(plugin-chatbot,app-shell): keep an assistant answer's chart after a reload - #27396
Conversation
… 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>
…angeset Claude-Session: https://claude.ai/code/session_01JQwxbYXpAEUq8s7bsevHLW Co-Authored-By: Claude <noreply@anthropic.com>
|
domain:ui#5 · os-warren · session_01JQwxbXGpVJHZTXXerMoY4C Contract reviewServed-tier: The face is a declaration: ① Derived judgments
② Semver levelAdditive exports on a published package with no protocol kind ship as ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:ui#5 · os-warren · session_01JQwxbXGpVJHZTXXerMoY4C 席位核对ACCEPT · seat 席位意见:以前 AI 助手回答里的图表一刷新就没了,因为直播流里的图表只是临时数据,服务器从不保存。服务器保存的是
Generated by Claude Code |
Requested by Warren · project thread
Fixes #25908
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
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-chartpart that the server never stores. What the server stores is thevisualize_datatool result, one JSON string whosechartkey holds the same descriptor the part carried (#26744, comment 6457382770). Nothing read that key back.plugin-chatbotmapMessages.ts,chartFromToolResult). It readschartoff avisualize_dataresult through the existingparseResultEnvelope, and narrows it with the samechartFromDescriptorthedata-chartpart goes through, now shared by both. The top-levelrows(a 20-row narration preview) are never drawn. A result stored before the producer wrotechartgives no chart, with no rebuild from its other keys.extractChartsreads tool results only for a message that has nodata-chartpart. 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.app-shellhydratedMessagesToChatMessages) setschartsfromchartsFromParts(message.parts). That walk covers both the server'svisualize_dataresults and the cache'sdata-chartparts, and the offsets index the same text join the hydration makescontent. The share page (SharedRecordPage) renders these messages directly, so it gets the chart too, with no change to its file.useObjectChataiInitialMessages). The rebuild used to write one text part and then the tools, so a hydratedchartskey was dropped. It now writeschartAnswerParts(content, charts): the text cut at each chart's offset, with adata-chartpart between the runs. The mapper reads that back as the same content and the same charts at the same offsets.app-shellsanitizeChatMessagesForCache). It writes the samechartAnswerPartsin 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.data-chartparts, ormapMessagesre-derives charts from the tool output. This PR takes the first for the round trip and the cache, because the live path already draws fromdata-chartparts and only position-encoded parts keeptextOffsetthrough a rebuild that joins text. The one reader ofchartsits inmapMessages, and the hydration mapper reaches it throughchartsFromParts.@objectstack/specdeclaresvisualize_dataonly as a member ofPLATFORM_TOOLS_BY_PACKAGE['service-ai'], with no constant of its own.mapMessageskeeps one literal,VISUALIZE_DATA_TOOL(a module export, not a package export), and a test pins it to that registry.Clause-②: yes (widening), claim 6487631480):chartsFromParts,chartAnswerPartsand theChartAnswerParttype, on@object-ui/plugin-chatbotand@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-chatbotmapMessages.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 withoutchartgives no chart and does not throw, and neither does a non-JSON one. Onlyvisualize_datais read, and its name is pinned to the spec registry. A live message with both the part and the result draws one chart.chartAnswerPartsmaps back to the same message, and a chart without an offset follows the text.plugin-chatbotuseObjectChat.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-shellAiChatPage.persistedChart-25908.test.ts. Server history restores the chart between the text it sat between, and a result withoutchartrestores 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
abffd69) runs on hosted deploys only (@objectstack/service-ai10.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 cloudmaina32c80c.{ type: 'text', value }), the form a string tool result takes, and a bare string. A JSON-wrapped output ({ type: 'json', value }) with nostatuskey resolves to the wrapper in the sharedparseResultEnvelope, 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.api) keeps dropping hydrated render-only keys innormalizeMessages,chartsandbuildProgressalike, 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 fromorigin/mainc4c515ad. The four commitsmaingained since touch none of this PR's files.New pins and ablations
Test Files 3 passed (3),Tests 14 passed (14).scripts/ablation-replace.mjs, which confirms the mutation landed, restores the file and reads it back equal to the HEAD blob with an emptygit diff HEAD. Every leg is a source-level mutation, becauseui/vitest.config.mtsaliases@object-ui/plugin-chatbotand its/coreentry tosrc.chartFromToolResultreturns undefined): 3 tests fail, the two result-reading pins inmapMessages.persistedChart-25908and the server-history pin inAiChatPage.persistedChart-25908.chartAnswerPartshanded no charts inaiInitialMessages): 2 tests fail, both inuseObjectChat.persistedChart-25908.sanitizeChatMessagesForCachehanded no charts): 2 tests fail, both cache-fallback pins inAiChatPage.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).SharedRecordPagetest files inui/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-configon 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)
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
packages/specholds no runtime logic. (Nopackages/specchange.)pnpm lintpasses. (eslint on the touched files: 0 errors; the repo-wide run is CI's.)main, one pull request.packages/spectouched: not applicable.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code