feat(storybook): add agent chat message-part stories
**PR:**feat(storybook): add agent chat message-part stories (#4390)
source_automation: “voyager-merged-pr” run_date: “2026-07-06” anchor_pr_number: 4390 pr_number: 4390 pr_title: “feat(storybook): add agent chat message-part stories” pr_url: “https://github.com/adastralab-ai/voyager/pull/4390 ↗” author: “Horcrux / magicismight” merged_at: “2026-07-06T03:17:37Z” modules: [“packages/site/src/app/(main)/_agent/chat/_toolPart”,“packages/site/src/app/(main)/_agent/chat/_components”,“packages/storybook/.storybook”] files_changed: 34 learning_tags: [“agent-chat”,“tool-part-registry”,“storybook-fixtures”,“presentational-adapter-split”,“merged-tool-parts”,“devex”] business_line: null related_prs: [4344,4314] line_stage: null open_questions: [] feishu_doc_url: “https://my.feishu.cn/docx/HYX3dp8LSoNcDpx46RKc3KMHnch ↗”#
正文来源:飞书学习文档 ↗。以下为通过个人 Feishu API 获取并转换后的完整 Markdown 正文。
今日选择#
**PR:**feat(storybook): add agent chat message-part stories (#4390)
**作者:**Horcrux / magicismight
**Merge 时间:**2026-07-06T03:17:37Z(Australia/Melbourne 2026-07-06 13:17:37)
链接:https://github.com/adastralab-ai/voyager/pull/4390 ↗
**模块:**packages/site agent chat tool-part UI、packages/storybook Vite/Next Storybook 配置
**学习标签:**agent-chat、tool-part-registry、storybook-fixtures、presentational-adapter-split、merged-tool-parts、devex
为什么值得学#
- 标题是补 Storybook,但实际做了边界重切:把 web_search / image_generation 的展示组件从 registry adapter 中剥离出来,adapter 只负责把 AI SDK 的 ToolUIPart 映射成稳定 UI props。
- 它没有为 Storybook 另起一条假路径。bespoke tool 的 story 可以直接覆盖纯展示组件,而生产消息列表继续通过 renderToolPartAtIndex 走 registry dispatch,避免 demo 和产品行为分叉。
- 它把“多个 image_generation tool call 合并展示”的语义显式放进 registry entry 的 merge=true,而不是散落在消息列表循环里。这让 grouped UI、隐藏 stopped unfinished tool part、fallback legacy tool 等行为集中在 registry 层。
- 它补的是高杠杆调试面:agent 聊天状态以前要靠 live agent 恰好跑到某个状态,现在 running / completed / error / no_image / insufficient credits 都能用固定 fixture 看。
关键代码#
1. registry entry 同时声明 tool 覆盖范围和是否合并#
export function defineToolPart<Name extends keyof AgentTools>({
tool,
render,
aliases,
merge,
}: ...): ToolPartEntry<Name> {
const tools = [tool, ...(aliases ?? [])];
if (merge) {
return {
tools,
merge: true,
render: (parts, ctx) =>
render(
parts.filter((part) => isToolPartForEntry<Name>(part, tools)),
ctx,
),
};
}
return {
tools,
render: (part, ctx) =>
isToolPartForEntry<Name>(part, tools) ? render(part, ctx) : null,
};
}plaintext设计点:一个 tool renderer 的类型覆盖、legacy alias、是否按邻近 part 合并都由同一个 entry 表达。这样扩展新 tool 时不是去改中心 switch,而是新增一个局部 entry,并让类型系统继续检查覆盖面。
2. 消息列表按 registry 渲染,合并逻辑集中在一处#
export function renderToolPartAtIndex(
parts: readonly MessagePart[],
index: number,
ctx: ToolPartContext,
shouldHideToolPart: (part: StaticMessageToolPart) => boolean,
): ReactNode {
const part = parts[index];
if (!isStaticMessageToolPart(part) || shouldHideToolPart(part)) return null;
const entry = getToolPartEntry(part);
if (!entry) return <FallbackToolCallPart part={part} />;
if (!entry.merge) {
return entry.render(part, ctx);
}
if (hasPreviousVisibleMergedToolPart(parts, index, entry, shouldHideToolPart)) {
return null;
}
return entry.render(
collectMergedToolParts(parts, index, entry, shouldHideToolPart),
ctx,
);
}plaintext设计点:消息列表只提供 part 数组、当前位置、runtime context 和隐藏规则;registry 负责 fallback、single render、merged render。这个边界让 Storybook 可以测底层展示组件,也让生产渲染保持一条入口。
3. image_generation adapter 把 runtime state 映射成 UI tile union#
function ImageGenerationAssetTileAdapter({ part, onInsert }: { ... }) {
const output = part.state === "output-available" ? part.output : undefined;
const showLoading = useToolIsLoading(part);
if (output?.status === "insufficient_credits") {
return <ImageGenerationAssetTile tile={{ kind: "insufficientCredits", ... }} />;
}
if (output?.status === "no_image") {
return <ImageGenerationAssetTile tile={{ kind: "noImage", ... }} />;
}
const { aspectRatio } = getToolInput(part);
const style = thumbnailStyle(aspectRatio);
if (!style) return null;
if (isSuccessfulImageOutput(output)) {
return <ImageGenerationAssetTile onInsert={onInsert} tile={{ kind: "success", ... }} />;
}
if (part.state === "output-error") {
return <ImageGenerationAssetTile tile={{ kind: "error", ... }} />;
}
return showLoading ? <ImageGenerationAssetTile tile={{ kind: "loading", ... }} /> : null;
}plaintext设计点:UI 组件不直接理解 ToolUIPart、output-available、output-error、tool input 等 runtime 细节;adapter 把它们压成 tile.kind。这个拆法使 fixture 很稳定,也让 no_image 这种业务语义不会被误渲染成失败。
4. Storybook Vite shim 只处理实际打包边界#
viteFinal: async (viteConfig) => {
const { mergeConfig } = await import("vite");
const tailwindcss = (await import("@tailwindcss/vite")).default;
return mergeConfig(viteConfig, {
plugins: [tailwindcss()],
define: {
__dirname: '""',
"process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID": JSON.stringify(
process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID ?? "",
),
},
resolve: {
alias: {
"@": fileURLToPath(new URL("../../site/src", import.meta.url)),
},
},
});
}plaintext设计点:Storybook 复用 site 的 Next config,但 browser bundle 会碰到 next/navigation 间接拉入的 Node-only __dirname。这里把兼容性修补放在 Storybook 的 bundling boundary,而不是污染业务组件。
和最近学习记录的关系#
和昨天 #4344 有直接模块关系:#4344 在 backend/workers/agent 的 image_generation tool schema 中把附件图作为 referenceAssetIds 传给任务边界;#4390 则在 packages/site 的 agent chat 渲染层,把 image_generation tool part 的各种输出状态拆成可观察、可固定 fixture 的 UI。两者都围绕 image_generation,但关注点分别是 agent runtime/tool schema 和前端 tool-part presentation boundary。
和 2026-07-04 的 #4314 也有间接关系:#4314 迁移 task worker 的图片生成供应商边界,并把 content-policy/no-image 等供应商结果映射成产品语义;#4390 在 UI 侧继续消费 no_image / error / loading / success 这些语义,说明后端 outcome taxonomy 正在向前端调试面沉淀。
我会怎么吸收#
- 给复杂 runtime UI 做 Storybook 时,先拆 presentational component 和 runtime adapter;fixture 测展示层,生产路径保留真实 dispatch。
- 多工具、多状态、多 legacy alias 的 UI,不要靠消息列表堆 if;用 registry entry 表达 tool 覆盖、合并策略和 fallback。
- 把“模型没产出图”这类业务结果建成中性状态,而不是默认 error;这能降低用户误判,也方便日志和 UI 一致。
边界/风险#
- 这个 PR 主要验证 Storybook 和静态 fixture,没有新增交互级 regression test。对纯展示组件这符合“测试要值回成本”,但 registry merge/hide 的边界仍依赖现有路径和人工 Storybook 验证。
- ImageGenerationToolPart 仍依赖 AgentImagePreview、InsufficientCreditsNotice 这类 site runtime 组件;stories 里覆盖了 tile 外观,但成功态的真实图片加载/插入行为没有在本 PR 中端到端验证。
- Storybook shim __dirname 是明确的 bundler 兼容补丁;后续 next/navigation 或 OpenTelemetry 打包路径变化时,这类 shim 可能需要重新评估。
候选说明#
按 Australia/Melbourne 2026-07-06 当天窗口,查到 4 个未讲过的 merged PR:#4406 Cloudflare agents runtime 依赖升级、#4390 agent chat message-part stories、#4384 editor-api 本地开发修补、#4310 UI states polish。昨天窗口里还有 #4352、#4335、#4341、#4180 等;#4344 已在学习日志中记录,因此去重跳过。
最终选择 #4390,因为它虽然以 Storybook 为标题,但 diff 里有清晰的工程边界调整:tool-part registry、presentational/adapter split、merged tool part grouping、Storybook bundling shim。它比依赖升级和纯 UI polish 更适合作为今天的工程学习样本。