|
|
@@ -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>
|
|
|
+ )
|
|
|
+}
|