Skip to content

Commit 3adfb97

Browse files
authored
feat(tui): add debug info dialog with copy to clipboard (#35004)
1 parent 5ecd19d commit 3adfb97

6 files changed

Lines changed: 127 additions & 20 deletions

File tree

packages/tui/src/app.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { DialogModel } from "./component/dialog-model"
4242
import { useConnected } from "./component/use-connected"
4343
import { DialogMcp } from "./component/dialog-mcp"
4444
import { DialogStatus } from "./component/dialog-status"
45+
import { DialogDebug } from "./component/dialog-debug"
4546
import { DialogThemeList } from "./component/dialog-theme-list"
4647
import { DialogHelp } from "./ui/dialog-help"
4748
import { DialogAgent } from "./component/dialog-agent"
@@ -115,6 +116,7 @@ const appBindingCommands = [
115116
"provider.connect",
116117
"console.org.switch",
117118
"opencode.status",
119+
"opencode.debug",
118120
"theme.switch",
119121
"theme.switch_mode",
120122
"theme.mode.lock",
@@ -764,6 +766,15 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
764766
},
765767
category: "System",
766768
},
769+
{
770+
name: "opencode.debug",
771+
title: "View debug info",
772+
slashName: "debug",
773+
run: () => {
774+
dialog.replace(() => <DialogDebug />)
775+
},
776+
category: "System",
777+
},
767778
{
768779
name: "theme.switch",
769780
title: "Switch theme",
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { TextAttributes } from "@opentui/core"
2+
import { createMemo, createSignal, For } from "solid-js"
3+
import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version"
4+
import { useTheme } from "../context/theme"
5+
import { useDialog } from "../ui/dialog"
6+
import { useRoute } from "../context/route"
7+
import { useLocal } from "../context/local"
8+
import { useClipboard } from "../context/clipboard"
9+
import { useToast } from "../ui/toast"
10+
import { useBindings } from "../keymap"
11+
import { describeOS, describeTerminal } from "../util/system"
12+
13+
export function DialogDebug() {
14+
const { theme } = useTheme()
15+
const dialog = useDialog()
16+
const route = useRoute()
17+
const local = useLocal()
18+
const clipboard = useClipboard()
19+
const toast = useToast()
20+
const [copied, setCopied] = createSignal(false)
21+
22+
dialog.setSize("large")
23+
24+
const entries = createMemo(() => {
25+
const model = local.model.current()
26+
return [
27+
{ label: "Version", value: `${InstallationVersion} (${InstallationChannel})` },
28+
{ label: "Date", value: new Date().toISOString() },
29+
{ label: "OS", value: describeOS() },
30+
{ label: "Terminal", value: describeTerminal() },
31+
{ label: "Session ID", value: route.data.type === "session" ? route.data.sessionID : "n/a" },
32+
{ label: "Model", value: model ? `${model.providerID}/${model.modelID}` : "n/a" },
33+
]
34+
})
35+
36+
const copy = () => {
37+
const text = entries()
38+
.map((entry) => `${entry.label}: ${entry.value}`)
39+
.join("\n")
40+
void clipboard
41+
.write?.(text)
42+
.then(() => {
43+
setCopied(true)
44+
toast.show({ message: "Debug info copied to clipboard", variant: "info" })
45+
})
46+
.catch(toast.error)
47+
}
48+
49+
useBindings(() => ({
50+
bindings: [{ key: "return", desc: "Copy debug info", group: "Dialog", cmd: copy }],
51+
}))
52+
53+
return (
54+
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
55+
<box flexDirection="row" justifyContent="space-between">
56+
<text fg={theme.text} attributes={TextAttributes.BOLD}>
57+
Debug
58+
</text>
59+
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
60+
esc
61+
</text>
62+
</box>
63+
{/* No click-to-copy here: releasing a mouse selection must trigger the
64+
global copy-on-select so users can copy a single value, e.g. the session id. */}
65+
<box>
66+
<For each={entries()}>
67+
{(entry) => (
68+
<box flexDirection="row" gap={1}>
69+
<text flexShrink={0} fg={theme.textMuted}>
70+
{entry.label.padEnd(10)}
71+
</text>
72+
<text fg={theme.text} wrapMode="word">
73+
{entry.value}
74+
</text>
75+
</box>
76+
)}
77+
</For>
78+
</box>
79+
<box flexDirection="row" justifyContent="space-between">
80+
<text fg={theme.textMuted}>Share this when reporting an issue.</text>
81+
<text onMouseUp={copy}>
82+
<span style={{ fg: copied() ? theme.success : theme.text }}>
83+
<b>{copied() ? "✓ copied" : "copy"}</b>{" "}
84+
</span>
85+
<span style={{ fg: theme.textMuted }}>enter</span>
86+
</text>
87+
</box>
88+
</box>
89+
)
90+
}

packages/tui/src/component/error-component.tsx

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { release } from "node:os"
21
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
32
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
43
import { createSignal, For, Show } from "solid-js"
54
import { getScrollAcceleration } from "../util/scroll"
65
import { useClipboard } from "../context/clipboard"
76
import { InstallationVersion } from "@opencode-ai/core/installation/version"
87
import { useExit } from "../context/exit"
8+
import { describeOS, describeTerminal } from "../util/system"
99

1010
export function ErrorComponent(props: { error: Error; reset: () => void; mode?: "dark" | "light" }) {
1111
const term = useTerminalDimensions()
@@ -238,22 +238,3 @@ function buildIssueURL(message: string, stack: string) {
238238
setBody(stack.slice(0, lo) + marker)
239239
return url
240240
}
241-
242-
function describeOS() {
243-
const name =
244-
process.platform === "darwin"
245-
? "macOS"
246-
: process.platform === "win32"
247-
? "Windows"
248-
: process.platform === "linux"
249-
? "Linux"
250-
: process.platform
251-
return `${name} ${release()} (${process.arch})`
252-
}
253-
254-
function describeTerminal() {
255-
const program = process.env.TERM_PROGRAM || process.env.TERM || "unknown"
256-
const version = process.env.TERM_PROGRAM_VERSION ? ` ${process.env.TERM_PROGRAM_VERSION}` : ""
257-
const multiplexer = process.env.TMUX ? " in tmux" : process.env.STY ? " in screen" : ""
258-
return `${program}${version}${multiplexer}`
259-
}

packages/tui/src/config/keybind.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export const Definitions = {
8181
sidebar_toggle: keybind("<leader>b", "Toggle sidebar"),
8282
scrollbar_toggle: keybind("none", "Toggle session scrollbar"),
8383
status_view: keybind("<leader>s", "View status"),
84+
debug_view: keybind("none", "View debug info"),
8485

8586
session_export: keybind("<leader>x", "Export session to editor"),
8687
session_copy: keybind("none", "Copy session transcript"),
@@ -288,6 +289,7 @@ export const CommandMap = {
288289
sidebar_toggle: "session.sidebar.toggle",
289290
scrollbar_toggle: "session.toggle.scrollbar",
290291
status_view: "opencode.status",
292+
debug_view: "opencode.debug",
291293
session_export: "session.export",
292294
session_copy: "session.copy",
293295
session_move: "session.move",

packages/tui/src/ui/dialog.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export function Dialog(
4949
>
5050
<box
5151
onMouseUp={(e: { stopPropagation(): void }) => {
52+
// A selection release must bubble up to the copy-on-select handler in
53+
// DialogProvider; the backdrop's dismiss flag keeps it from closing the dialog.
54+
if (renderer.getSelection()?.getSelectedText()) return
5255
dismiss = false
5356
e.stopPropagation()
5457
}}

packages/tui/src/util/system.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { release } from "node:os"
2+
3+
export function describeOS() {
4+
const name =
5+
process.platform === "darwin"
6+
? "macOS"
7+
: process.platform === "win32"
8+
? "Windows"
9+
: process.platform === "linux"
10+
? "Linux"
11+
: process.platform
12+
return `${name} ${release()} (${process.arch})`
13+
}
14+
15+
export function describeTerminal() {
16+
const program = process.env.TERM_PROGRAM || process.env.TERM || "unknown"
17+
const version = process.env.TERM_PROGRAM_VERSION ? ` ${process.env.TERM_PROGRAM_VERSION}` : ""
18+
const multiplexer = process.env.TMUX ? " in tmux" : process.env.STY ? " in screen" : ""
19+
return `${program}${version}${multiplexer}`
20+
}

0 commit comments

Comments
 (0)