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

@@ -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,