Merge remote-tracking branch 'origin/master' into worktree/web-theme-settings-integration-fde706

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.zh.md
#	.agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.zh.md
#	.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.zh.md
#	.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml
#	apps/web/tests/scaffold.ts
#	docs/module-graph.md
#	packages/client/runtime/README.i18n.yaml
#	packages/client/runtime/package.json
#	packages/client/test-runtime/README.i18n.yaml
#	packages/client/test-runtime/README.zh.md
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/README.zh.md
#	packages/client/ui-conversation/package.json
#	packages/client/ui-conversation/src/client/apply.ts
#	packages/client/ui-theme/README.i18n.yaml
#	packages/client/ui-theme/README.md
#	packages/client/ui-theme/README.zh.md
#	packages/host/apiproxy/README.i18n.yaml
#	packages/host/apiproxy/README.zh.md
#	pnpm-lock.yaml
This commit is contained in:
Yichen Jiang
2026-08-10 12:50:43 +08:00
3485 changed files with 86192 additions and 25255 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-tool/README.md
README.md: d6bc0f248cffaf4c65ecf6d97c7a4afb17fc8941
README.zh.md: f4e8044fe6e10e1fbdfb70987efe07c884fa9ac8
README.md: 8255aee4292ea821962477a7108b40cf3b937981
README.zh.md: 2bc5859f592efb6ac4b67157be09a3c09afb691c

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
Client Tool presentation plugin. `ui-conversation` supplies one ordered root call through `conversation.chat.tool`; this package renders that root and its Code Dispatch children, then dispatches every atomic call through the keyed `tool.call.toolview` slot. Unregistered Tool names use the generic card.
Client Tool presentation plugin. `ui-conversation` dispatches each ordered `tool-call` Conversation Node through the matching key of `conversation.chat.node`; this package renders its root and Code Dispatch children, then dispatches every atomic call through the keyed `tool.call.toolview` slot. Unregistered Tool names use the generic card.
Business UI packages register only their wire Tool names and atomic views. They do not pair Session events, rebuild the transcript, or own root/subcall topology. The Runtime remains authoritative for call/result pairing, lifecycle, and recursive `subCalls` projection; the conversation view remains authoritative for ChatFlow placement.
@@ -10,7 +10,7 @@ Business UI packages register only their wire Tool names and atomic views. They
`ToolCallTree` receives one root `ToolCallBlock` that already contains recursive `subCalls`, selection state, the session `cwd`, and Host callbacks for opening files and inspecting calls. It recursively walks the standard call blocks and sends the root and children at every depth through the same atomic dispatch path, without subscribing to a separate parent-to-children map.
Each root and child wrapper preserves the `conversation.chat.tool` call-anchor DOM contract used for paging and selection.
Each root and child wrapper preserves the `data-chat-anchor-key="call:<id>"` and `data-chat-call-id` DOM contract used for paging and selection.
The package also fills `conversation.details.tool` with `ToolDetails`. The row and details renderers share the same pure card models for `terminal`, `read`, `diff`, `search`, and `web` render intents. Unknown intent tags and malformed wire card data fall back to flattened Tool result text.
@@ -44,6 +44,6 @@ None. The package is client-only presentation.
## Known Limitations and Deferred Work
- The Host excludes `run_code` from Code Mode program bindings, so production events currently produce one dispatch level; the recursive Runtime/UI contract is ready for future nested producers.
- Existing first-party Tool views are initially colocated here and can move to their owning business packages independently through the keyed slot.
- Tool copy temporarily reuses the `ui-conversation` locale namespace.
- The Host excludes `run_code` from Code Mode program bindings, so production events produce one dispatch level; the recursive Runtime/UI contract supports nesting.
- First-party Tool views are colocated here and can move to their owning business packages independently through the keyed slot.
- Tool copy reuses the `ui-conversation` locale namespace.

View File

