Skip to content

Commit 8198d17

Browse files
committed
Release 1.0.11: fix Edit diff extraction for real ACP wire data
- diffFromContent reads camelCase oldText/newText/_meta (real wire shape); snake_case previews still accepted - isEdit flag follows detail merge semantics: content-less updates with explicit isEdit=false no longer clear a diff card's flag - verified end-to-end against real ~/.grok session updates.jsonl (26 diff cards rendered with @@ headers, line numbers, +/- gutters)
1 parent 7c60cdc commit 8198d17

8 files changed

Lines changed: 86 additions & 13 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pinkcode",
3-
"version": "1.0.10",
3+
"version": "1.0.11",
44
"description": "Desktop mission control for Grok agents.",
55
"type": "module",
66
"keywords": [

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pinkcode"
3-
version = "1.0.10"
3+
version = "1.0.11"
44
description = "Desktop control plane for Grok Build multi-task observability"
55
authors = ["PinkCode"]
66
license = "Apache-2.0"

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "PinkCode",
44
"mainBinaryName": "PinkCode",
5-
"version": "1.0.10",
5+
"version": "1.0.11",
66
"identifier": "com.pinkcode.app",
77
"build": {
88
"beforeDevCommand": "npm run dev",

src/hooks/liveTimeline.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,51 @@ describe("live timeline reducer", () => {
627627
expect(state.get("session")?.[0].detail).toContain("@@ -4,3 +4,3 @@");
628628
});
629629

630+
it("keeps the diff flag when a content-less update carries explicit isEdit=false", () => {
631+
// Real wire shape: tool_call_update with title/status but no content sets
632+
// detail=undefined, isEdit=false; it must not clear the card's diff flag.
633+
const indexes = createTimelineReducerState();
634+
let state = reduceAgentUpdate(
635+
new Map(),
636+
{
637+
handleId: "handle",
638+
sessionId: "session",
639+
description: {
640+
kind: "tool",
641+
title: "Edit `src/main.ts`",
642+
toolCallId: "call-edit-2",
643+
toolBase: "Edit `src/main.ts`",
644+
detail: "@@ -4,3 +4,3 @@\n let x = 1;\n-let x = 2;\n+let x = 3;",
645+
isEdit: true,
646+
},
647+
now: 1,
648+
nextId: () => "one",
649+
},
650+
indexes,
651+
);
652+
state = reduceAgentUpdate(
653+
state,
654+
{
655+
handleId: "handle",
656+
sessionId: "session",
657+
description: {
658+
kind: "tool",
659+
title: "Edit `src/main.ts` ✓",
660+
toolCallId: "call-edit-2",
661+
toolBase: "Edit `src/main.ts`",
662+
// No content on this update; formatToolCardParts yields no detail.
663+
isEdit: false,
664+
},
665+
now: 2,
666+
nextId: () => "two",
667+
},
668+
indexes,
669+
);
670+
const item = state.get("session")?.[0];
671+
expect(item?.detail).toContain("@@ -4,3 +4,3 @@");
672+
expect(item?.isEdit).toBe(true);
673+
});
674+
630675
it("merges shell snapshots without regressing output", () => {
631676
const indexes = createTimelineReducerState();
632677
let state = reduceShellUpdate(

src/hooks/liveTimeline.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -405,10 +405,14 @@ export function reduceAgentUpdate(
405405
toolBase: merged.baseTitle,
406406
toolStatus: merged.status,
407407
toolCallId: description.toolCallId,
408-
// `detail` merges as next-wins/fallback-prev; the diff flag must track
409-
// the same rule so a status-only update keeps the card flagged while a
410-
// completed update with plain detail clears it.
411-
isEdit: description.isEdit ?? prev.isEdit,
408+
// `detail` merges next-wins/fallback-prev (mergeToolCardParts); the diff
409+
// flag follows the same rule: a content-less update (no detail, but an
410+
// explicit isEdit=false) must not clear the flag, while an update that
411+
// brings plain detail replaces both.
412+
isEdit:
413+
description.detail != null
414+
? Boolean(description.isEdit)
415+
: Boolean(prev.isEdit),
412416
ts: now,
413417
};
414418
next.set(key, list);

src/utils/editDiff.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,28 @@ describe("extractEditDiff", () => {
120120
expect(diff).toContain(" keep");
121121
});
122122

123+
it("accepts the real wire camelCase diff blocks (oldText/newText/_meta)", () => {
124+
// Live ACP records serialize Diff blocks as camelCase with snake_case
125+
// line numbers inside `_meta` (verified against ~/.grok sessions).
126+
const diff = extractEditDiff({
127+
kind: "edit",
128+
title: "Edit `src/out.txt`",
129+
content: [
130+
{
131+
type: "diff",
132+
path: "/abs/src/out.txt",
133+
oldText: "line1\nold\n",
134+
newText: "line1\nnew\n",
135+
_meta: { old_line: 1, new_line: 1 },
136+
},
137+
],
138+
});
139+
expect(diff).toContain("@@ -1,2 +1,2 @@");
140+
expect(diff).toContain("-old");
141+
expect(diff).toContain("+new");
142+
expect(diff).toContain(" line1");
143+
});
144+
123145
it("returns undefined for non-edit output", () => {
124146
expect(
125147
extractEditDiff({ kind: "Read", rawOutput: { type: "ReadFile" } }),

src/utils/editDiff.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -282,18 +282,20 @@ function diffFromRawOutput(rawOutput: unknown): string | undefined {
282282
}
283283

284284
/**
285-
* Strategy 2: ACP `content` Diff blocks `{ type: "diff", old_text, new_text, meta }`.
286-
* Full-text fallback (pre-execution previews and the write tool).
285+
* Strategy 2: ACP `content` Diff blocks. The wire ships camelCase
286+
* `{ type: "diff", path, oldText, newText, _meta: {old_line, new_line} }`;
287+
* pre-execution previews and disk logs also carry snake_case `old_text` /
288+
* `new_text` / `meta`. Accept both spellings.
287289
*/
288290
function diffFromContent(content: unknown): string | undefined {
289291
if (!Array.isArray(content)) return undefined;
290292
for (const block of content) {
291293
const b = asRecord(block);
292294
if (!b || b.type !== "diff") continue;
293-
const oldText = str(b.old_text);
294-
const newText = str(b.new_text);
295+
const oldText = str(b.oldText) || str(b.old_text);
296+
const newText = str(b.newText) || str(b.new_text);
295297
if (!oldText && !newText) continue;
296-
const meta = asRecord(b.meta);
298+
const meta = asRecord(b._meta) ?? asRecord(b.meta);
297299
const startLine = num(meta?.new_line) ?? 1;
298300
const hunks = [
299301
trimHunk(

0 commit comments

Comments
 (0)