diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 979d35e198e..1b9fad87302 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -42,6 +42,7 @@ import { DialogModel } from "./component/dialog-model" import { useConnected } from "./component/use-connected" import { DialogMcp } from "./component/dialog-mcp" import { DialogStatus } from "./component/dialog-status" +import { DialogDebug } from "./component/dialog-debug" import { DialogThemeList } from "./component/dialog-theme-list" import { DialogHelp } from "./ui/dialog-help" import { DialogAgent } from "./component/dialog-agent" @@ -115,6 +116,7 @@ const appBindingCommands = [ "provider.connect", "console.org.switch", "opencode.status", + "opencode.debug", "theme.switch", "theme.switch_mode", "theme.mode.lock", @@ -764,6 +766,15 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi }, category: "System", }, + { + name: "opencode.debug", + title: "View debug info", + slashName: "debug", + run: () => { + dialog.replace(() => ) + }, + category: "System", + }, { name: "theme.switch", title: "Switch theme", diff --git a/packages/tui/src/component/dialog-debug.tsx b/packages/tui/src/component/dialog-debug.tsx new file mode 100644 index 00000000000..21554aac957 --- /dev/null +++ b/packages/tui/src/component/dialog-debug.tsx @@ -0,0 +1,90 @@ +import { TextAttributes } from "@opentui/core" +import { createMemo, createSignal, For } from "solid-js" +import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" +import { useTheme } from "../context/theme" +import { useDialog } from "../ui/dialog" +import { useRoute } from "../context/route" +import { useLocal } from "../context/local" +import { useClipboard } from "../context/clipboard" +import { useToast } from "../ui/toast" +import { useBindings } from "../keymap" +import { describeOS, describeTerminal } from "../util/system" + +export function DialogDebug() { + const { theme } = useTheme() + const dialog = useDialog() + const route = useRoute() + const local = useLocal() + const clipboard = useClipboard() + const toast = useToast() + const [copied, setCopied] = createSignal(false) + + dialog.setSize("large") + + const entries = createMemo(() => { + const model = local.model.current() + return [ + { label: "Version", value: `${InstallationVersion} (${InstallationChannel})` }, + { label: "Date", value: new Date().toISOString() }, + { label: "OS", value: describeOS() }, + { label: "Terminal", value: describeTerminal() }, + { label: "Session ID", value: route.data.type === "session" ? route.data.sessionID : "n/a" }, + { label: "Model", value: model ? `${model.providerID}/${model.modelID}` : "n/a" }, + ] + }) + + const copy = () => { + const text = entries() + .map((entry) => `${entry.label}: ${entry.value}`) + .join("\n") + void clipboard + .write?.(text) + .then(() => { + setCopied(true) + toast.show({ message: "Debug info copied to clipboard", variant: "info" }) + }) + .catch(toast.error) + } + + useBindings(() => ({ + bindings: [{ key: "return", desc: "Copy debug info", group: "Dialog", cmd: copy }], + })) + + return ( + + + + Debug + + dialog.clear()}> + esc + + + {/* No click-to-copy here: releasing a mouse selection must trigger the + global copy-on-select so users can copy a single value, e.g. the session id. */} + + + {(entry) => ( + + + {entry.label.padEnd(10)} + + + {entry.value} + + + )} + + + + Share this when reporting an issue. + + + {copied() ? "✓ copied" : "copy"}{" "} + + enter + + + + ) +} diff --git a/packages/tui/src/component/error-component.tsx b/packages/tui/src/component/error-component.tsx index 40f0a830a12..1141da83822 100644 --- a/packages/tui/src/component/error-component.tsx +++ b/packages/tui/src/component/error-component.tsx @@ -1,4 +1,3 @@ -import { release } from "node:os" import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core" import { useKeyboard, useTerminalDimensions } from "@opentui/solid" import { createSignal, For, Show } from "solid-js" @@ -6,6 +5,7 @@ import { getScrollAcceleration } from "../util/scroll" import { useClipboard } from "../context/clipboard" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { useExit } from "../context/exit" +import { describeOS, describeTerminal } from "../util/system" export function ErrorComponent(props: { error: Error; reset: () => void; mode?: "dark" | "light" }) { const term = useTerminalDimensions() @@ -238,22 +238,3 @@ function buildIssueURL(message: string, stack: string) { setBody(stack.slice(0, lo) + marker) return url } - -function describeOS() { - const name = - process.platform === "darwin" - ? "macOS" - : process.platform === "win32" - ? "Windows" - : process.platform === "linux" - ? "Linux" - : process.platform - return `${name} ${release()} (${process.arch})` -} - -function describeTerminal() { - const program = process.env.TERM_PROGRAM || process.env.TERM || "unknown" - const version = process.env.TERM_PROGRAM_VERSION ? ` ${process.env.TERM_PROGRAM_VERSION}` : "" - const multiplexer = process.env.TMUX ? " in tmux" : process.env.STY ? " in screen" : "" - return `${program}${version}${multiplexer}` -} diff --git a/packages/tui/src/config/keybind.ts b/packages/tui/src/config/keybind.ts index c58189552d8..5dd7e4b5aaf 100644 --- a/packages/tui/src/config/keybind.ts +++ b/packages/tui/src/config/keybind.ts @@ -81,6 +81,7 @@ export const Definitions = { sidebar_toggle: keybind("b", "Toggle sidebar"), scrollbar_toggle: keybind("none", "Toggle session scrollbar"), status_view: keybind("s", "View status"), + debug_view: keybind("none", "View debug info"), session_export: keybind("x", "Export session to editor"), session_copy: keybind("none", "Copy session transcript"), @@ -288,6 +289,7 @@ export const CommandMap = { sidebar_toggle: "session.sidebar.toggle", scrollbar_toggle: "session.toggle.scrollbar", status_view: "opencode.status", + debug_view: "opencode.debug", session_export: "session.export", session_copy: "session.copy", session_move: "session.move", diff --git a/packages/tui/src/ui/dialog.tsx b/packages/tui/src/ui/dialog.tsx index b6cd705b1e8..50281630bc5 100644 --- a/packages/tui/src/ui/dialog.tsx +++ b/packages/tui/src/ui/dialog.tsx @@ -49,6 +49,9 @@ export function Dialog( > { + // A selection release must bubble up to the copy-on-select handler in + // DialogProvider; the backdrop's dismiss flag keeps it from closing the dialog. + if (renderer.getSelection()?.getSelectedText()) return dismiss = false e.stopPropagation() }} diff --git a/packages/tui/src/util/system.ts b/packages/tui/src/util/system.ts new file mode 100644 index 00000000000..f55848bfc12 --- /dev/null +++ b/packages/tui/src/util/system.ts @@ -0,0 +1,20 @@ +import { release } from "node:os" + +export function describeOS() { + const name = + process.platform === "darwin" + ? "macOS" + : process.platform === "win32" + ? "Windows" + : process.platform === "linux" + ? "Linux" + : process.platform + return `${name} ${release()} (${process.arch})` +} + +export function describeTerminal() { + const program = process.env.TERM_PROGRAM || process.env.TERM || "unknown" + const version = process.env.TERM_PROGRAM_VERSION ? ` ${process.env.TERM_PROGRAM_VERSION}` : "" + const multiplexer = process.env.TMUX ? " in tmux" : process.env.STY ? " in screen" : "" + return `${program}${version}${multiplexer}` +}