From 64837575a91fa8f67e00de72bce12d58d3cc2676 Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 30 Sep 2026 15:47:22 +0000 Subject: [PATCH] Port artifact events to the items stream and finish the main merge Why: the merge commit left tsc broken in two spots - an artifact.created handler still pushing to the removed LiveState.parts, and main's newer ui icons not yet checked out in this worktree; also Part was imported unused. Evidence: artifact.created now appends an artifact LiveItem (process=true, matching the backend's ae8e5d0) projected to artifact_ref by liveToMessage; new test asserts it lands after its tool call. tsc -b exit 0 after initializing the ui submodule checkout; vitest 33/33. Impact: live artifact chips render inside the process stream and fold on reconcile; no functional change elsewhere. Refs: feature/chat-process-split-r1 --- src/live.ts | 8 ++++++++ src/store.ts | 6 +++--- test/live.test.ts | 14 +++++++++++++- tsconfig.tsbuildinfo | 2 +- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/live.ts b/src/live.ts index b07812b..b2f008d 100644 --- a/src/live.ts +++ b/src/live.ts @@ -14,6 +14,7 @@ export type LiveItem = | { kind: 'thinking'; text: string } | { kind: 'text'; index: number; text: string } | { kind: 'tool'; callId?: string; name: string; status?: string; summary?: string } + | { kind: 'artifact'; artifact: { artifactId: string; name: string; kind?: string } } // applyThinkingDelta 并入思考增量:末段是思考则追加,否则开启新思考段(新轮开始)。 export function applyThinkingDelta(items: LiveItem[], delta: string): LiveItem[] { @@ -41,6 +42,11 @@ export function applyToolCall(items: LiveItem[], name: string, callId?: string): return [...items, { kind: 'tool', callId, name }] } +// applyArtifactCreated 追加工件引用(artifact.created 事件;与落库一致标记为过程)。 +export function applyArtifactCreated(items: LiveItem[], artifact: { artifactId: string; name: string; kind?: string }): LiveItem[] { + return [...items, { kind: 'artifact', artifact }] +} + // applyToolResult 回填对应工具(优先按 callId 配对,回落到最近一个未决工具—— // 与落库的尾部配对语义一致)。 export function applyToolResult(items: LiveItem[], callId: string | undefined, status: string, summary?: string): LiveItem[] { @@ -77,6 +83,8 @@ export function liveToMessage(live: LiveState): Message { parts.push({ type: 'tool_result', process: true, toolCall: { id: it.callId ?? '', name: it.name }, toolResult: { callId: it.callId ?? '', status: it.status === 'error' ? 'error' : 'ok', summary: it.summary } }) } + } else if (it.kind === 'artifact') { + parts.push({ type: 'artifact_ref', process: true, artifact: it.artifact }) } else { parts.push({ type: 'text', text: it.text, process: textProcess.get(i) || undefined }) } diff --git a/src/store.ts b/src/store.ts index a4f1ee5..522118f 100644 --- a/src/store.ts +++ b/src/store.ts @@ -2,10 +2,10 @@ // useSyncExternalStore 驱动;live 流式部件为会话内临时态,run 终态后由消息树对账收敛。 import { useSyncExternalStore } from 'react' import { api, connectEvents } from './api' -import { applyTextDelta, applyThinkingDelta, applyToolCall, applyToolResult, type LiveItem } from './live' +import { applyTextDelta, applyThinkingDelta, applyToolCall, applyToolResult, applyArtifactCreated, type LiveItem } from './live' import type { Project, Conversation, Message, Run, Artifact, HITLCard, PlanSnapshot, TodoList, - AgentProfile, StreamEvent, PluginPanel, ActivityCard, Part, + AgentProfile, StreamEvent, PluginPanel, ActivityCard, } from './contracts' export interface ContextObs { @@ -342,7 +342,7 @@ export const store = { const p = e.payload as { artifact?: { id: string; name: string; kind?: string } } if (p?.artifact) { set({ live: touchLive(e.runId ?? '', l => { - l.parts.push({ type: 'artifact_ref', artifact: { artifactId: p.artifact!.id, name: p.artifact!.name, kind: p.artifact!.kind } }) + l.items = applyArtifactCreated(l.items, { artifactId: p.artifact!.id, name: p.artifact!.name, kind: p.artifact!.kind }) }) }) } if (state.currentProjectId) store.refreshArtifacts(state.currentProjectId) diff --git a/test/live.test.ts b/test/live.test.ts index 0639695..c2549d6 100644 --- a/test/live.test.ts +++ b/test/live.test.ts @@ -2,7 +2,7 @@ // 轮次穿插同构、过程区折叠谓词。 import { describe, it, expect } from 'vitest' import { - applyTextDelta, applyThinkingDelta, applyToolCall, applyToolResult, + applyTextDelta, applyThinkingDelta, applyToolCall, applyToolResult, applyArtifactCreated, liveToMessage, showProcessInline, type LiveItem, } from '../src/live' import type { LiveState } from '../src/store' @@ -122,6 +122,18 @@ describe('liveToMessage', () => { const msg = liveToMessage(live({ items })) for (const p of msg.parts.filter(p => p.type !== 'text')) expect(p.process).toBe(true) }) + it('artifact refs land after their tool call and are process', () => { + let items: LiveItem[] = [] + items = applyTextDelta(items, 0, '生成报表。') + items = applyToolCall(items, 'make_report', 'c1') + items = applyToolResult(items, 'c1', 'ok') + items = applyArtifactCreated(items, { artifactId: 'a1', name: 'report.csv', kind: 'csv' }) + items = applyTextDelta(items, 1, '完成。') + const msg = liveToMessage(live({ items })) + expect(msg.parts.map(p => p.type)).toEqual(['text', 'tool_call', 'tool_result', 'artifact_ref', 'text']) + expect(msg.parts.find(p => p.type === 'artifact_ref')).toMatchObject({ process: true }) + expect(msg.parts.filter(p => p.type === 'text').map(p => p.process)).toEqual([true, undefined]) + }) }) describe('showProcessInline', () => { diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index ea1364b..70fbd13 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/api.ts","./src/auth.ts","./src/contracts.ts","./src/live.ts","./src/main.tsx","./src/messageparts.ts","./src/passwords.ts","./src/store.ts","./src/tasklist.ts","./src/components/AdminUsersModal.tsx","./src/components/AgentTracker.tsx","./src/components/Conversation.tsx","./src/components/DocViewer.tsx","./src/components/LoginPage.tsx","./src/components/ProjectSettingsModal.tsx","./src/components/SettingsModal.tsx","./src/components/TaskSidebar.tsx","./src/components/Workbench.tsx","./src/components/Workspaces.tsx","./test/auth.test.ts","./test/contracts.test.ts","./test/live.test.ts","./test/messageparts.test.ts","./test/tasklist.test.ts"],"errors":true,"version":"5.9.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/api.ts","./src/auth.ts","./src/contracts.ts","./src/live.ts","./src/main.tsx","./src/messageparts.ts","./src/passwords.ts","./src/store.ts","./src/tasklist.ts","./src/components/AdminUsersModal.tsx","./src/components/AgentTracker.tsx","./src/components/Conversation.tsx","./src/components/DocViewer.tsx","./src/components/LoginPage.tsx","./src/components/ProjectSettingsModal.tsx","./src/components/SettingsModal.tsx","./src/components/TaskSidebar.tsx","./src/components/Workbench.tsx","./src/components/Workspaces.tsx","./test/auth.test.ts","./test/contracts.test.ts","./test/live.test.ts","./test/messageparts.test.ts","./test/tasklist.test.ts"],"version":"5.9.3"} \ No newline at end of file