diff options
| author | Daniel Polito <[email protected]> | 2026-01-10 19:04:01 -0300 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-01-10 16:04:01 -0600 |
| commit | 76386f5cfc310dd3abea76222ba80450dc5c6253 (patch) | |
| tree | 49a77747d49037482d1580acfd5a68063e746284 | |
| parent | a9275def432b4594698a80f1d882da11f72531d2 (diff) | |
| download | opencode-76386f5cfc310dd3abea76222ba80450dc5c6253.tar.gz opencode-76386f5cfc310dd3abea76222ba80450dc5c6253.zip | |
feat(desktop): Fork Session (#7673)
| -rw-r--r-- | packages/app/src/components/dialog-fork.tsx | 99 | ||||
| -rw-r--r-- | packages/app/src/pages/session.tsx | 10 |
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) => { |
