Browse Source

feat(desktop): Fork Session (#7673)

Daniel Polito 1 month ago
parent
commit
76386f5cfc
2 changed files with 109 additions and 0 deletions
  1. 99 0
      packages/app/src/components/dialog-fork.tsx
  2. 10 0
      packages/app/src/pages/session.tsx

+ 99 - 0
packages/app/src/components/dialog-fork.tsx

@@ -0,0 +1,99 @@
+import { Component, createMemo } from "solid-js"
+import { useNavigate, useParams } from "@solidjs/router"
+import { useSync } from "@/context/sync"
+import { useSDK } from "@/context/sdk"
+import { usePrompt } from "@/context/prompt"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { List } from "@opencode-ai/ui/list"
+import { extractPromptFromParts } from "@/utils/prompt"
+import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
+import { base64Encode } from "@opencode-ai/util/encode"
+
+interface ForkableMessage {
+  id: string
+  text: string
+  time: string
+}
+
+function formatTime(date: Date): string {
+  return date.toLocaleTimeString(undefined, { timeStyle: "short" })
+}
+
+export const DialogFork: Component = () => {
+  const params = useParams()
+  const navigate = useNavigate()
+  const sync = useSync()
+  const sdk = useSDK()
+  const prompt = usePrompt()
+  const dialog = useDialog()
+
+  const messages = createMemo((): ForkableMessage[] => {
+    const sessionID = params.id
+    if (!sessionID) return []
+
+    const msgs = sync.data.message[sessionID] ?? []
+    const result: ForkableMessage[] = []
+
+    for (const message of msgs) {
+      if (message.role !== "user") continue
+
+      const parts = sync.data.part[message.id] ?? []
+      const textPart = parts.find((x): x is SDKTextPart => x.type === "text" && !x.synthetic && !x.ignored)
+      if (!textPart) continue
+
+      result.push({
+        id: message.id,
+        text: textPart.text.replace(/\n/g, " ").slice(0, 200),
+        time: formatTime(new Date(message.time.created)),
+      })
+    }
+
+    return result.reverse()
+  })
+
+  const handleSelect = (item: ForkableMessage | undefined) => {
+    if (!item) return
+
+    const sessionID = params.id
+    if (!sessionID) return
+
+    const parts = sync.data.part[item.id] ?? []
+    const restored = extractPromptFromParts(parts, { directory: sdk.directory })
+
+    dialog.close()
+
+    sdk.client.session.fork({ sessionID, messageID: item.id }).then((forked) => {
+      if (!forked.data) return
+      navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`)
+      requestAnimationFrame(() => {
+        prompt.set(restored)
+      })
+    })
+  }
+
+  return (
+    <Dialog title="Fork from message">
+      <List
+        class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
+        search={{ placeholder: "Search", autofocus: true }}
+        emptyMessage="No messages to fork from"
+        key={(x) => x.id}
+        items={messages}
+        filterKeys={["text"]}
+        onSelect={handleSelect}
+      >
+        {(item) => (
+          <div class="w-full flex items-center gap-2">
+            <span class="truncate flex-1 min-w-0 text-left" style={{ "font-weight": "400" }}>
+              {item.text}
+            </span>
+            <span class="text-text-weak shrink-0" style={{ "font-weight": "400" }}>
+              {item.time}
+            </span>
+          </div>
+        )}
+      </List>
+    </Dialog>
+  )
+}

+ 10 - 0
packages/app/src/pages/session.tsx

@@ -31,6 +31,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { DialogSelectFile } from "@/components/dialog-select-file"
 import { DialogSelectFile } from "@/components/dialog-select-file"
 import { DialogSelectModel } from "@/components/dialog-select-model"
 import { DialogSelectModel } from "@/components/dialog-select-model"
 import { DialogSelectMcp } from "@/components/dialog-select-mcp"
 import { DialogSelectMcp } from "@/components/dialog-select-mcp"
+import { DialogFork } from "@/components/dialog-fork"
 import { useCommand } from "@/context/command"
 import { useCommand } from "@/context/command"
 import { useNavigate, useParams } from "@solidjs/router"
 import { useNavigate, useParams } from "@solidjs/router"
 import { UserMessage } from "@opencode-ai/sdk/v2"
 import { UserMessage } from "@opencode-ai/sdk/v2"
@@ -645,6 +646,15 @@ export default function Page() {
         })
         })
       },
       },
     },
     },
+    {
+      id: "session.fork",
+      title: "Fork from message",
+      description: "Create a new session from a previous message",
+      category: "Session",
+      slash: "fork",
+      disabled: !params.id || visibleUserMessages().length === 0,
+      onSelect: () => dialog.show(() => <DialogFork />),
+    },
   ])
   ])
 
 
   const handleKeyDown = (event: KeyboardEvent) => {
   const handleKeyDown = (event: KeyboardEvent) => {