From 5c427c31ff50e8d3483091bb81357ff5273be972 Mon Sep 17 00:00:00 2001 From: tt-a1i Date: Sun, 23 Aug 2026 10:25:34 +0800 Subject: [PATCH] feat(ui): color edit stats and spin the pending activity row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - edit rows now show Kimi-style diff stats: additions in green, removals in red (+14 -15), and rows without a diff stay quiet instead of showing +0 -0; - the pending row swaps the static ◌ for the package braille spinner, in step with the subagent and workflow views (bash already re-renders every second, so the frame advances); - all three states now share one 8-column verb field, so the target column lines up across pending, success, and failure rows. --- .../file-mutation-display/render.test.ts | 10 +++-- extensions/file-mutation-display/render.ts | 42 ++++++++++++++----- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/extensions/file-mutation-display/render.test.ts b/extensions/file-mutation-display/render.test.ts index ced8e94f..846a0cf4 100644 --- a/extensions/file-mutation-display/render.test.ts +++ b/extensions/file-mutation-display/render.test.ts @@ -114,7 +114,7 @@ const fixtures: Array<{ patch: "@@ -1 +1,2 @@\n-old\n+new\n+second", }, }, - success: /Edited\s+README\.md\s+\+2\s+−1/, + success: /Edited\s+README\.md\s+\+2 -1/, }, { name: "grep", @@ -236,7 +236,11 @@ test("all activity tools render pending and failure as one explicit row", () => const definition = withActivityRenderer(fixture.definition); const pending = renderCollapsed(definition, fixture.args); assert.equal(pending.length, 1, `${fixture.name} pending`); - assert.match(pending[0] ?? "", /^ {2}◌ /, `${fixture.name} pending`); + assert.match( + pending[0] ?? "", + /^ {2}[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] /, + `${fixture.name} pending`, + ); assert.ok(pending[0]?.endsWith(" "), `${fixture.name} pending`); const failed = renderCollapsed( @@ -274,7 +278,7 @@ test("long activity rows stay one line and fit narrow terminals", () => { ); assert.equal(lines.length, 1); assert.ok(visibleWidth(lines[0]!) <= 24); - assert.match(lines[0] ?? "", /^ {2}◌ Running\s+bun/); + assert.match(lines[0] ?? "", /^ {2}[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Running\s+bun/); assert.ok(lines[0]?.endsWith(" ")); }); diff --git a/extensions/file-mutation-display/render.ts b/extensions/file-mutation-display/render.ts index edb96437..fe9c1b07 100644 --- a/extensions/file-mutation-display/render.ts +++ b/extensions/file-mutation-display/render.ts @@ -7,6 +7,7 @@ import type { } from "@earendil-works/pi-coding-agent"; import { truncateToWidth, type Component } from "@earendil-works/pi-tui"; import type { TSchema } from "typebox"; +import { spinnerFrame } from "../shared/spinner.ts"; type ActivityStatus = "pending" | "success" | "error"; @@ -91,13 +92,14 @@ function plural(count: number, singular: string) { function editStats(details: unknown) { const diff = string(record(details).diff); + if (!diff) return undefined; let additions = 0; let removals = 0; for (const line of diff.split(/\r?\n/)) { if (line.startsWith("+") && !line.startsWith("+++")) additions += 1; if (line.startsWith("-") && !line.startsWith("---")) removals += 1; } - return `+${additions} −${removals}`; + return { additions, removals }; } function range(args: Record) { @@ -140,11 +142,7 @@ function activityRow( return { verb: "Wrote", target: path, detail: plural(lines, "line") }; } case "edit": - return { - verb: "Edited", - target: path, - detail: editStats(result?.details), - }; + return { verb: "Edited", target: path }; case "grep": return { verb: "Searched", @@ -242,17 +240,41 @@ function activityText( ) { const row = activityRow(name, args, state.result, cwd); const elapsed = duration(state); + const verbText = ( + state.status === "pending" + ? pendingVerb(name) + : state.status === "error" + ? "Failed" + : row.verb + ).padEnd(8); + const verb = theme.fg( + state.status === "error" ? "error" : "toolTitle", + verbText, + ); if (state.status === "pending") { const detail = elapsed ? ` · ${elapsed}` : ""; - return `${theme.fg("warning", "◌")} ${theme.fg("toolTitle", pendingVerb(name))} ${row.target}${theme.fg("dim", detail)}`; + return `${theme.fg("warning", spinnerFrame(Date.now()))} ${verb} ${row.target}${theme.fg("dim", detail)}`; } if (state.status === "error") { const summary = errorSummary(state.result); const detail = [elapsed, summary].filter(Boolean).join(" · "); - return `${theme.fg("error", "✕")} ${theme.fg("error", "Failed")} ${row.target}${detail ? theme.fg("dim", ` · ${detail}`) : ""}`; + return `${theme.fg("error", "✕")} ${verb} ${row.target}${detail ? theme.fg("dim", ` · ${detail}`) : ""}`; + } + const parts: string[] = []; + if (name === "edit") { + // Kimi-style diff stats: additions green, removals red. + const stats = editStats(state.result?.details); + if (stats) { + parts.push( + `${theme.fg("success", `+${stats.additions}`)} ${theme.fg("error", `-${stats.removals}`)}`, + ); + } + } else if (row.detail) { + parts.push(theme.fg("dim", row.detail)); } - const detail = [row.detail, elapsed].filter(Boolean).join(" · "); - return `${theme.fg("dim", activityIcon(name))} ${theme.fg("toolTitle", row.verb.padEnd(8))} ${row.target}${detail ? theme.fg("dim", ` ${detail}`) : ""}`; + if (elapsed) parts.push(theme.fg("dim", elapsed)); + const detail = parts.join(theme.fg("dim", " · ")); + return `${theme.fg("dim", activityIcon(name))} ${verb} ${row.target}${detail ? ` ${detail}` : ""}`; } function activityComponent(