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 177c43a463a..d85a3307a65 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"
@@ -68,6 +67,7 @@ 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 +784,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 +826,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 +848,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 00000000000..7543a7785e7
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx
@@ -0,0 +1,152 @@
+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()
+ })
+
+ const toggleOption = (key: "thinking" | "toolDetails") => {
+ setStore(key, !store[key])
+ }
+
+ return (
+
+
+
+ Export Options
+
+ esc
+
+
+
+ Filename:
+
+
+
+ setStore("active", "thinking")}
+ >
+
+ {store.thinking ? "[x]" : "[ ]"}
+
+ Include thinking
+
+ setStore("active", "toolDetails")}
+ >
+
+ {store.toolDetails ? "[x]" : "[ ]"}
+
+ Include tool details
+
+
+
+
+ Press space to toggle, return{" "}
+ to confirm
+
+
+
+
+ Press return to confirm, tab{" "}
+ for options
+
+
+
+ )
+}
+
+DialogExportOptions.show = (
+ dialog: DialogContext,
+ defaultFilename: string,
+ defaultThinking: boolean,
+ defaultToolDetails: boolean,
+) => {
+ return new Promise<{ filename: string; thinking: boolean; toolDetails: boolean } | null>((resolve) => {
+ dialog.replace(
+ () => (
+ resolve(options)}
+ onCancel={() => resolve(null)}
+ />
+ ),
+ () => resolve(null),
+ )
+ })
+}