@@ -2,15 +2,15 @@
[English](README.md) | 中文
Client Tool 展示插件。`ui-conversation` 通过 `conversation.chat.tool` 交付一个已经排好位置的 root call;本包渲染 root 及其 Code Dispatch 子调用,并把每个原子调用通过 keyed slot `tool.call.toolview` 分发。没有注册的 Tool 名称使用通用卡片。
Client Tool 展示插件。`ui-conversation` 通过 `conversation.chat.node` 的同名 key 分发每个已排序的 `tool-call` Conversation Node;本包渲染其中的 root 及其 Code Dispatch 子调用,并把每个原子调用通过 keyed slot `tool.call.toolview` 分发。没有注册的 Tool 名称使用通用卡片。
业务 UI 包只注册 wire Tool 名称和原子视图,不配对 Session Event、不重建 transcript也不拥有 root/subcall 拓扑。Runtime 继续负责 call/result 配对、生命周期和递归 `subCalls` 投影conversation view 继续负责 ChatFlow 位置。
## 渲染
## 渲染约
`ToolCallTree` 接收一个已经包含递归 `subCalls` 的 root `ToolCallBlock`、selection 状态、会话 `cwd`,以及用于打开文件和检查调用的 Host 回调。它递归遍历标准 call block让 root 与任意深度的 child 经过同一条原子分发路径,不再订阅独立的 parent-to-children map。
每个 root 和 child wrapper 都保留 `conversation.chat.tool` 的 call-anchor DOM 约,供分页和 selection 使用。
每个 root 和 child wrapper 都保留 `data-chat-anchor-key="call:<id>"``data-chat-call-id` DOM 约,供分页和 selection 使用。
本包还通过 `ToolDetails` 填充 `conversation.details.tool`。行 renderer 与详情 renderer 为 `terminal``read``diff``search``web` render intent 共用同一组纯 card model。本版本不认识的 intent 标签和格式错误的 wire card 数据都会回退为压平的 Tool result 文本。
@@ -44,6 +44,6 @@ owner 载荷为 `ToolCallOwnerProps``callId`、`toolName`、冻结的 `block`
## 已知限制与后续工作
- Host 不把 `run_code` 暴露为 Code Mode 程序 binding因此生产事件目前只能产生一层分发;递归的运行时/UI 契约已为未来的嵌套生产者做好准备
- 现有第一方 Tool 视图初期仍集中在本包,之后可以通过 keyed slot 独立迁回各自业务包。
- Tool 文案暂时复用 `ui-conversation` locale namespace。
- Host 不把 `run_code` 暴露为 Code Mode 程序 binding因此生产事件产生一层分发;递归的运行时/UI 约定支持嵌套
- 第一方 Tool 视图集中在本包,可以通过 keyed slot 独立迁回各自业务包。
- Tool 文案复用 `ui-conversation` locale namespace。

View File

