Преглед изворни кода

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

Signed-off-by: Christian Stewart <[email protected]>
Christian Stewart пре 3 месеци
родитељ
комит
b90c0b5fac

+ 10 - 0
packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx

@@ -243,6 +243,16 @@ export function Autocomplete(props: {
           description: "rename session",
           description: "rename session",
           onSelect: () => command.trigger("session.rename"),
           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",
           display: "/timeline",
           description: "jump to message",
           description: "jump to message",

+ 102 - 0
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 { 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",