Repository navigation
Commit 9f76037
fix(plugin-chatbot,app-shell): keep an assistant answer's chart after a reload (#27396)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8HgycqL368Q1SBpG5BAa6AZ)_
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 #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.
<details><summary>New pins and ablations</summary>
- 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`.
</details>
<details><summary>Package suites and typecheck</summary>
- `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.
</details>
<details><summary>Derived gates (dispatch-gates --commands, reconciled
with --ran: 35 derived, 35 run, 0 NOT-MEASURED)</summary>
```text
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
```
</details>
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
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No `packages/spec` change.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (eslint on the touched
files: 0 errors; the repo-wide run is CI's.)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: not applicable.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English, apart from the 维护者速读
block the dispatch order asks for.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JQwxbYXpAEUq8s7bsevHLW)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent b223072 commit 9f76037
10 files changed
Lines changed: 540 additions & 34 deletions
File tree
- .changeset
- ui/packages
- app-shell/src
- console/ai
- __tests__
- hooks
- plugin-chatbot/src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
| 71 | + | |
71 | 72 | | |
72 | 73 | | |
73 | 74 | | |
| |||
294 | 295 | | |
295 | 296 | | |
296 | 297 | | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
297 | 305 | | |
298 | 306 | | |
299 | 307 | | |
300 | 308 | | |
301 | 309 | | |
302 | 310 | | |
| 311 | + | |
303 | 312 | | |
304 | 313 | | |
305 | 314 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
| 19 | + | |
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| |||
213 | 213 | | |
214 | 214 | | |
215 | 215 | | |
| 216 | + | |
| 217 | + | |
216 | 218 | | |
217 | 219 | | |
218 | 220 | | |
| |||
443 | 445 | | |
444 | 446 | | |
445 | 447 | | |
446 | | - | |
447 | | - | |
448 | | - | |
449 | | - | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
450 | 457 | | |
451 | 458 | | |
452 | 459 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
0 commit comments