summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorrektide <[email protected]>2025-12-28 01:32:33 +0000
committerGitHub <[email protected]>2025-12-27 19:32:33 -0600
commit7ea0d37ee3b01be8788a95db5b6f08690d01465c (patch)
tree2b00a188f6822bce0bc899c3cf730c264db5c9a3
parente35d97f9d7005a4227eb56cc008cffb230161eda (diff)
downloadopencode-7ea0d37ee3b01be8788a95db5b6f08690d01465c.tar.gz
opencode-7ea0d37ee3b01be8788a95db5b6f08690d01465c.zip
Thinking & tool call visibility settings for `/copy` and `/export` (#6243)
Co-authored-by: Aiden Cline <[email protected]>
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/index.tsx51
-rw-r--r--packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx148
2 files changed, 187 insertions, 12 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index 177c43a46..d52985187 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -52,7 +52,6 @@ import { DialogMessage } from "./dialog-message"
import type { PromptInfo } from "../../component/prompt/history"
import { iife } from "@/util/iife"
import { DialogConfirm } from "@tui/ui/dialog-confirm"
-import { DialogPrompt } from "@tui/ui/dialog-prompt"
import { DialogTimeline } from "./dialog-timeline"
import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
import { DialogSessionRename } from "../../component/dialog-session-rename"
@@ -67,7 +66,7 @@ import stripAnsi from "strip-ansi"
import { Footer } from "./footer.tsx"
import { usePromptRef } from "../../context/prompt"
import { Filesystem } from "@/util/filesystem"
-import { DialogSubagent } from "./dialog-subagent.tsx"
+import { DialogExportOptions } from "../../ui/dialog-export-options"
addDefaultParsers(parsers.parsers)
@@ -784,8 +783,22 @@ export function Session() {
for (const part of parts) {
if (part.type === "text" && !part.synthetic) {
transcript += `${part.text}\n\n`
+ } else if (part.type === "reasoning") {
+ if (showThinking()) {
+ transcript += `_Thinking:_\n\n${part.text}\n\n`
+ }
} else if (part.type === "tool") {
- transcript += `\`\`\`\nTool: ${part.tool}\n\`\`\`\n\n`
+ transcript += `\`\`\`\nTool: ${part.tool}\n`
+ if (showDetails() && part.state.input) {
+ transcript += `\n**Input:**\n\`\`\`json\n${JSON.stringify(part.state.input, null, 2)}\n\`\`\``
+ }
+ if (showDetails() && part.state.status === "completed" && part.state.output) {
+ transcript += `\n**Output:**\n\`\`\`\n${part.state.output}\n\`\`\``
+ }
+ if (showDetails() && part.state.status === "error" && part.state.error) {
+ transcript += `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\``
+ }
+ transcript += `\n\`\`\`\n\n`
}
}
@@ -812,6 +825,14 @@ export function Session() {
const sessionData = session()
const sessionMessages = messages()
+ const defaultFilename = `session-${sessionData.id.slice(0, 8)}.md`
+
+ const options = await DialogExportOptions.show(dialog, defaultFilename, showThinking(), showDetails())
+
+ if (options === null) return
+
+ const { filename: customFilename, thinking: includeThinking, toolDetails: includeToolDetails } = options
+
let transcript = `# ${sessionData.title}\n\n`
transcript += `**Session ID:** ${sessionData.id}\n`
transcript += `**Created:** ${new Date(sessionData.time.created).toLocaleString()}\n`
@@ -826,22 +847,28 @@ export function Session() {
for (const part of parts) {
if (part.type === "text" && !part.synthetic) {
transcript += `${part.text}\n\n`
+ } else if (part.type === "reasoning") {
+ if (includeThinking) {
+ transcript += `_Thinking:_\n\n${part.text}\n\n`
+ }
} else if (part.type === "tool") {
- transcript += `\`\`\`\nTool: ${part.tool}\n\`\`\`\n\n`
+ transcript += `\`\`\`\nTool: ${part.tool}\n`
+ if (includeToolDetails && part.state.input) {
+ transcript += `\n**Input:**\n\`\`\`json\n${JSON.stringify(part.state.input, null, 2)}\n\`\`\``
+ }
+ if (includeToolDetails && part.state.status === "completed" && part.state.output) {
+ transcript += `\n**Output:**\n\`\`\`\n${part.state.output}\n\`\`\``
+ }
+ if (includeToolDetails && part.state.status === "error" && part.state.error) {
+ transcript += `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\``
+ }
+ transcript += `\n\`\`\`\n\n`
}
}
transcript += `---\n\n`
}
- // Prompt for optional filename
- const customFilename = await DialogPrompt.show(dialog, "Export filename", {
- value: `session-${sessionData.id.slice(0, 8)}.md`,
- })
-
- // Cancel if user pressed escape
- if (customFilename === null) return
-
// Save to file in current working directory
const exportDir = process.cwd()
const filename = customFilename.trim()
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx
new file mode 100644
index 000000000..874a236ee
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx
@@ -0,0 +1,148 @@
+import { TextareaRenderable, TextAttributes } from "@opentui/core"
+import { useTheme } from "../context/theme"
+import { useDialog, type DialogContext } from "./dialog"
+import { createStore } from "solid-js/store"
+import { onMount, Show, type JSX } from "solid-js"
+import { useKeyboard } from "@opentui/solid"
+
+export type DialogExportOptionsProps = {
+ defaultFilename: string
+ defaultThinking: boolean
+ defaultToolDetails: boolean
+ onConfirm?: (options: { filename: string; thinking: boolean; toolDetails: boolean }) => void
+ onCancel?: () => void
+}
+
+export function DialogExportOptions(props: DialogExportOptionsProps) {
+ const dialog = useDialog()
+ const { theme } = useTheme()
+ let textarea: TextareaRenderable
+ const [store, setStore] = createStore({
+ thinking: props.defaultThinking,
+ toolDetails: props.defaultToolDetails,
+ active: "filename" as "filename" | "thinking" | "toolDetails",
+ })
+
+ useKeyboard((evt) => {
+ if (evt.name === "return") {
+ props.onConfirm?.({
+ filename: textarea.plainText,
+ thinking: store.thinking,
+ toolDetails: store.toolDetails,
+ })
+ }
+ if (evt.name === "tab") {
+ const order: Array<"filename" | "thinking" | "toolDetails"> = ["filename", "thinking", "toolDetails"]
+ const currentIndex = order.indexOf(store.active)
+ const nextIndex = (currentIndex + 1) % order.length
+ setStore("active", order[nextIndex])
+ evt.preventDefault()
+ }
+ if (evt.name === "space") {
+ if (store.active === "thinking") setStore("thinking", !store.thinking)
+ if (store.active === "toolDetails") setStore("toolDetails", !store.toolDetails)
+ evt.preventDefault()
+ }
+ })
+
+ onMount(() => {
+ dialog.setSize("medium")
+ setTimeout(() => {
+ textarea.focus()
+ }, 1)
+ textarea.gotoLineEnd()
+ })
+
+ return (
+ <box paddingLeft={2} paddingRight={2} gap={1}>
+ <box flexDirection="row" justifyContent="space-between">
+ <text attributes={TextAttributes.BOLD} fg={theme.text}>
+ Export Options
+ </text>
+ <text fg={theme.textMuted}>esc</text>
+ </box>
+ <box gap={1}>
+ <box>
+ <text fg={theme.text}>Filename:</text>
+ </box>
+ <textarea
+ onSubmit={() => {
+ props.onConfirm?.({
+ filename: textarea.plainText,
+ thinking: store.thinking,
+ toolDetails: store.toolDetails,
+ })
+ }}
+ height={3}
+ keyBindings={[{ name: "return", action: "submit" }]}
+ ref={(val: TextareaRenderable) => (textarea = val)}
+ initialValue={props.defaultFilename}
+ placeholder="Enter filename"
+ textColor={theme.text}
+ focusedTextColor={theme.text}
+ cursorColor={theme.text}
+ />
+ </box>
+ <box flexDirection="column">
+ <box
+ flexDirection="row"
+ gap={2}
+ paddingLeft={1}
+ backgroundColor={store.active === "thinking" ? theme.backgroundElement : undefined}
+ onMouseUp={() => setStore("active", "thinking")}
+ >
+ <text fg={store.active === "thinking" ? theme.primary : theme.textMuted}>
+ {store.thinking ? "[x]" : "[ ]"}
+ </text>
+ <text fg={store.active === "thinking" ? theme.primary : theme.text}>Include thinking</text>
+ </box>
+ <box
+ flexDirection="row"
+ gap={2}
+ paddingLeft={1}
+ backgroundColor={store.active === "toolDetails" ? theme.backgroundElement : undefined}
+ onMouseUp={() => setStore("active", "toolDetails")}
+ >
+ <text fg={store.active === "toolDetails" ? theme.primary : theme.textMuted}>
+ {store.toolDetails ? "[x]" : "[ ]"}
+ </text>
+ <text fg={store.active === "toolDetails" ? theme.primary : theme.text}>Include tool details</text>
+ </box>
+ </box>
+ <Show when={store.active !== "filename"}>
+ <text fg={theme.textMuted} paddingBottom={1}>
+ Press <span style={{ fg: theme.text }}>space</span> to toggle, <span style={{ fg: theme.text }}>return</span>{" "}
+ to confirm
+ </text>
+ </Show>
+ <Show when={store.active === "filename"}>
+ <text fg={theme.textMuted} paddingBottom={1}>
+ Press <span style={{ fg: theme.text }}>return</span> to confirm, <span style={{ fg: theme.text }}>tab</span>{" "}
+ for options
+ </text>
+ </Show>
+ </box>
+ )
+}
+
+DialogExportOptions.show = (
+ dialog: DialogContext,
+ defaultFilename: string,
+ defaultThinking: boolean,
+ defaultToolDetails: boolean,
+) => {
+ return new Promise<{ filename: string; thinking: boolean; toolDetails: boolean } | null>((resolve) => {
+ dialog.replace(
+ () => (
+ <DialogExportOptions
+ defaultFilename={defaultFilename}
+ defaultThinking={defaultThinking}
+ defaultToolDetails={defaultToolDetails}
+ onConfirm={(options) => resolve(options)}
+ onCancel={() => resolve(null)}
+ />
+ ),
+ () => resolve(null),
+ )
+ })
+}