diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/diff-viewer.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/diff-viewer.tsx index 0c0ed9fe15..924bbd7eb3 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/diff-viewer.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/diff-viewer.tsx @@ -1,5 +1,5 @@ /** @jsxImportSource @opentui/solid */ -import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginApi, TuiRouteCurrent } from "@opencode-ai/plugin/tui" import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" import { TextAttributes, type BorderSides, type BoxRenderable, type ScrollBoxRenderable } from "@opentui/core" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" @@ -80,7 +80,12 @@ function DiffViewer(props: { api: TuiPluginApi }) { const theme = () => props.api.theme.current const params = () => ("params" in props.api.route.current ? props.api.route.current.params : undefined) as - | { mode?: DiffMode; sessionID?: string; messageID?: string } + | { + mode?: DiffMode + sessionID?: string + messageID?: string + returnRoute?: TuiRouteCurrent + } | undefined const mode = () => params()?.mode ?? "git" const diffInput = createMemo(() => ({ @@ -369,8 +374,13 @@ function DiffViewer(props: { api: TuiPluginApi }) { title: "Close diff viewer", category: "VCS", run() { + const returnRoute = params()?.returnRoute props.api.ui.dialog.clear() - props.api.route.navigate("home") + + props.api.route.navigate( + returnRoute?.name ?? "home", + returnRoute && "params" in returnRoute ? returnRoute.params : undefined, + ) }, }, { @@ -619,6 +629,7 @@ function DiffViewer(props: { api: TuiPluginApi }) { mode: option.value, sessionID: params()?.sessionID, messageID: params()?.messageID, + returnRoute: params()?.returnRoute, }) }, }))} @@ -933,6 +944,7 @@ const tui: TuiPlugin = async (api) => { api.route.navigate(ROUTE, { mode: "git", sessionID: "params" in api.route.current ? api.route.current.params?.sessionID : undefined, + returnRoute: api.route.current, }) api.ui.dialog.clear() }, diff --git a/packages/opencode/test/cli/tui/diff-viewer.test.tsx b/packages/opencode/test/cli/tui/diff-viewer.test.tsx new file mode 100644 index 0000000000..ec6a3cf955 --- /dev/null +++ b/packages/opencode/test/cli/tui/diff-viewer.test.tsx @@ -0,0 +1,106 @@ +/** @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 { testRender, useRenderer } from "@opentui/solid" +import { Global } from "@opencode-ai/core/global" +import type { TuiPluginApi, TuiPluginMeta, TuiRouteCurrent, TuiRouteDefinition } from "@opencode-ai/plugin/tui" +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 { 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 startRoute: TuiRouteCurrent = { name: "session", params: { sessionID: "session-1" } } + const commands = new Map[0]["commands"]>[number]>() + let current = startRoute + let renderDiff: TuiRouteDefinition["render"] | undefined + 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 () => ({ data: [] }) }, + session: { diff: async () => ({ data: [] }) }, + } as unknown as TuiPluginApi["client"], + }) + 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: 20 }) + try { + await waitForCommand(app, commands, "diff.close") + expect(current).toEqual({ name: "diff", params: { mode: "git", sessionID: "session-1", returnRoute: startRoute } }) + + expect(commands.has("diff.close")).toBe(true) + commands.get("diff.close")!.run?.({} as never) + expect(current).toEqual(startRoute) + } finally { + app.renderer.destroy() + } +}) + +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