Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input

# Conflicts:
#	apps/cli/src/web.ts
#	apps/web/tests/smoke-fixture.e2e.ts
#	docs/architecture.i18n.yaml
#	packages/client/connection/src/client/fixture.ts
#	packages/client/ui-conversation/README.md
#	packages/client/ui-conversation/package.json
#	packages/client/ui-conversation/src/client/apply.ts
#	packages/client/ui-conversation/src/client/chat/ChatView.tsx
#	packages/client/ui-conversation/src/client/chat/register.ts
#	packages/client/ui-conversation/src/client/contract/slots.ts
#	packages/client/ui-conversation/src/client/contract/views.ts
#	packages/client/ui-conversation/src/client/service.ts
#	packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx
#	packages/client/ui-conversation/src/client/stores.ts
#	packages/client/ui-conversation/tests/skeleton-branches.spec.tsx
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/host/runtime/src/boot.ts
#	packages/host/runtime/tests/host-runtime.spec.ts
#	pnpm-lock.yaml
This commit is contained in:
Yichen Jiang
2026-07-23 19:51:50 +08:00
390 changed files with 18322 additions and 5565 deletions

View File

@@ -60,7 +60,7 @@ import {
} from '@deepseek-ai/dsh-agent'
import type {} from '@deepseek-ai/dsh-commands'
import { encodeSessionReferenceUri } from '@deepseek-ai/dsh-session-reference'
import { displayPromptContent, SessionId } from '@deepseek-ai/dsh-session'
import { displayPromptContent, SessionId, type JsonValue } from '@deepseek-ai/dsh-session'
// Side-effect type import: resolves `ctx.get('permission')` to the service.
import type {} from '@deepseek-ai/dsh-permission'
import type { SessionEvent, TodoItem, TurnEndReason } from '@deepseek-ai/dsh-session'
@@ -1499,7 +1499,7 @@ export class ToolPresenter {
* @param meta - the result's machine-readable meta, forwarded when present.
* @returns a normalized tool-owned view or raw-content fallback.
*/
result(callId: CallId, content: ContentBlock[], isError: boolean, meta?: unknown): ToolResultView {
result(callId: CallId, content: ContentBlock[], isError: boolean, meta?: JsonValue): ToolResultView {
const call = this.pending.get(callId)
this.pending.delete(callId)
// No remembered call (unknown/late callId) → nothing to present from; raw content.

View File

@@ -12,6 +12,11 @@ import FsLocal from '@deepseek-ai/dsh-fs-local'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import { streamSessionEventUpdate, agentOptions, todosToPlan, ToolPresenter } from '../src/index.ts'
const UNUSED_TOOL_OUTPUT: ToolDefinition['output'] = {
schema: { type: 'null' },
render: () => [],
}
/** Collect the updates a single event produces (no presenter → generic fallback). */
function updatesFor(event: SessionEvent): SessionNotification['update'][] {
const out: SessionNotification['update'][] = []
@@ -279,6 +284,7 @@ describe('ToolPresenter (tool-owned presentation via the tool registry)', () =>
name: 'bash',
description: 'run a command',
parameters: {},
output: UNUSED_TOOL_OUTPUT,
execute: async () => [],
presentCall: (args: unknown) => {
const a = args as { command: string; description: string }
@@ -336,7 +342,7 @@ describe('ToolPresenter (tool-owned presentation via the tool registry)', () =>
})
it('a tool with no presentCall/presentResult gets the generic fallback (title = name)', () => {
const plain: ToolDefinition = { name: 'plain', description: 'p', parameters: {}, execute: async () => [] }
const plain: ToolDefinition = { name: 'plain', description: 'p', parameters: {}, output: UNUSED_TOOL_OUTPUT, execute: async () => [] }
const presenter = new ToolPresenter(registryOf(plain))
const [update] = updatesWith(presenter, evt('tool/call', {
turn: 1, step: 1, callId: CallId('c1'), name: 'plain', arguments: '{"a":1}',
@@ -352,6 +358,7 @@ describe('ToolPresenter (tool-owned presentation via the tool registry)', () =>
name: 'mini',
description: 'm',
parameters: {},
output: UNUSED_TOOL_OUTPUT,
execute: async () => [],
presentCall: () => ({ card: 'generic', title: 'Doing a thing' }),
presentResult: () => ({ card: 'generic', title: 'Did the thing' }),
@@ -398,6 +405,7 @@ describe('ToolPresenter (tool-owned presentation via the tool registry)', () =>
name: 'boom',
description: 'b',
parameters: {},
output: UNUSED_TOOL_OUTPUT,
execute: async () => [],
presentCall: () => { throw new Error('call boom') },
presentResult: () => { throw new Error('result boom') },
@@ -427,6 +435,7 @@ describe('ToolPresenter (tool-owned presentation via the tool registry)', () =>
name: 'boom',
description: 'b',
parameters: {},
output: UNUSED_TOOL_OUTPUT,
execute: async () => [],
presentCall: () => { throw new Error('call boom') },
presentResult: () => { throw new Error('result boom') },
@@ -450,6 +459,7 @@ describe('ToolPresenter (tool-owned presentation via the tool registry)', () =>
name: 'rogue',
description: 'r',
parameters: {},
output: UNUSED_TOOL_OUTPUT,
execute: async () => [],
// A card value outside the union — forced with a cast (no valid input reaches this).
presentCall: () => ({ card: 'chart', title: 'nope' }) as unknown as ReturnType<NonNullable<ToolDefinition['presentCall']>>,
@@ -468,6 +478,7 @@ describe('ToolPresenter (tool-owned presentation via the tool registry)', () =>
name: 'rogue',
description: 'r',
parameters: {},
output: UNUSED_TOOL_OUTPUT,
execute: async () => [],
presentCall: () => ({ card: 'generic', title: 'r' }),
presentResult: () => ({ card: 'chart' }) as unknown as ReturnType<NonNullable<ToolDefinition['presentResult']>>,
@@ -530,6 +541,7 @@ describe('terminal-card mapping (capability-gated)', () => {
name: 'bash',
description: 'run a command',
parameters: {},
output: UNUSED_TOOL_OUTPUT,
execute: async () => [],
presentCall: (args: unknown) => {
const command = (args as { command: string }).command
@@ -691,6 +703,7 @@ describe('terminal-card mapping (capability-gated)', () => {
name: 'bash',
description: 'run a command',
parameters: {},
output: UNUSED_TOOL_OUTPUT,
execute: async () => [],
presentCall: (args: unknown) => ({ card: 'terminal', title: (args as { command: string }).command }),
}
@@ -713,6 +726,7 @@ describe('diff-card mapping', () => {
name: 'writer',
description: 'writes a file',
parameters: {},
output: UNUSED_TOOL_OUTPUT,
execute: async () => [],
presentCall: () => view as ReturnType<NonNullable<ToolDefinition['presentCall']>>,
})
@@ -848,6 +862,7 @@ describe('result-time diff card (REAL fs edit tool → tool_call_update diff blo
name: 'writer',
description: 'writes a file',
parameters: {},
output: UNUSED_TOOL_OUTPUT,
execute: async () => [],
presentCall: () => ({ card: 'diff', title: 'Write x', diffs: [{ path: 'x', oldText: null, newText: 'y' }] }),
presentResult: () => ({ card: 'diff', diffs: [] }),

View File

@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import {
errorResponse,
@@ -63,7 +63,7 @@ describe('acp bridge — turn outcomes', () => {
storageDir,
script: [toolCallResponse('c1', 'bash', { command: 'echo hi' }), textResponse('done')],
})
harness.ctx.tools.register(defineTool({
harness.ctx.tools.register(defineContentToolFixture({
name: 'bash',
description: 'run a command',
parameters: { command: { type: 'string' } },
@@ -204,7 +204,7 @@ describe('acp bridge — turn outcomes', () => {
storageDir,
script: [toolCallResponse('c1', 'kaboom', { x: 1 }), textResponse('done')],
})
harness.ctx.tools.register(defineTool({
harness.ctx.tools.register(defineContentToolFixture({
name: 'kaboom',
description: 'explodes when presented',
parameters: { x: { type: 'number' } },
@@ -227,7 +227,7 @@ describe('acp bridge — turn outcomes', () => {
storageDir,
script: [toolCallResponse('c1', 'bash', { command: 'boom' }), textResponse('ok')],
})
harness.ctx.tools.register(defineTool({
harness.ctx.tools.register(defineContentToolFixture({
name: 'bash',
description: 'run a command',
parameters: { command: { type: 'string' } },