From 844410e37ca77ab9b4c0078f7b2dd3be28ffb1cb Mon Sep 17 00:00:00 2001 From: Gregory Linford Date: Fri, 15 May 2026 23:23:10 +0200 Subject: [PATCH] feat(tui): show sidebar file diff totals --- .../cmd/tui/feature-plugins/sidebar/files.tsx | 17 +++++ .../test/cli/tui/sidebar-files.test.tsx | 73 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 packages/opencode/test/cli/tui/sidebar-files.test.tsx diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx index 8951bdcab016..df0647510e5c 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx @@ -8,6 +8,15 @@ function View(props: { api: TuiPluginApi; session_id: string }) { const [open, setOpen] = createSignal(true) const theme = () => props.api.theme.current const list = createMemo(() => props.api.state.session.diff(props.session_id)) + const totals = createMemo(() => + list().reduce( + (acc, item) => ({ + additions: acc.additions + item.additions, + deletions: acc.deletions + item.deletions, + }), + { additions: 0, deletions: 0 }, + ), + ) return ( 0}> @@ -19,6 +28,14 @@ function View(props: { api: TuiPluginApi; session_id: string }) { Modified Files + + + +{totals().additions} + + + -{totals().deletions} + + diff --git a/packages/opencode/test/cli/tui/sidebar-files.test.tsx b/packages/opencode/test/cli/tui/sidebar-files.test.tsx new file mode 100644 index 000000000000..f7bb296f6333 --- /dev/null +++ b/packages/opencode/test/cli/tui/sidebar-files.test.tsx @@ -0,0 +1,73 @@ +/** @jsxImportSource @opentui/solid */ +import { expect, test } from "bun:test" +import { testRender } from "@opentui/solid" +import type { TuiPluginApi, TuiSlotPlugin } from "@opencode-ai/plugin/tui" +import SidebarFiles from "@/cli/cmd/tui/feature-plugins/sidebar/files" +import { createTuiPluginApi } from "../../fixture/tui-plugin" + +test("sidebar files renders aggregate additions and deletions in the header", async () => { + let sidebarContent: TuiSlotPlugin["slots"]["sidebar_content"] + const base = createTuiPluginApi({ + state: { + session: { + diff(sessionID) { + if (sessionID !== "ses_test") return [] + return [ + { file: "src/one.ts", additions: 2, deletions: 1 }, + { file: "src/two.ts", additions: 4, deletions: 0 }, + { file: "src/three.ts", additions: 0, deletions: 3 }, + ] + }, + }, + }, + }) + const api: TuiPluginApi = { + ...base, + slots: { + register(plugin: TuiSlotPlugin) { + sidebarContent = plugin.slots.sidebar_content + return "internal:sidebar-files" + }, + }, + } + + await SidebarFiles.tui(api, undefined, { + id: "internal:sidebar-files", + source: "internal", + spec: "internal:sidebar-files", + target: "internal:sidebar-files", + first_time: 0, + last_time: 0, + time_changed: 0, + load_count: 1, + fingerprint: "internal", + state: "same", + }) + + if (!sidebarContent) throw new Error("sidebar files slot was not registered") + const renderSidebarContent = sidebarContent + + const app = await testRender( + () => ( + + {renderSidebarContent({ theme: api.theme }, { session_id: "ses_test" })} + + ), + { + width: 80, + height: 8, + }, + ) + + try { + await app.renderOnce() + + const frame = app.captureCharFrame().replace(/ +/g, " ") + expect(frame).toContain("Modified Files +6 -4") + expect(frame).toContain("src/one.ts") + expect(frame).toContain("+2") + expect(frame).toContain("-1") + } finally { + app.renderer.destroy() + } +})