feat(tui): add /export and /copy commands (#3883)

Signed-off-by: Christian Stewart <christian@aperture.us>
This commit is contained in:
Christian Stewart
2025-11-04 21:02:45 -08:00
committed by GitHub
parent 3b1ab444fd
commit b90c0b5fac
2 changed files with 112 additions and 0 deletions

View File

@@ -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",

View File

@@ -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",