diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml index 8245fb8396..f8e974a4cd 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml @@ -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 .agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md -2026-08-23-client-derived-tool-presentation.md: 3643c003058f2c17c297606357e35fb43f2e5280 -2026-08-23-client-derived-tool-presentation.zh.md: 31d3a9f082b937dd8d8002bfbc30ef85200ba367 +2026-08-23-client-derived-tool-presentation.md: 957d2e6c1a79cb0b0a246066463e6ac960fb3d5b +2026-08-23-client-derived-tool-presentation.zh.md: 5fa1a350963ff74306520bad6a4fc89765c592a7 diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md index 3643c00305..957d2e6c1a 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md @@ -322,7 +322,10 @@ Standard and persistent providers sharing the same tool name are a special compa |---|---| | running `write` | intended added-only diff from `file_path` and `content` | | running `edit` | intended replacement diff from `file_path`, `old_string`, and `new_string` | +| running `str_replace_editor create` | intended added-only diff from `path` and `file_text` | +| running `str_replace_editor str_replace` | intended replacement diff from `path`, `old_str`, and `new_str` | | successful settled `write`/`edit` | applied contextual hunks from `meta.diffs` | +| settled `str_replace_editor` | Generic, because the tool defines no result presenter | | write create or missing/malformed/empty applied metadata | current argument fallback | | error, malformed arguments, edit with malformed metadata, or Code Dispatch child | Generic | diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md index 31d3a9f082..5fa1a35096 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md @@ -322,7 +322,10 @@ TerminalBlock 的 ANSI、光标重放、宽字符、行数上限、展开、复 |---|---| | running `write` | 从 `file_path` 与 `content` 生成 intended added-only diff | | running `edit` | 从 `file_path`、`old_string`、`new_string` 生成 intended replacement diff | +| running `str_replace_editor create` | 从 `path` 与 `file_text` 生成 intended added-only diff | +| running `str_replace_editor str_replace` | 从 `path`、`old_str` 与 `new_str` 生成 intended replacement diff | | settled `write`/`edit` success | 从 `meta.diffs` 生成 applied contextual hunks | +| settled `str_replace_editor` | Generic,因为该工具没有 result presenter | | write create 或 applied metadata 缺失、畸形、为空 | 当前 args fallback | | error、畸形 args、edit 的 metadata 畸形、Code Dispatch child | Generic | diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml index a9bd41853a..3f895f0eb9 100644 --- a/packages/client/ui-tool/README.i18n.yaml +++ b/packages/client/ui-tool/README.i18n.yaml @@ -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: 16fd06332d24b265ac7d6b7b000870686262b9c5 -README.zh.md: a21acab8ed6e7b52770a9ee6a63219c11b949c3d +README.md: 2db7d716dc80fbf40a953b217810fb8674e2e98f +README.zh.md: 79ed5befe751b329984c1320144921339fdf3d3f diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md index 16fd06332d..2db7d716dc 100644 --- a/packages/client/ui-tool/README.md +++ b/packages/client/ui-tool/README.md @@ -30,7 +30,7 @@ ctx.slots.inject('tool.call.toolview', () => The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the frozen `block`, optional `cwd` and `home`, and plain `openFile`/`inspect` callbacks. A Code Dispatch block retains its event's `parentCallId`; the field is absent on a root Session call, so row and Details card models preserve the generic flattened form for descendants without another placement flag. Path summaries relativize to the Session cwd first, then replace a leftover POSIX Host home with `~`; `filePath` and Host open keep the authored filesystem path. The registration receives the normal Session slot runtime share but no React node or Runtime service. -This package currently owns the generic fallback and the built-in shell/pwsh, read, write/edit, grep/glob, web, todo, question, and Code Dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall`/`presentResult` values never enter the Client. `ui-skill` demonstrates a business-owned registration for `skill`. +This package currently owns the generic fallback and the built-in shell/pwsh, read, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and Code Dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall`/`presentResult` values never enter the Client. `ui-skill` demonstrates a business-owned registration for `skill`. Card-specific limits and fallback rules remain in the owning [terminal](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md), [diff](../../../.agents/notes/implemented/feature/2026-07-30-web-diff-card.md), [read](../../../.agents/notes/implemented/feature/2026-07-30-web-read-card-frontend.md), [search](../../../.agents/notes/implemented/feature/2026-07-30-web-search-card.md), and [web](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.md) notes. diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md index a21acab8ed..79ed5befe7 100644 --- a/packages/client/ui-tool/README.zh.md +++ b/packages/client/ui-tool/README.zh.md @@ -30,7 +30,7 @@ ctx.slots.inject('tool.call.toolview', () => owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block`、可选 `cwd` 与 `home`,以及普通的 `openFile`、`inspect` 回调。Code Dispatch block 保留其事件已有的 `parentCallId`;root Session call 没有该字段,因此 row 与 Details card model 无需另一项 placement 标志即可让 descendant 保持 generic 压平形态。路径摘要先相对 Session cwd 缩短,再把剩余的 POSIX Host home 写成 `~`;`filePath` 与 Host 打开仍使用作者给出的文件系统路径。注册项会收到常规 Session slot runtime share,但不会收到 React node 或 runtime service。 -本包当前拥有 generic fallback,以及 shell/pwsh、read、write/edit、grep/glob、web、todo、question 和 Code Dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall`/`presentResult` 值不会进入 Client。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。 +本包当前拥有 generic fallback,以及 shell/pwsh、read、write/edit、running `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 和 Code Dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall`/`presentResult` 值不会进入 Client。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。 各类卡片的上限与 fallback 规则仍由对应的 [terminal](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.zh.md)、[diff](../../../.agents/notes/implemented/feature/2026-07-30-web-diff-card.zh.md)、[read](../../../.agents/notes/implemented/feature/2026-07-30-web-read-card-frontend.zh.md)、[search](../../../.agents/notes/implemented/feature/2026-07-30-web-search-card.zh.md) 和 [web](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.zh.md) Agent Note 负责。 diff --git a/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts b/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts index eed7826594..03eaf04a3a 100644 --- a/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts +++ b/packages/client/ui-tool/src/client/tool/models/diff-card-model.ts @@ -1,4 +1,4 @@ -/** Pure diff-card derivation from raw write/edit calls and result metadata. @module */ +/** Pure diff-card derivation from raw file-mutation calls and result metadata. @module */ import type { DiffBlockProps, DiffHunk } from '@deepseek-ai/dsh-client-ui-primitives' import type { ToolCallBlock } from './tool-call-model.ts' import { parsedToolCall, validEscalationFields } from './raw-tool-call.ts' @@ -47,11 +47,31 @@ function narrowDiffs(diffs: unknown): DiffHunk[] | null { return out } -type IntendedDiff = { tool: 'write' | 'edit'; diff: DiffHunk } +type IntendedDiff = { tool: 'write' | 'edit' | 'str_replace_editor'; diff: DiffHunk } function intendedDiff(block: ToolCallBlock): IntendedDiff | null { const parsed = parsedToolCall(block) if (parsed === null) return null + if (parsed.name === 'str_replace_editor') { + const { command, path, file_text: fileText, old_str: oldText, new_str: newText } = parsed.args + if (typeof path !== 'string' || path.trim() === '') return null + if (command === 'create') { + if (fileText !== undefined && typeof fileText !== 'string') return null + return { + tool: 'str_replace_editor', + diff: { path, oldText: null, newText: fileText ?? '' }, + } + } + if (command === 'str_replace') { + if (oldText !== undefined && typeof oldText !== 'string') return null + if (newText !== undefined && typeof newText !== 'string') return null + return { + tool: 'str_replace_editor', + diff: { path, oldText: oldText ?? null, newText: newText ?? '' }, + } + } + return null + } const { file_path: path } = parsed.args if (typeof path !== 'string' || path.trim() === '') return null if (!validEscalationFields(parsed.args)) return null @@ -77,9 +97,11 @@ function appliedDiffs(meta: unknown): DiffHunk[] | 'empty' | null { } /** - * Derive intended running or applied settled diffs for a root write/edit call. + * Derive running diffs for root write/edit and `str_replace_editor` + * create/replace calls, plus applied settled diffs for root write/edit calls. * A successful write with valid empty metadata uses its argument-derived - * whole-file diff, matching create and identical-overwrite presentation. + * whole-file diff, matching create and identical-overwrite presentation; + * `str_replace_editor` settles through Generic because it has no result view. * @param block - running or settled Tool block. * @returns the diff-card props, or null for the generic path. */ @@ -88,6 +110,7 @@ export function diffCardModel(block: ToolCallBlock): DiffCardModel | null { const intended = intendedDiff(block) if (intended === null) return null if (!('kind' in block)) return { card: { diffs: [intended.diff] } } + if (intended.tool === 'str_replace_editor') return null if (block.isError) return null const applied = appliedDiffs(block.meta) if (applied === null || applied === 'empty') { diff --git a/packages/client/ui-tool/tests/diff-card.client.spec.tsx b/packages/client/ui-tool/tests/diff-card.client.spec.tsx index 3c113b3d3e..8a263f2983 100644 --- a/packages/client/ui-tool/tests/diff-card.client.spec.tsx +++ b/packages/client/ui-tool/tests/diff-card.client.spec.tsx @@ -63,6 +63,47 @@ describe('diffCardModel', () => { }) }) + it.each([ + { + command: 'create', + args: { command: 'create', path: 'notes/new.txt', file_text: 'new file\n' }, + diff: { path: 'notes/new.txt', oldText: null, newText: 'new file\n' }, + }, + { + command: 'str_replace', + args: { command: 'str_replace', path: 'notes/demo.txt', old_str: 'old', new_str: 'new' }, + diff: { path: 'notes/demo.txt', oldText: 'old', newText: 'new' }, + }, + ])('preserves the running str_replace_editor $command diff', ({ args, diff }) => { + expect(diffCardModel(running({ + name: 'str_replace_editor', + argsRaw: JSON.stringify(args), + }))).toEqual({ card: { diffs: [diff] } }) + }) + + it('preserves str_replace_editor defaults and its settled Generic result', () => { + const argsRaw = JSON.stringify({ command: 'str_replace', path: 'notes/demo.txt' }) + expect(diffCardModel(running({ name: 'str_replace_editor', argsRaw }))).toEqual({ + card: { diffs: [{ path: 'notes/demo.txt', oldText: null, newText: '' }] }, + }) + expect(diffCardModel(settled({ + call: { name: 'str_replace_editor', argsRaw }, + meta: { diffs: [{ path: 'notes/demo.txt', oldText: 'old', newText: 'new' }] }, + }))).toBeNull() + }) + + it('keeps unsupported or malformed str_replace_editor calls generic', () => { + const editor = (args: Record) => running({ + name: 'str_replace_editor', argsRaw: JSON.stringify(args), + }) + expect(diffCardModel(editor({ command: 'view', path: 'notes/demo.txt' }))).toBeNull() + expect(diffCardModel(editor({ command: 'insert', path: 'notes/demo.txt', new_str: 'x' }))).toBeNull() + expect(diffCardModel(editor({ command: 'create', path: '', file_text: 'x' }))).toBeNull() + expect(diffCardModel(editor({ command: 'create', path: 'notes/demo.txt', file_text: 1 }))).toBeNull() + expect(diffCardModel(editor({ command: 'str_replace', path: 'notes/demo.txt', old_str: 1 }))).toBeNull() + expect(diffCardModel(editor({ command: 'str_replace', path: 'notes/demo.txt', new_str: 1 }))).toBeNull() + }) + it('derives a settled card from result metadata, which replaces the intended diff', () => { expect(diffCardModel(settled({ meta: { diffs: [{ path: 'notes/demo.txt', oldText: 'a', newText: 'b' }] },