diff options
| author | rektide <[email protected]> | 2025-12-28 01:32:33 +0000 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-12-27 19:32:33 -0600 |
| commit | 7ea0d37ee3b01be8788a95db5b6f08690d01465c (patch) | |
| tree | 2b00a188f6822bce0bc899c3cf730c264db5c9a3 /packages | |
| parent | e35d97f9d7005a4227eb56cc008cffb230161eda (diff) | |
| download | opencode-7ea0d37ee3b01be8788a95db5b6f08690d01465c.tar.gz opencode-7ea0d37ee3b01be8788a95db5b6f08690d01465c.zip | |
Thinking & tool call visibility settings for `/copy` and `/export` (#6243)
Co-authored-by: Aiden Cline <[email protected]>
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/opencode/src/cli/cmd/tui/routes/session/index.tsx | 51 | ||||
| -rw-r--r-- | packages/opencode/src/cli/cmd/tui/ui/dialog-export-options.tsx | 148 |
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), + ) + }) +} |
