feat(desktop): Fork Session (#7673)
This commit is contained in:
99
packages/app/src/components/dialog-fork.tsx
Normal file
99
packages/app/src/components/dialog-fork.tsx
Normal file
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user