From cb6a6486a763462e30bc460107ca714d01d3c321 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Wed, 19 Aug 2026 02:41:27 -0700 Subject: [PATCH] fix(diff): drop the phantom trailing row from every edit's diff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An edit rendered a fourth row past the end of a three-line file: ⎿ Added 1 line, removed 1 line 1 alpha 2 -beta 2 +BETA-changed 3 gamma 4 No newline at end of file `\ No newline at end of file` is a patch MARKER, not a line, and the row renderer has no marker for it — `\` parses as an unknown prefix, so it came out as an ordinary context line, numbered one past the file. It is also not true. The diff is computed over the tool's applied HUNK, not the whole file: a fragment that stops mid-file has no trailing newline by construction, and `alpha\nbeta\ngamma\n` on disk is perfectly fine. So the row asserted something false about the user's file, in the one place a reader looks to see what changed. Filtered where the hunks are built, which is the only producer, so replay gets the same shape. Upstream renders no such row; the diff is now byte-identical to what Claude Code 2.1.235 shows for the same edit. Also tidies the gallery's mutation scenarios: `write` and `edit` each own their own file and the leftovers are cleared at bootstrap, so `edit` exercises the diff card instead of failing on yesterday's file, and it reads before editing the way the harness requires. Co-Authored-By: Claude Opus 5 --- package-lock.json | 4 ++-- package.json | 2 +- scripts/tool-gallery.py | 15 +++++++++++---- src/__tests__/harnessClient.test.ts | 15 ++++++++++++++- src/harness/client.ts | 8 +++++++- 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85d1123..350c4c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dsh-cctui", - "version": "0.3.9", + "version": "0.3.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dsh-cctui", - "version": "0.3.9", + "version": "0.3.10", "license": "MIT", "dependencies": { "@dsh-cctui/ink": "file:./packages/dsh-cctui-ink", diff --git a/package.json b/package.json index 55630cb..c05319d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-cctui", - "version": "0.3.9", + "version": "0.3.10", "description": "Claude-Code-style terminal UI for deepseek-harness, ported from clawcodex ui-tui", "license": "MIT", "repository": { diff --git a/scripts/tool-gallery.py b/scripts/tool-gallery.py index 47b1c3b..242832c 100755 --- a/scripts/tool-gallery.py +++ b/scripts/tool-gallery.py @@ -58,10 +58,13 @@ "glob": 'PROBE glob {"pattern": "src/components/*.tsx"}', "bash": 'PROBE bash {"command": "ls -1 src | head -12", "description": "list src"}', "bash_fail": 'PROBE bash {"command": "ls /no/such/dir", "description": "failing ls"}', - "write": 'PROBE write {"file_path": "e2e-scratch/gallery.txt", "content": "alpha\\nbeta\\ngamma\\n"}', + "write": 'PROBE write {"file_path": "e2e-scratch/gallery-write.txt", "content": "alpha\\nbeta\\ngamma\\n"}', + # the harness refuses to edit a file this session has not observed, so the + # scenario writes it, reads it, then edits it "edit": [ - 'PROBE write {"file_path": "e2e-scratch/gallery.txt", "content": "alpha\\nbeta\\ngamma\\n"}', - 'PROBE edit {"file_path": "e2e-scratch/gallery.txt", "old_string": "beta", "new_string": "BETA-changed"}', + 'PROBE write {"file_path": "e2e-scratch/gallery-edit.txt", "content": "alpha\\nbeta\\ngamma\\n"}', + 'PROBE read {"file_path": "e2e-scratch/gallery-edit.txt"}', + 'PROBE edit {"file_path": "e2e-scratch/gallery-edit.txt", "old_string": "beta", "new_string": "BETA-changed"}', ], # reaches the network on purpose — the fetch card is a retrieval summary "web_fetch": 'PROBE web_fetch {"url": "https://example.com"}', @@ -83,7 +86,8 @@ ), "skill": 'PROBE skill {"name": "nonexistent-skill"}', "read_image": 'PROBE read_image {"file_path": "e2e-scratch/gallery.png"}', - "str_replace_editor": 'PROBE str_replace_editor {"command": "view", "path": "src/domain/usage.ts"}', + # this one insists on an absolute path + "str_replace_editor": ('PROBE str_replace_editor {"command": "view", "path": "%s/src/domain/usage.ts"}' % ROOT), "job_output": 'PROBE job_output {"job_id": "no-such-job"}', "send_message": 'PROBE send_message {"subagent_id": "no-such-agent", "message": "hello"}', "get_goal": 'PROBE get_goal {}', @@ -111,6 +115,9 @@ def bootstrap() -> None: + # the mutation scenarios must CREATE their file, not find yesterday's + for leftover in (ROOT / "e2e-scratch").glob("gallery-*.txt"): + leftover.unlink() PROFILE.mkdir(parents=True, exist_ok=True) (PROFILE / "package.json").write_text( json.dumps( diff --git a/src/__tests__/harnessClient.test.ts b/src/__tests__/harnessClient.test.ts index fac9ed5..0fc4d45 100644 --- a/src/__tests__/harnessClient.test.ts +++ b/src/__tests__/harnessClient.test.ts @@ -51,7 +51,10 @@ const TOOL_CARDS: Record = { path: 'src/y.ts', totalLines: 40 }, - write: { card: 'diff', diffs: [{ newText: 'line one\nline two\n', oldText: null, path: 'notes.txt' }] } + write: { card: 'diff', diffs: [{ newText: 'line one\nline two\n', oldText: null, path: 'notes.txt' }] }, + // an APPLIED HUNK rather than a whole file: it stops mid-file, so neither + // side carries a trailing newline + write_hunk: { card: 'diff', diffs: [{ newText: 'beta\nBETA', oldText: 'beta\nbeta', path: 'notes.txt' }] } } function makeWorld() { @@ -1024,6 +1027,16 @@ describe('HarnessGatewayClient', () => { expect(usage.context_percent).toBe(5) }) + it('drops the no-newline marker a hunk-shaped diff always produces', async () => { + const done = await runTool('write_hunk', '{"file_path":"notes.txt"}') + const lines = done.payload.structured_diff?.hunks.flatMap(hunk => hunk.lines) ?? [] + + // the row renderer has no marker for `\ No newline at end of file`, so it + // came out as a phantom context line numbered one past the end + expect(lines.length).toBeGreaterThan(0) + expect(lines.some(line => line.startsWith('\\'))).toBe(false) + }) + it('converts diff presentation views into structured_diff payloads', async () => { const w = makeWorld() diff --git a/src/harness/client.ts b/src/harness/client.ts index 057ca37..3fd22d3 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -1092,7 +1092,13 @@ export class HarnessGatewayClient extends GatewayClient { const structuredDiff: StructuredDiffPayload = { filePath: first.path, hunks: patch.hunks.map(h => ({ - lines: h.lines, + // `\ No newline at end of file` is an artifact of diffing the + // tool's applied HUNK rather than the whole file: a fragment that + // stops mid-file has no trailing newline by construction, and the + // file it came from is usually fine. The row renderer has no marker + // for it either, so it came out as a phantom context line numbered + // one past the end. Upstream shows no such row. + lines: h.lines.filter(line => !line.startsWith('\\')), newLines: h.newLines, newStart: h.newStart, oldLines: h.oldLines,