From 2a2423ce03d190d863bee223e050ddcb13f61e2e Mon Sep 17 00:00:00 2001 From: Lihua <1017343802@qq.com> Date: Sun, 18 Jan 2026 00:28:26 +0800 Subject: [PATCH] feat(tui): add minimal diff display mode Add tui.diff_display config option to collapse Edit/Write tool diffs by default, showing only file name and line statistics. - Add diff_display config (full | minimal) to Config.TUI - Add parseDiffStats() and formatDiffStats() utility functions - Enhance Edit tool with collapse/expand functionality - Enhance Write tool with collapse/expand functionality - Add comprehensive unit tests (12 tests, all passing) In minimal mode: - Edit: shows "+N, -M lines (press Enter to expand)" - Write: shows "N lines (press Enter to expand)" - Click to toggle between collapsed and expanded views - Full diff remains in metadata for AI context Fixes #9089 --- .../src/cli/cmd/tui/routes/session/index.tsx | 198 +++++++++++++----- .../opencode/src/cli/cmd/tui/util/diff.ts | 48 +++++ packages/opencode/src/config/config.ts | 4 + .../test/cli/cmd/tui/util/diff.test.ts | 98 +++++++++ 4 files changed, 298 insertions(+), 50 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/util/diff.ts create mode 100644 packages/opencode/test/cli/cmd/tui/util/diff.test.ts diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 1d64a2ff156..c9036d0d978 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -28,6 +28,7 @@ import { } from "@opentui/core" import { Prompt, type PromptRef } from "@tui/component/prompt" import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk/v2" +import { parseDiffStats, formatDiffStats } from "@/cli/cmd/tui/util/diff" import { useLocal } from "@tui/context/local" import { Locale } from "@/util/locale" import type { Tool } from "@/tool/tool" @@ -1600,12 +1601,27 @@ function Bash(props: ToolProps) { } function Write(props: ToolProps) { + const ctx = use() const { theme, syntax } = useTheme() + const sync = useSync() + const code = createMemo(() => { if (!props.input.content) return "" return props.input.content }) + // Check if minimal mode is enabled + const diffDisplay = createMemo(() => sync.data.config.tui?.diff_display ?? "full") + + // Calculate line count + const lineCount = createMemo(() => code().split("\n").length) + + // Expanded/collapsed state + const [expanded, setExpanded] = createSignal(false) + + // Should show collapsed view + const showCollapsed = createMemo(() => diffDisplay() === "minimal" && !expanded()) + const diagnostics = createMemo(() => { const filePath = Filesystem.normalizePath(props.input.filePath ?? "") return props.metadata.diagnostics?.[filePath] ?? [] @@ -1614,24 +1630,56 @@ function Write(props: ToolProps) { return ( - - - - - - - {(diagnostic) => ( - - Error [{diagnostic.range.start.line}:{diagnostic.range.start.character}]: {diagnostic.message} - - )} - + { + if (diffDisplay() === "minimal") { + setExpanded(!expanded()) + } + }} + > + + + + + + + {(diagnostic) => ( + + Error [{diagnostic.range.start.line}:{diagnostic.range.start.character}]: {diagnostic.message} + + )} + + + + } + > + {/* Collapsed view */} + + + {lineCount()} lines [press Enter to expand] + + + + + {(diagnostic) => ( + + Error [{diagnostic.range.start.line}:{diagnostic.range.start.character}]: {diagnostic.message} + + )} + + @@ -1781,6 +1829,21 @@ function Edit(props: ToolProps) { const diffContent = createMemo(() => props.metadata.diff) + // Check if minimal mode is enabled + const diffDisplay = createMemo(() => ctx.sync.data.config.tui?.diff_display ?? "full") + + // Parse diff statistics + const diffStats = createMemo(() => { + if (!props.metadata.diff) return null + return parseDiffStats(props.metadata.diff) + }) + + // Expanded/collapsed state + const [expanded, setExpanded] = createSignal(false) + + // Should show collapsed view + const showCollapsed = createMemo(() => diffDisplay() === "minimal" && !expanded()) + const diagnostics = createMemo(() => { const filePath = Filesystem.normalizePath(props.input.filePath ?? "") const arr = props.metadata.diagnostics?.[filePath] ?? [] @@ -1790,39 +1853,74 @@ function Edit(props: ToolProps) { return ( - - - - - - - - {(diagnostic) => ( - - Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}]{" "} - {diagnostic.message} - - )} - + { + if (diffDisplay() === "minimal") { + setExpanded(!expanded()) + } + }} + > + + + + + + + + {(diagnostic) => ( + + Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}]{" "} + {diagnostic.message} + + )} + + + + + } + > + {/* Collapsed view */} + + + {formatDiffStats(diffStats()!)} [press Enter to expand] + + + + + {(diagnostic) => ( + + Error [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}]{" "} + {diagnostic.message} + + )} + + + diff --git a/packages/opencode/src/cli/cmd/tui/util/diff.ts b/packages/opencode/src/cli/cmd/tui/util/diff.ts new file mode 100644 index 00000000000..b1826590181 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/util/diff.ts @@ -0,0 +1,48 @@ +/** + * Diff utility functions for TUI display + */ + +/** + * Parse unified diff format to extract line statistics + * @param diff - Unified diff string + * @returns Object with added and removed line counts + * @example + * parseDiffStats(` + * --- a/file.ts + * +++ b/file.ts + * @@ -1,3 +1,5 @@ + * const x = 1 + * +const y = 2 + * +const z = 3 + * `) + * // Returns: { added: 2, removed: 0 } + */ +export function parseDiffStats(diff: string): { added: number; removed: number } { + const lines = diff.split("\n") + let added = 0 + let removed = 0 + + for (const line of lines) { + if (line.startsWith("+") && !line.startsWith("+++")) added++ + else if (line.startsWith("-") && !line.startsWith("---")) removed++ + } + + return { added, removed } +} + +/** + * Format diff statistics for display + * @param stats - Object with added and removed line counts + * @returns Formatted string like "+5 lines" or "+5, -3 lines" + * @example + * formatDiffStats({ added: 5, removed: 3 }) // "+5, -3 lines" + * formatDiffStats({ added: 0, removed: 3 }) // "-3 lines" + */ +export function formatDiffStats(stats: { added: number; removed: number }): string { + const parts: string[] = [] + if (stats.added > 0) parts.push(`+${stats.added}`) + if (stats.removed > 0) parts.push(`-${stats.removed}`) + + if (parts.length === 0) return "0 lines" + return parts.join(", ") + " lines" +} diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 1574c644d32..93dcc12013a 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -790,6 +790,10 @@ export namespace Config { .enum(["auto", "stacked"]) .optional() .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"), + diff_display: z + .enum(["full", "minimal"]) + .optional() + .describe("Control diff display mode: 'full' shows complete diff, 'minimal' shows collapsed by default with line statistics"), }) export const Server = z diff --git a/packages/opencode/test/cli/cmd/tui/util/diff.test.ts b/packages/opencode/test/cli/cmd/tui/util/diff.test.ts new file mode 100644 index 00000000000..e83db793d13 --- /dev/null +++ b/packages/opencode/test/cli/cmd/tui/util/diff.test.ts @@ -0,0 +1,98 @@ +import { describe, test, expect } from "bun:test" +import { parseDiffStats, formatDiffStats } from "@/cli/cmd/tui/util/diff" + +describe("diff utility functions", () => { + describe("parseDiffStats", () => { + test("parses added lines", () => { + const diff = ` +--- a/test.ts ++++ b/test.ts +@@ -1,3 +1,5 @@ + const x = 1 ++const y = 2 ++const z = 3 +` + expect(parseDiffStats(diff)).toEqual({ added: 2, removed: 0 }) + }) + + test("parses removed lines", () => { + const diff = ` +--- a/test.ts ++++ b/test.ts +@@ -1,3 +1,2 @@ + const x = 1 +-const y = 2 +` + expect(parseDiffStats(diff)).toEqual({ added: 0, removed: 1 }) + }) + + test("parses mixed changes", () => { + const diff = ` +--- a/test.ts ++++ b/test.ts +@@ -1,5 +1,6 @@ + const x = 1 +-const old = 2 ++const new = 2 ++const extra = 3 +` + expect(parseDiffStats(diff)).toEqual({ added: 2, removed: 1 }) + }) + + test("handles empty diff", () => { + expect(parseDiffStats("")).toEqual({ added: 0, removed: 0 }) + }) + + test("ignores diff headers", () => { + const diff = ` +--- a/test.ts ++++ b/test.ts +@@ -1,3 +1,5 @@ +-old line ++new line +` + expect(parseDiffStats(diff)).toEqual({ added: 1, removed: 1 }) + }) + + test("handles multi-hunk diff", () => { + const diff = ` +--- a/test.ts ++++ b/test.ts +@@ -1,3 +1,4 @@ + const x = 1 ++const y = 2 +@@ -5,3 +6,5 @@ + const a = 1 ++const b = 2 +-const c = 3 +` + expect(parseDiffStats(diff)).toEqual({ added: 2, removed: 1 }) + }) + }) + + describe("formatDiffStats", () => { + test("formats added lines", () => { + expect(formatDiffStats({ added: 5, removed: 0 })).toBe("+5 lines") + }) + + test("formats removed lines", () => { + expect(formatDiffStats({ added: 0, removed: 3 })).toBe("-3 lines") + }) + + test("formats mixed changes", () => { + expect(formatDiffStats({ added: 5, removed: 3 })).toBe("+5, -3 lines") + }) + + test("formats no changes", () => { + expect(formatDiffStats({ added: 0, removed: 0 })).toBe("0 lines") + }) + + test("formats single added line", () => { + expect(formatDiffStats({ added: 1, removed: 0 })).toBe("+1 lines") + }) + + test("formats single removed line", () => { + expect(formatDiffStats({ added: 0, removed: 1 })).toBe("-1 lines") + }) + }) +})