/** @jsxImportSource @opentui/solid */ import { expect, test } from "bun:test" import path from "path" import { mkdir } from "fs/promises" import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui" import type { DiffRenderable, Renderable, ScrollBoxRenderable } from "@opentui/core" import { testRender, useRenderer } from "@opentui/solid" import { Global } from "@opencode-ai/core/global" import type { TuiPluginApi, TuiPluginMeta, TuiRouteCurrent, TuiRouteDefinition } from "@opencode-ai/plugin/tui" import type { Session } from "@opencode-ai/sdk/v2" import { KVProvider } from "../../../src/cli/cmd/tui/context/kv" import { ThemeProvider } from "../../../src/cli/cmd/tui/context/theme" import { TuiConfigProvider } from "../../../src/cli/cmd/tui/context/tui-config" import { TuiKeybind } from "../../../src/cli/cmd/tui/config/keybind" import { OpencodeKeymapProvider } from "../../../src/cli/cmd/tui/keymap" import diffViewerPlugin from "../../../src/cli/cmd/tui/feature-plugins/system/diff-viewer" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" test("closing the diff viewer returns to the route it opened from", async () => { const viewer = await renderDiffViewer([]) try { expect(viewer.current()).toEqual({ name: "diff", params: { mode: "git", sessionID: "session-1", returnRoute: startRoute }, }) expect(viewer.vcsDiffInput()).toEqual({ directory: "/repo/session", mode: "git", context: 12 }) expect(viewer.commands.has("diff.close")).toBe(true) viewer.commands.get("diff.close")!.run?.({} as never) expect(viewer.current()).toEqual(startRoute) } finally { viewer.app.renderer.destroy() } }) test("brackets navigate diff hunks", async () => { const viewer = await renderDiffViewer( [ { file: "src/file.ts", additions: 3, deletions: 3, status: "modified", patch: `--- a/src/file.ts +++ b/src/file.ts @@ -1,3 +1,3 @@ const first = true -const oldFirst = true +const newFirst = true const afterFirst = true @@ -20,3 +20,3 @@ const second = true -const oldSecond = true +const newSecond = true const afterSecond = true @@ -40,3 +40,3 @@ const third = true -const oldThird = true +const newThird = true const afterThird = true`, }, ], 12, ) try { await viewer.app.waitForFrame((frame) => frame.includes("const first")) await viewer.app.waitFor(() => Boolean(findRenderable(viewer.app.renderer.root, "diff-viewer-patches"))) await viewer.app.flush() const scroll = findRenderable(viewer.app.renderer.root, "diff-viewer-patches") as ScrollBoxRenderable const diff = findRenderable(viewer.app.renderer.root, "diff-viewer-patch-0") as DiffRenderable expect(diff.getHunkRowOffsets()).toEqual([0, 4, 8]) const initial = scroll.scrollTop expect(TuiKeybind.defaultValue("diff_next_hunk")).toBe("]") expect(TuiKeybind.defaultValue("diff_previous_hunk")).toBe("[") viewer.commands.get("diff.next_hunk")!.run?.({} as never) await viewer.app.renderOnce() const first = scroll.scrollTop expect(first).toBeGreaterThan(initial) viewer.commands.get("diff.next_hunk")!.run?.({} as never) await viewer.app.renderOnce() const second = scroll.scrollTop expect(second).toBeGreaterThan(first) viewer.commands.get("diff.previous_hunk")!.run?.({} as never) await viewer.app.renderOnce() expect(scroll.scrollTop).toBe(first) viewer.commands.get("diff.next_hunk")!.run?.({} as never) await viewer.app.renderOnce() expect(scroll.scrollTop).toBe(second) scroll.scrollTo(initial) viewer.commands.get("diff.next_hunk")!.run?.({} as never) await viewer.app.renderOnce() expect(scroll.scrollTop).toBe(first) } finally { viewer.app.renderer.destroy() } }) async function renderDiffViewer(vcsDiff: unknown[], height = 20) { const commands = new Map< string, NonNullable[0]["commands"]>[number] >() let current = startRoute let renderDiff: TuiRouteDefinition["render"] | undefined let vcsDiffInput: unknown const config = createTuiResolvedConfig() await mkdir(Global.Path.state, { recursive: true }) await Bun.write(path.join(Global.Path.state, "kv.json"), "{}") function Harness() { const renderer = useRenderer() const keymap = createDefaultOpenTuiKeymap(renderer) const registerLayer = keymap.registerLayer.bind(keymap) keymap.registerLayer = (layer) => { layer.commands?.forEach((command) => commands.set(command.name, command)) return registerLayer(layer) } const base = createTuiPluginApi({ keymap, client: { vcs: { diff: async (input: unknown) => { vcsDiffInput = input return { data: vcsDiff } }, }, session: { diff: async () => ({ data: [] }) }, } as unknown as TuiPluginApi["client"], state: { session: { get: () => session, }, }, }) const api = { ...base, route: { register(routes) { renderDiff = routes.find((route) => route.name === "diff")?.render return () => {} }, navigate(name, params) { current = params ? { name, params } : { name } }, get current() { return current }, }, } satisfies TuiPluginApi void diffViewerPlugin.tui(api, undefined, pluginMeta) commands.get("diff.open")?.run?.({} as never) return ( {renderDiff?.({ params: "params" in current ? current.params : undefined })} ) } const app = await testRender(() => , { width: 80, height }) await waitForCommand(app, commands, "diff.close") return { app, commands, current: () => current, vcsDiffInput: () => vcsDiffInput, } } const startRoute: TuiRouteCurrent = { name: "session", params: { sessionID: "session-1" } } function findRenderable(root: Renderable, id: string): Renderable | undefined { if (root.id === id) return root return root .getChildren() .map((child) => findRenderable(child, id)) .find(Boolean) } const session = { id: "session-1", slug: "session-1", projectID: "project-1", directory: "/repo/session", title: "Session", version: "1", time: { created: 0, updated: 0, }, } satisfies Session async function waitForCommand( app: Awaited>, commands: Map, command: string, ) { for (let attempt = 0; attempt < 10; attempt++) { await app.renderOnce() if (commands.has(command)) return await new Promise((resolve) => setTimeout(resolve, 25)) } } const pluginMeta = { id: "diff-viewer", source: "internal", spec: "diff-viewer", target: "diff-viewer", first_time: 0, last_time: 0, time_changed: 0, load_count: 1, fingerprint: "test", state: "same", } satisfies TuiPluginMeta