@@ -20,8 +20,9 @@ export const inject = ['slots']
* @param ctx - Client root context.
*/
export function apply(ctx: ClientContext): void {
ctx.slots.inject('conversation.chat.tool', () => ctx.slots.register({
name: 'conversation.chat.tool',
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
name: 'conversation.chat.node',
key: 'tool-call',
locale: NS,
children: {
'tool.call.toolview': { kind: 'keyed', scope: 'session' },

View File

@@ -30,8 +30,8 @@ export interface ToolCallOwnerProps {
/** Full props of a registered atomic Tool view. */
export type ToolCallViewProps = PropsRuntime<'tool.call.toolview'>
/** Full props of the Tool call-tree renderer registered into the chat flow. */
export type ToolTreeProps = PropsRuntime<'conversation.chat.tool'>
/** Full props of the Tool call-tree renderer registered as a `tool-call` Chat Node. */
export type ToolTreeProps = PropsRuntime<'conversation.chat.node', 'tool-call'>
& PropsRenderSlots<'tool.call.toolview'>
& PropsLocale<'conversation'>

View File

@@ -88,8 +88,9 @@ const ToolCallBranch = memo(function ToolCallBranch({
* @returns the Tool call tree.
*/
export function ToolCallTree({
renderSlot, block, selectedCallId, cwd, openFile, inspectCall, t,
renderSlot, node, selectedCallId, cwd, openFile, inspectCall, t,
}: ToolTreeProps) {
const block = node.data.root
return (
<ToolCallBranch
renderSlot={renderSlot}

View File

@@ -49,8 +49,8 @@ export function SearchRow({ toolName, block, inspect, t }: SearchRowProps) {
// sub-dispatch, legacy generic result) has its text nowhere else to go;
// ToolRow's Output section carries it, and errorSummary its first line.
// When a card is present ToolRow renders it instead of the output, so
// passing model.output unconditionally is safe and keeps the four card
// rows symmetric.
// model.output passes unconditionally and the four card rows stay
// symmetric.
output={model.output}
errorSummary={model.errorSummary}
search={search}

View File

@@ -8,6 +8,7 @@ import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots'
import { SlotTestRuntime, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
import { apply as applyConversation, inject as injectConversation } from '@deepseek-ai/dsh-client-ui-conversation/client'
import { apply as applyTool, inject as injectTool } from '../src/client/apply.ts'
import { toolChatSnapshot } from './tool-details-render.tsx'
// The service reads its initial locale from the browser; these specs assert
// the shipped Chinese copy, so they state the browser they assume.
@@ -75,7 +76,7 @@ async function bench(nodes: ToolResultNode[]) {
await runtime.sessions.add({
id: SID,
summary: { title: 'S', displayTitle: 'S', cwd: '/proj' },
snapshot: { nodes },
snapshot: { nodes, chat: toolChatSnapshot(nodes) },
session: {
loadOlder: vi.fn<ISession['loadOlder']>(),
prompt: vi.fn<ISession['prompt']>(async () => ({ ok: true, value: { accepted: true } })),

View File

@@ -11,7 +11,9 @@
import { Context } from 'cordis'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render } from '@testing-library/react'
import { createSnapshotStore, SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
import {
ConversationEventRegistry, ConversationViewRegistry, createSnapshotStore, SlotsService,
} from '@deepseek-ai/dsh-client-runtime/client'
import type {
ConversationSnapshot, RunningToolCall, SessionId, SessionListState,
ToolCallBlock, ToolResultNode, WorkspaceListState,
@@ -21,6 +23,7 @@ import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots'
import { apply as applyConversation, inject as injectConversation } from '@deepseek-ai/dsh-client-ui-conversation/client'
import { apply as applyTool, inject as injectTool } from '../src/client/apply.ts'
import { toolChatSnapshot } from './tool-details-render.tsx'
const SID = 's1' as SessionId
@@ -75,7 +78,8 @@ function snapshotWith(
const nestedNodes = nodes.map(node => ({ ...node, subCalls }))
const nestedRunningCalls = runningCalls.map(call => ({ ...call, subCalls }))
return {
sessionId: SID, nodes: nestedNodes, turnTimings: new Map(), turnEnds: new Map(), partial: null,
sessionId: SID, chat: toolChatSnapshot(nestedNodes, nestedRunningCalls),
nodes: nestedNodes, turnTimings: new Map(), turnEnds: new Map(), partial: null,
runningCalls: nestedRunningCalls,
pending: [], queue: [], running: runningCalls.length > 0, composerPhase: 'active', removed: false,
openState: 'open', openError: null,
@@ -89,11 +93,16 @@ function AppRoot({ renderSlot }: AppRootProps) {
return <>{renderSlot('conversation', {})}</>
}
/** Same real-stack bench as the toolview-slot spec: SlotsService + renderer + both owning package applies; fakes only at service seams. */
/**
* Same real-stack bench as the toolview-slot spec: SlotsService + renderer +
* both owning package applies; fakes only at service boundaries.
*/
async function bench(snapshot: ConversationSnapshot) {
const ctx = new Context()
const slotsFiber = ctx.plugin(SlotsService)
await slotsFiber.await()
await ctx.plugin(ConversationEventRegistry).await()
await ctx.plugin(ConversationViewRegistry).await()
const slots = ctx.get('slots') as SlotsService
const session = createSnapshotStore<ConversationSnapshot>(snapshot)

View File

@@ -20,7 +20,7 @@ import { createChatStore } from '@deepseek-ai/dsh-client-ui-conversation/src/cli
import { GenericToolCard, type GenericToolCardProps } from '../src/client/tool/toolviews/GenericToolCard.tsx'
import { DetailsPanel } from '@deepseek-ai/dsh-client-ui-conversation/src/client/skeleton/DetailsPanel.tsx'
import { FileMutationRow, fileMutationToolview } from '../src/client/tool/toolviews/file-mutation-row.tsx'
import { renderToolDetails, SessionProviderStub } from './tool-details-render.tsx'
import { renderToolDetails, SessionProviderStub, toolChatSnapshot } from './tool-details-render.tsx'
import { zh } from '@deepseek-ai/dsh-client-ui-conversation/src/client/locales.ts'
afterEach(cleanup)
@@ -342,8 +342,11 @@ describe('DetailsPanel diff Output section', () => {
}
function snapshot(over: Partial<ConversationSnapshot> = {}): ConversationSnapshot {
const nodes = over.nodes ?? []
const runningCalls = over.runningCalls ?? []
return {
sessionId: SID, nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
sessionId: SID, chat: over.chat ?? toolChatSnapshot(nodes, runningCalls),
nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
pending: [], queue: [], running: false, composerPhase: 'active', removed: false,
openState: 'open', openError: null, hasMore: false, loadingOlder: false,
promptError: null, blank: false, subagent: null, lastAgentError: null, ...over,

View File

@@ -24,7 +24,7 @@ import { GenericToolCard, type GenericToolCardProps } from '../src/client/tool/t
import { zh } from '@deepseek-ai/dsh-client-ui-conversation/src/client/locales.ts'
import { DetailsPanel } from '@deepseek-ai/dsh-client-ui-conversation/src/client/skeleton/DetailsPanel.tsx'
import { ReadRow, readToolview } from '../src/client/tool/toolviews/read-row.tsx'
import { renderToolDetails, SessionProviderStub } from './tool-details-render.tsx'
import { renderToolDetails, SessionProviderStub, toolChatSnapshot } from './tool-details-render.tsx'
afterEach(cleanup)
@@ -288,8 +288,11 @@ describe('DetailsPanel Output section (read)', () => {
}
function snapshot(over: Partial<ConversationSnapshot> = {}): ConversationSnapshot {
const nodes = over.nodes ?? []
const runningCalls = over.runningCalls ?? []
return {
sessionId: SID, nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
sessionId: SID, chat: over.chat ?? toolChatSnapshot(nodes, runningCalls),
nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
pending: [], queue: [], running: false, composerPhase: 'active', removed: false,
openState: 'open', openError: null, hasMore: false, loadingOlder: false,
promptError: null, blank: false, subagent: null, lastAgentError: null, ...over,

View File

@@ -23,7 +23,7 @@ import { createChatStore } from '@deepseek-ai/dsh-client-ui-conversation/src/cli
import { GenericToolCard, type GenericToolCardProps } from '../src/client/tool/toolviews/GenericToolCard.tsx'
import { DetailsPanel } from '@deepseek-ai/dsh-client-ui-conversation/src/client/skeleton/DetailsPanel.tsx'
import { SearchRow, searchToolview } from '../src/client/tool/toolviews/search-row.tsx'
import { renderToolDetails, SessionProviderStub } from './tool-details-render.tsx'
import { renderToolDetails, SessionProviderStub, toolChatSnapshot } from './tool-details-render.tsx'
/** SearchRow now composes ToolRow, so its props include the locale `t` seat. */
type SearchRowProps = Parameters<typeof SearchRow>[0]
@@ -404,8 +404,11 @@ describe('DetailsPanel Output section (search)', () => {
}
function snapshot(over: Partial<ConversationSnapshot> = {}): ConversationSnapshot {
const nodes = over.nodes ?? []
const runningCalls = over.runningCalls ?? []
return {
sessionId: SID, nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
sessionId: SID, chat: over.chat ?? toolChatSnapshot(nodes, runningCalls),
nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
pending: [], queue: [], running: false, composerPhase: 'active', removed: false,
openState: 'open', openError: null, hasMore: false, loadingOlder: false,
promptError: null, blank: false, subagent: null, lastAgentError: null, ...over,

View File

@@ -20,7 +20,7 @@ import { createChatStore } from '@deepseek-ai/dsh-client-ui-conversation/src/cli
import { GenericToolCard, type GenericToolCardProps } from '../src/client/tool/toolviews/GenericToolCard.tsx'
import { DetailsPanel } from '@deepseek-ai/dsh-client-ui-conversation/src/client/skeleton/DetailsPanel.tsx'
import { BashRow } from '../src/client/tool/toolviews/bash-sample.tsx'
import { renderToolDetails, SessionProviderStub } from './tool-details-render.tsx'
import { renderToolDetails, SessionProviderStub, toolChatSnapshot } from './tool-details-render.tsx'
import { zh } from '@deepseek-ai/dsh-client-ui-conversation/src/client/locales.ts'
type BashRowProps = Parameters<typeof BashRow>[0]
@@ -479,8 +479,11 @@ describe('DetailsPanel Output section', () => {
}
function snapshot(over: Partial<ConversationSnapshot> = {}): ConversationSnapshot {
const nodes = over.nodes ?? []
const runningCalls = over.runningCalls ?? []
return {
sessionId: SID, nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
sessionId: SID, chat: over.chat ?? toolChatSnapshot(nodes, runningCalls),
nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
pending: [], queue: [], running: false, composerPhase: 'active', removed: false,
openState: 'open', openError: null, hasMore: false, loadingOlder: false,
promptError: null, blank: false, subagent: null, lastAgentError: null, ...over,

View File

@@ -29,12 +29,21 @@ function props(
return {
useSession,
renderSlot,
callId: block.callId,
toolName: block.call?.name ?? '',
block,
node: {
key: `tool:${block.callId}`,
kind: 'tool-call',
id: block.callId,
target: 'chat',
anchorSeq: block.seq,
location: { kind: 'session' },
visibility: 'visible',
data: { root: block },
},
selectedCallId,
openFile: vi.fn(),
inspectCall: vi.fn(),
forkAt: vi.fn(),
fileMentions: vi.fn(),
t,
} as unknown as ToolTreeProps
}

View File

@@ -1,5 +1,7 @@
/** Test adapter for the production conversation.details.tool registration. */
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import type {
ChatConversationViewNode, ChatSnapshot, ConversationNode, RunningToolCall, SessionId,
} from '@deepseek-ai/dsh-client-runtime/client'
import type { SessionProviderComponent, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
import type { DetailsSlotProps, DetailsToolOwnerProps } from '@deepseek-ai/dsh-client-ui-conversation/src/client/contract/slots.ts'
import { ToolDetails } from '../src/client/tool/ToolDetails.tsx'
@@ -7,6 +9,45 @@ import { ToolDetails } from '../src/client/tool/ToolDetails.tsx'
/** Framework session-area seat used by direct DetailsPanel tests. */
export const SessionProviderStub: SessionProviderComponent = ({ children }) => children('s1' as SessionId)
/** Build the canonical Chat slice consumed by Tool rows and details tests. */
export function toolChatSnapshot(
settled: readonly ConversationNode[] = [],
running: readonly RunningToolCall[] = [],
): ChatSnapshot {
const roots = [...settled.filter(node => node.kind === 'tool-result'), ...running]
const nodes: ChatConversationViewNode[] = roots.map(root => ({
key: `tool:${root.callId}`,
kind: 'tool-call',
id: root.callId,
target: 'chat',
anchorSeq: 'kind' in root ? root.seq : Number.MAX_SAFE_INTEGER,
location: { kind: 'session' },
visibility: 'visible',
data: { root },
}))
const byKey = new Map(nodes.map(node => [node.key, node]))
const empty: readonly string[] = []
return {
order: nodes.map(node => node.key),
nodes: {
get: key => byKey.get(key),
values: () => nodes,
},
locations: {
getTurn: () => empty,
getStep: () => empty,
},
timeline: { turnOrder: [], turns: new Map() },
legacy: {
nodes: settled,
runningCalls: running,
partial: null,
turnTimings: new Map(),
turnEnds: new Map(),
},
}
}
/**
* Bind ui-tool's details renderer to the conversation slot callback shape.
* @param t - conversation locale seat used by Tool cards.
@@ -16,7 +57,7 @@ export function renderToolDetails(t: TranslateNS<'conversation'>): DetailsSlotPr
return (_key, owner) => {
// PropsRenderSlots keeps its key generic even for this one-key share;
// recover the concrete owner selected by the adapter's fixed slot.
const details = owner as DetailsToolOwnerProps
const details = owner as unknown as DetailsToolOwnerProps
return <ToolDetails block={details.block} cwd={details.cwd} t={t} />
}
}

View File

@@ -71,8 +71,8 @@ describe('tool-call-model', () => {
expect(toolRowModel('read', running({ name: 'read', argsRaw: '{"path":"/tmp/x.ts"}' })).summary).toBe('/tmp/x.ts')
expect(toolRowModel('write', running({ name: 'write', argsRaw: '{"file_path":"src/x.ts"}' })).summary).toBe('src/x.ts')
expect(toolRowModel('edit', running({ name: 'edit', argsRaw: '{"file_path":"src/x.ts"}' })).summary).toBe('src/x.ts')
// Others rows prefix the real tool name into the summary slot (figma-flows
// ruling: static "Tool call" title, name rides the mutable summary).
// Other rows prefix the real tool name into the summary slot (figma
// flows: static "Tool call" title, the name rides the mutable summary).
expect(toolRowModel('x', running({ argsRaw: '{"n":1}' })).summary).toBe('x · {"n":1}')
expect(toolRowModel('x', running({ argsRaw: 'not json' })).summary).toBe('x · not json')
expect(toolRowModel('x', running({ argsRaw: '' })).summary).toBe('x · c1')

View File

@@ -18,6 +18,7 @@ import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import { apply as applyConversation, inject as injectConversation } from '@deepseek-ai/dsh-client-ui-conversation/client'
import { apply as applyTool, inject as injectTool } from '@deepseek-ai/dsh-client-ui-tool/client'
import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client'
import { toolChatSnapshot } from './tool-details-render.tsx'
const SID = 's1' as SessionId
@@ -58,7 +59,7 @@ const LAYOUT_CHILDREN = {
/**
* Real-stack bench: SlotTestRuntime with the session/layout doubles at the
* service seams only (external boundaries), the package apply on its own
* service boundaries only, the package apply on its own
* fiber, and the test AppFrame occupying 'root'.
*/
async function bench(nodes: ToolResultNode[]) {
@@ -72,7 +73,7 @@ async function bench(nodes: ToolResultNode[]) {
await runtime.sessions.add({
id: SID,
summary: { title: 'S', displayTitle: 'S' },
snapshot: { nodes },
snapshot: { nodes, chat: toolChatSnapshot(nodes) },
session: {
loadOlder: vi.fn<ISession['loadOlder']>(),
prompt: vi.fn<ISession['prompt']>(async () => ({ ok: true, value: { accepted: true } })),

View File

@@ -23,7 +23,7 @@ import { createChatStore } from '@deepseek-ai/dsh-client-ui-conversation/src/cli
import { GenericToolCard } from '../src/client/tool/toolviews/GenericToolCard.tsx'
import { DetailsPanel } from '@deepseek-ai/dsh-client-ui-conversation/src/client/skeleton/DetailsPanel.tsx'
import { WebRow, webToolview } from '../src/client/tool/toolviews/web-row.tsx'
import { renderToolDetails, SessionProviderStub } from './tool-details-render.tsx'
import { renderToolDetails, SessionProviderStub, toolChatSnapshot } from './tool-details-render.tsx'
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
import { zh } from '@deepseek-ai/dsh-client-ui-conversation/src/client/locales.ts'
@@ -234,8 +234,11 @@ describe('DetailsPanel web Output section', () => {
}
function snapshot(over: Partial<ConversationSnapshot> = {}): ConversationSnapshot {
const nodes = over.nodes ?? []
const runningCalls = over.runningCalls ?? []
return {
sessionId: SID, nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
sessionId: SID, chat: over.chat ?? toolChatSnapshot(nodes, runningCalls),
nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
pending: [], queue: [], running: false, composerPhase: 'active', removed: false,
openState: 'open', openError: null, hasMore: false, loadingOlder: false,
promptError: null, blank: false, subagent: null, lastAgentError: null, ...over,