Skip to content

Commit 7c60cdc

Browse files
committed
Release 1.0.10: Timeline edit cards show Grok Build-style before/after diff snippets
- DiffSnippet renders @@ hunks with old/new line numbers and tinted +/- gutters - parseUnifiedDiff now exposes classified hunk lines (shared canonical model) - isEdit flag threads edit-diff detection from describeUpdate through the reducer - adds tests for line classification, \\ No newline markers, and flag lifecycle
1 parent 840b865 commit 7c60cdc

16 files changed

Lines changed: 496 additions & 7 deletions

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.9",
3+
"version": "1.0.10",
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.9"
3+
version = "1.0.10"
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.9",
5+
"version": "1.0.10",
66
"identifier": "com.pinkcode.app",
77
"build": {
88
"beforeDevCommand": "npm run dev",

src/components/DiffSnippet.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { memo, useMemo } from "react";
2+
import { buildDiffSnippet } from "../utils/diffSnippet";
3+
4+
/**
5+
* Grok Build-style before/after snippet for an edited file: @@ hunk headers,
6+
* old/new line-number gutters, and tinted +/- lines.
7+
*
8+
* Falls back to the raw patch text when the detail carries no parseable hunk
9+
* (callers reuse the same `detail` for tool cards that are not edit diffs).
10+
*/
11+
export const DiffSnippet = memo(function DiffSnippet({
12+
patch,
13+
}: {
14+
patch: string;
15+
}) {
16+
const snippet = useMemo(() => buildDiffSnippet(patch), [patch]);
17+
18+
if (!snippet) {
19+
return <pre className="tl-diff-fallback">{patch}</pre>;
20+
}
21+
22+
return (
23+
<div className="tl-diff" aria-label="Edited file diff">
24+
{snippet.rows.map((row, i) =>
25+
row.tag === "hunk" ? (
26+
<div key={i} className="tl-diff-hunk">
27+
{row.text}
28+
</div>
29+
) : (
30+
<div
31+
key={i}
32+
className={`tl-diff-line tl-diff-${row.tag}`}
33+
>
34+
<span className="tl-diff-num" aria-hidden>
35+
{row.oldLine ?? ""}
36+
</span>
37+
<span className="tl-diff-num" aria-hidden>
38+
{row.newLine ?? ""}
39+
</span>
40+
<span className="tl-diff-sign" aria-hidden>
41+
{row.tag === "insert" ? "+" : row.tag === "delete" ? "−" : ""}
42+
</span>
43+
<span className="tl-diff-text">{row.text}</span>
44+
</div>
45+
),
46+
)}
47+
{snippet.truncated && (
48+
<div className="tl-diff-trunc" aria-hidden>
49+
50+
</div>
51+
)}
52+
</div>
53+
);
54+
});

src/components/SessionDetail.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import type { ResolvePermissionFn } from "../utils/permissionPayload";
3434
import type { PromptQueueController } from "../hooks/usePromptQueueController";
3535
import { extractToolPath } from "../utils/paths";
3636
import { DiffPanel } from "./DiffPanel";
37+
import { DiffSnippet } from "./DiffSnippet";
3738
import { FilePathLink } from "./FilePathLink";
3839
import { Markdown } from "./Markdown";
3940
import { PermissionGate } from "./PermissionGate";
@@ -794,6 +795,8 @@ const LiveItemRow = memo(function LiveItemRow({
794795
<Markdown onOpenFile={onOpenFile}>{item.detail}</Markdown>
795796
) : isMdKind ? (
796797
<pre className="tl-stream-plain">{item.detail}</pre>
798+
) : item.isEdit && item.detail ? (
799+
<DiffSnippet patch={item.detail} />
797800
) : toolPath && item.detail === toolPath ? (
798801
<FilePathLink
799802
path={toolPath}

src/hooks/liveTimeline.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,65 @@ describe("live timeline reducer", () => {
568568
expect(item?.streaming).toBe(true);
569569
});
570570

571+
it("flags tool cards as isEdit once a diff lands and keeps it on status-only updates", () => {
572+
const indexes = createTimelineReducerState();
573+
const card = (patch?: string) => ({
574+
kind: "tool" as const,
575+
title: "Edit `src/main.ts`",
576+
toolCallId: "call-edit-1",
577+
toolBase: "Edit `src/main.ts`",
578+
...(patch ? { detail: patch, isEdit: true } : { toolStatus: "pending" }),
579+
});
580+
let state = reduceAgentUpdate(
581+
new Map(),
582+
{
583+
handleId: "handle",
584+
sessionId: "session",
585+
description: card(),
586+
now: 1,
587+
nextId: () => "one",
588+
},
589+
indexes,
590+
);
591+
expect(state.get("session")?.[0].isEdit).toBeUndefined();
592+
593+
state = reduceAgentUpdate(
594+
state,
595+
{
596+
handleId: "handle",
597+
sessionId: "session",
598+
description: card("@@ -4,3 +4,3 @@\n let x = 1;\n-let x = 2;\n+let x = 3;"),
599+
now: 2,
600+
nextId: () => "two",
601+
},
602+
indexes,
603+
);
604+
const item = state.get("session")?.[0];
605+
expect(item?.isEdit).toBe(true);
606+
expect(item?.detail).toContain("@@ -4,3 +4,3 @@");
607+
608+
// Status-only follow-up (no diff payload) must not clear the flag.
609+
state = reduceAgentUpdate(
610+
state,
611+
{
612+
handleId: "handle",
613+
sessionId: "session",
614+
description: {
615+
kind: "tool",
616+
title: "Edit `src/main.ts` ✓",
617+
toolCallId: "call-edit-1",
618+
toolBase: "Edit `src/main.ts`",
619+
toolStatus: "completed",
620+
},
621+
now: 3,
622+
nextId: () => "three",
623+
},
624+
indexes,
625+
);
626+
expect(state.get("session")?.[0].isEdit).toBe(true);
627+
expect(state.get("session")?.[0].detail).toContain("@@ -4,3 +4,3 @@");
628+
});
629+
571630
it("merges shell snapshots without regressing output", () => {
572631
const indexes = createTimelineReducerState();
573632
let state = reduceShellUpdate(

src/hooks/liveTimeline.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,10 @@ 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,
408412
ts: now,
409413
};
410414
next.set(key, list);
@@ -508,6 +512,7 @@ export function reduceAgentUpdate(
508512
toolCallId: description.toolCallId,
509513
toolBase: description.toolBase,
510514
toolStatus: description.toolStatus,
515+
isEdit: description.isEdit,
511516
ts: now,
512517
sourceEventId: sourceEventId ?? undefined,
513518
streaming: markStreaming,

src/styles/detail.css

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,89 @@
658658
line-height: 1.6;
659659
}
660660

661+
/* Edited-file diff snippet (Grok Build Edit block: @@ header + numbered +/- gutters). */
662+
.tl-diff {
663+
margin: 6px 0 2px;
664+
border: 1px solid var(--border);
665+
border-radius: 6px;
666+
overflow: hidden;
667+
background: var(--bg-elevated);
668+
font-family: var(--mono);
669+
font-size: 11.5px;
670+
line-height: 1.55;
671+
}
672+
673+
.tl-diff-hunk {
674+
padding: 2px 10px;
675+
color: var(--text-muted);
676+
background: var(--hl-low);
677+
border-bottom: 1px solid var(--border);
678+
white-space: pre;
679+
overflow-x: auto;
680+
}
681+
682+
.tl-diff-line {
683+
display: flex;
684+
align-items: baseline;
685+
white-space: pre;
686+
}
687+
688+
.tl-diff-num {
689+
flex: 0 0 auto;
690+
width: 3.2ch;
691+
text-align: right;
692+
padding: 0 6px 0 2px;
693+
color: var(--text-muted);
694+
opacity: 0.72;
695+
user-select: none;
696+
}
697+
698+
.tl-diff-sign {
699+
flex: 0 0 auto;
700+
width: 1.4ch;
701+
text-align: center;
702+
user-select: none;
703+
}
704+
705+
.tl-diff-text {
706+
flex: 1 1 auto;
707+
padding-left: 6px;
708+
white-space: pre-wrap;
709+
word-break: break-word;
710+
}
711+
712+
.tl-diff-insert {
713+
background: color-mix(in srgb, var(--add) 10%, transparent);
714+
}
715+
716+
.tl-diff-insert .tl-diff-text {
717+
color: var(--add);
718+
}
719+
720+
.tl-diff-delete {
721+
background: color-mix(in srgb, var(--del) 10%, transparent);
722+
}
723+
724+
.tl-diff-delete .tl-diff-text {
725+
color: var(--del);
726+
}
727+
728+
.tl-diff-trunc {
729+
padding: 2px 10px;
730+
color: var(--text-muted);
731+
text-align: center;
732+
}
733+
734+
/* Raw-patch fallback when a diff card's detail is not a parseable hunk. */
735+
.tl-diff-fallback {
736+
margin: 0;
737+
font-family: var(--mono);
738+
font-size: 11.5px;
739+
line-height: 1.55;
740+
white-space: pre-wrap;
741+
word-break: break-word;
742+
}
743+
661744
/* Copy: always visible, bottom-right, plain small text (no chrome).
662745
Absolute — does not reserve layout space or change body line-height. */
663746
.tl-detail.has-copy {

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,8 @@ export interface TimelineItem {
490490
toolBase?: string;
491491
/** Tool card ACP status (pending / completed / …). */
492492
toolStatus?: string;
493+
/** True when `detail` is a unified diff of a modified file (render as snippet). */
494+
isEdit?: boolean;
493495
/** Present when kind is `"shell"`. */
494496
shell?: TimelineShellPayload;
495497
/** Present when kind is `"subagent"`. */

0 commit comments

Comments
 (0)