opencode/packages/app/src/components/dialog-fork.tsx

121 lines
4 KiB
TypeScript

import { Component, createMemo } from "solid-js"
import { useNavigate, useParams } from "@solidjs/router"
import { useData } from "@/context/server"
import { usePrompt } from "@/context/prompt"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog, DialogBody, DialogHeader, DialogTitle } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { showToast } from "@/utils/toast"
import { useLanguage } from "@/context/language"
import { useServerSDK } from "@/context/server-sdk"
import { base64Encode } from "@opencode-ai/util/encode"
import { extractPromptComments, extractPromptFromMessage } from "@/utils/prompt"
import { useWorkspaceLocation } from "@/context/location"
import { useServer } from "@/context/server"
import { sessionHref } from "@/utils/session-route"
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 data = useData()
const serverSDK = useServerSDK()
const location = useWorkspaceLocation()
const prompt = usePrompt()
const dialog = useDialog()
const language = useLanguage()
const server = useServer()
const messages = createMemo((): ForkableMessage[] => {
const sessionID = params.id
if (!sessionID) return []
const msgs = data.session.message.list(sessionID)
const result: ForkableMessage[] = []
for (const message of msgs) {
if (message.type !== "user" || !message.text) continue
result.push({
id: message.id,
text: message.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 message = data.session.message.get(sessionID, item.id)
if (message?.type !== "user") return
const restored = extractPromptFromMessage(message, {
directory: location().directory,
attachmentName: language.t("common.attachment"),
})
const dir = base64Encode(location().directory)
serverSDK.api.session
.fork({ sessionID, boundary: { type: "before", messageID: item.id } })
.then((forked) => {
data.session.remember(forked)
dialog.close()
const target = prompt.capture({ dir, id: forked.id })
target.set(restored)
target.context.replaceComments(
extractPromptComments(message).map((comment) => ({
type: "file",
path: comment.path,
selection: comment.selection,
comment: comment.comment,
preview: comment.preview,
commentOrigin: comment.origin,
})),
)
navigate(sessionHref(server.key, forked.id))
})
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
})
}
return (
<Dialog>
<DialogHeader>
<DialogTitle>{language.t("command.session.fork")}</DialogTitle>
</DialogHeader>
<DialogBody>
<List
class="flex-1 px-3 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.fork.empty")}
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 font-normal">{item.text}</span>
<span class="text-text-weak shrink-0 font-normal">{item.time}</span>
</div>
)}
</List>
</DialogBody>
</Dialog>
)
}