|
@@ -65,6 +65,9 @@ import parsers from "../../../../../../parsers-config.ts"
|
|
|
import { Clipboard } from "../../util/clipboard"
|
|
import { Clipboard } from "../../util/clipboard"
|
|
|
import { Toast, useToast } from "../../ui/toast"
|
|
import { Toast, useToast } from "../../ui/toast"
|
|
|
import { useKV } from "../../context/kv.tsx"
|
|
import { useKV } from "../../context/kv.tsx"
|
|
|
|
|
+import { Editor } from "../../util/editor"
|
|
|
|
|
+import { Global } from "@/global"
|
|
|
|
|
+import fs from "fs/promises"
|
|
|
|
|
|
|
|
addDefaultParsers(parsers.parsers)
|
|
addDefaultParsers(parsers.parsers)
|
|
|
|
|
|
|
@@ -446,6 +449,105 @@ export function Session() {
|
|
|
dialog.clear()
|
|
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",
|
|
title: "Next child session",
|
|
|
value: "session.child.next",
|
|
value: "session.child.next",
|