diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index a4c1f33d..172ae8a0 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -243,6 +243,16 @@ export function Autocomplete(props: { description: "rename session", onSelect: () => command.trigger("session.rename"), }, + { + display: "/copy", + description: "copy session transcript to clipboard", + onSelect: () => command.trigger("session.copy"), + }, + { + display: "/export", + description: "export session transcript to file", + onSelect: () => command.trigger("session.export"), + }, { display: "/timeline", description: "jump to message", 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 ccd12819..9fa844b7 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -65,6 +65,9 @@ import parsers from "../../../../../../parsers-config.ts" import { Clipboard } from "../../util/clipboard" import { Toast, useToast } from "../../ui/toast" import { useKV } from "../../context/kv.tsx" +import { Editor } from "../../util/editor" +import { Global } from "@/global" +import fs from "fs/promises" addDefaultParsers(parsers.parsers) @@ -446,6 +449,105 @@ export function Session() { dialog.clear() }, }, + { + title: "Copy session transcript", + value: "session.copy", + keybind: "session_copy", + category: "Session", + onSelect: async (dialog) => { + try { + // Format session transcript as markdown + const sessionData = session() + const sessionMessages = messages() + + let transcript = `# ${sessionData.title}\n\n` + transcript += `**Session ID:** ${sessionData.id}\n` + transcript += `**Created:** ${new Date(sessionData.time.created).toLocaleString()}\n` + transcript += `**Updated:** ${new Date(sessionData.time.updated).toLocaleString()}\n\n` + transcript += `---\n\n` + + for (const msg of sessionMessages) { + const parts = sync.data.part[msg.id] ?? [] + const role = msg.role === "user" ? "User" : "Assistant" + transcript += `## ${role}\n\n` + + for (const part of parts) { + if (part.type === "text" && !part.synthetic) { + transcript += `${part.text}\n\n` + } else if (part.type === "tool") { + transcript += `\`\`\`\nTool: ${part.tool}\n\`\`\`\n\n` + } + } + + transcript += `---\n\n` + } + + // Copy to clipboard + await Clipboard.copy(transcript) + toast.show({ message: "Session transcript copied to clipboard!", variant: "success" }) + } catch (error) { + toast.show({ message: "Failed to copy session transcript", variant: "error" }) + } + dialog.clear() + }, + }, + { + title: "Export session transcript to file", + value: "session.export", + keybind: "session_export", + category: "Session", + onSelect: async (dialog) => { + try { + // Format session transcript as markdown + const sessionData = session() + const sessionMessages = messages() + + let transcript = `# ${sessionData.title}\n\n` + transcript += `**Session ID:** ${sessionData.id}\n` + transcript += `**Created:** ${new Date(sessionData.time.created).toLocaleString()}\n` + transcript += `**Updated:** ${new Date(sessionData.time.updated).toLocaleString()}\n\n` + transcript += `---\n\n` + + for (const msg of sessionMessages) { + const parts = sync.data.part[msg.id] ?? [] + const role = msg.role === "user" ? "User" : "Assistant" + transcript += `## ${role}\n\n` + + for (const part of parts) { + if (part.type === "text" && !part.synthetic) { + transcript += `${part.text}\n\n` + } else if (part.type === "tool") { + transcript += `\`\`\`\nTool: ${part.tool}\n\`\`\`\n\n` + } + } + + transcript += `---\n\n` + } + + // Save to file in data directory + const exportDir = path.join(Global.Path.data, "exports") + await fs.mkdir(exportDir, { recursive: true }) + + const timestamp = new Date().toISOString().replace(/[:.]/g, "-") + const filename = `session-${sessionData.id.slice(0, 8)}-${timestamp}.md` + const filepath = path.join(exportDir, filename) + + await Bun.write(filepath, transcript) + + // Open with EDITOR if available + const result = await Editor.open({ value: transcript, renderer }) + if (result !== undefined) { + // User edited the file, save the changes + await Bun.write(filepath, result) + } + + toast.show({ message: `Session exported to ${filename}`, variant: "success" }) + } catch (error) { + toast.show({ message: "Failed to export session", variant: "error" }) + } + dialog.clear() + }, + }, { title: "Next child session", value: "session.child.next",