summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDaniel Polito <[email protected]>2026-01-10 19:04:01 -0300
committerGitHub <[email protected]>2026-01-10 16:04:01 -0600
commit76386f5cfc310dd3abea76222ba80450dc5c6253 (patch)
tree49a77747d49037482d1580acfd5a68063e746284
parenta9275def432b4594698a80f1d882da11f72531d2 (diff)
downloadopencode-76386f5cfc310dd3abea76222ba80450dc5c6253.tar.gz
opencode-76386f5cfc310dd3abea76222ba80450dc5c6253.zip
feat(desktop): Fork Session (#7673)
-rw-r--r--packages/app/src/components/dialog-fork.tsx99
-rw-r--r--packages/app/src/pages/session.tsx10
2 files changed, 109 insertions, 0 deletions
diff --git a/packages/app/src/components/dialog-fork.tsx b/packages/app/src/components/dialog-fork.tsx
new file mode 100644
index 000000000..472a1994f
--- /dev/null
+++ b/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>
+ )
+}
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index ab6995d92..69065a8fa 100644
--- a/packages/app/src/pages/session.tsx
+++ b/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 { DialogSelectModel } from "@/components/dialog-select-model"
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
+import { DialogFork } from "@/components/dialog-fork"
import { useCommand } from "@/context/command"
import { useNavigate, useParams } from "@solidjs/router"
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) => {