diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 5e42b189b..32c70b6d0 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -68,6 +68,7 @@ import { createTuiAttention } from "@/cli/cmd/tui/attention" import { FormatError, FormatUnknownError } from "@/cli/error" import { CommandPaletteProvider, useCommandPalette } from "./context/command-palette" import { OpencodeKeymapProvider, registerOpencodeKeymap, useBindings, useOpencodeKeymap } from "./keymap" +import { DiffViewer } from "./routes/diff" import type { EventSource } from "./context/sdk" import { DialogVariant } from "./component/dialog-variant" @@ -102,6 +103,7 @@ const appBindingCommands = [ "theme.switch", "theme.switch_mode", "theme.mode.lock", + "diff.open", "help.show", "docs.open", "app.debug", @@ -352,6 +354,7 @@ function App(props: { onSnapshot?: () => Promise }) { renderer.clearSelection() } const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true)) + const [diffOpen, setDiffOpen] = createSignal(false) const [pasteSummaryEnabled, setPasteSummaryEnabled] = createSignal( kv.get("paste_summary_enabled", !sync.data.config.experimental?.disable_paste_summary), ) @@ -663,6 +666,16 @@ function App(props: { onSnapshot?: () => Promise }) { }, category: "System", }, + { + name: "diff.open", + title: "Open diff viewer", + slashName: "diff", + run: () => { + setDiffOpen(true) + dialog.clear() + }, + category: "VCS", + }, { name: "help.show", title: "Help", @@ -962,6 +975,9 @@ function App(props: { onSnapshot?: () => Promise }) { + + setDiffOpen(false)} /> + diff --git a/packages/opencode/src/cli/cmd/tui/config/keybind.ts b/packages/opencode/src/cli/cmd/tui/config/keybind.ts index bd26cd5d9..2837ca851 100644 --- a/packages/opencode/src/cli/cmd/tui/config/keybind.ts +++ b/packages/opencode/src/cli/cmd/tui/config/keybind.ts @@ -67,6 +67,7 @@ export const Definitions = { sidebar_toggle: keybind("b", "Toggle sidebar"), scrollbar_toggle: keybind("none", "Toggle session scrollbar"), status_view: keybind("s", "View status"), + diff_open: keybind("d", "Open diff viewer"), session_export: keybind("x", "Export session to editor"), session_copy: keybind("none", "Copy session transcript"), @@ -251,6 +252,7 @@ export const CommandMap = { sidebar_toggle: "session.sidebar.toggle", scrollbar_toggle: "session.toggle.scrollbar", status_view: "opencode.status", + diff_open: "diff.open", session_export: "session.export", session_copy: "session.copy", session_new: "session.new", diff --git a/packages/opencode/src/cli/cmd/tui/simulation-mcp.ts b/packages/opencode/src/cli/cmd/tui/simulation-mcp.ts index 37183d239..ee2c7b6e7 100644 --- a/packages/opencode/src/cli/cmd/tui/simulation-mcp.ts +++ b/packages/opencode/src/cli/cmd/tui/simulation-mcp.ts @@ -152,6 +152,12 @@ const ScriptActionSchema = z.union([ const ScriptSchema = z.union([z.array(ScriptActionSchema), z.object({ actions: z.array(ScriptActionSchema) })]) const TargetSchema = z.union([z.string(), z.array(z.string()).min(1), z.literal("all")]) +const BackendFetchSchema = z.object({ + url: z.string(), + method: z.string().optional(), + headers: z.record(z.string(), z.string()).optional(), + body: z.unknown().optional(), +}) const remoteInstances = new Map() function currentBuffer(renderer: CliRenderer): RenderBuffer { @@ -264,6 +270,27 @@ async function control(options: Options, method: string, pathname: string, body? throw new Error(typeof data?.error === "string" ? data.error : `Simulation control request failed: ${response.status}`) } +async function backendFetch(options: Options, input: z.infer) { + const running = current(options) + const headers = new Headers(input.headers) + const body = + input.body === undefined ? undefined : typeof input.body === "string" ? input.body : JSON.stringify(input.body) + if (body !== undefined && !headers.has("content-type") && typeof input.body !== "string") headers.set("content-type", "application/json") + + const response = await (running.controlFetch ?? fetch)(new URL(input.url, running.controlUrl), { + method: input.method ?? (body === undefined ? "GET" : "POST"), + headers, + body, + }) + return { + url: response.url, + status: response.status, + ok: response.ok, + headers: Object.fromEntries(response.headers.entries()), + body: await response.text(), + } +} + async function mcpRequest(url: string, method: string, params: unknown, timeout = 500) { const response = await fetch(url, { method: "POST", @@ -431,6 +458,14 @@ function createServer(options: Options) { await current(options).harness.renderOnce() return toolResult(snapshot(options)) }) + server.registerTool( + "simulation_backend_fetch", + { + description: "Proxy one fetch request through the simulated backend and return the raw response.", + inputSchema: BackendFetchSchema, + }, + async (input) => toolResult(await backendFetch(options, input)), + ) server.registerTool( "simulation_action_execute", {