mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix(client): preserve editor running diffs
This commit is contained in:
+2
-2
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
+3
@@ -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 |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 负责。
|
||||
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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<string, unknown>) => 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' }] },
|
||||
|
||||
Reference in New Issue
Block a user