feat(app): restore to message and fork session (#17092)

This commit is contained in:
Adam 2026-03-11 16:34:48 -05:00 committed by GitHub
parent 58f45ae22b
commit fbd9b7cf4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 328 additions and 6 deletions

View file

@ -43,6 +43,7 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel"
import { TerminalPanel } from "@/pages/session/terminal-panel"
import { useSessionCommands } from "@/pages/session/use-session-commands"
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
import { extractPromptFromParts } from "@/utils/prompt"
import { same } from "@/utils/same"
import { formatServerError } from "@/utils/server-errors"
@ -286,6 +287,7 @@ export default function Page() {
const [ui, setUi] = createStore({
git: false,
pendingMessage: undefined as string | undefined,
restoring: undefined as string | undefined,
reviewSnap: false,
scrollGesture: 0,
scroll: {
@ -1179,6 +1181,110 @@ export default function Page() {
scroller: () => scroller,
})
const draft = (id: string) =>
extractPromptFromParts(sync.data.part[id] ?? [], {
directory: sdk.directory,
attachmentName: language.t("common.attachment"),
})
const line = (id: string) => {
const text = draft(id)
.map((part) => (part.type === "image" ? `[image:${part.filename}]` : part.content))
.join("")
.replace(/\s+/g, " ")
.trim()
if (text) return text
return `[${language.t("common.attachment")}]`
}
const fail = (err: unknown) => {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: formatServerError(err, language.t),
})
}
const busy = (sessionID: string) => {
if (sync.data.session_status[sessionID]?.type !== "idle") return true
return (sync.data.message[sessionID] ?? []).some(
(item) => item.role === "assistant" && typeof item.time.completed !== "number",
)
}
const halt = (sessionID: string) =>
busy(sessionID) ? sdk.client.session.abort({ sessionID }).catch(() => {}) : Promise.resolve()
const fork = (input: { sessionID: string; messageID: string }) => {
const value = draft(input.messageID)
return sdk.client.session
.fork(input)
.then((result) => {
const next = result.data
if (!next) {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
})
return
}
navigate(`/${base64Encode(sdk.directory)}/session/${next.id}`)
requestAnimationFrame(() => {
prompt.set(value)
})
})
.catch(fail)
}
const revert = (input: { sessionID: string; messageID: string }) => {
const value = draft(input.messageID)
return halt(input.sessionID)
.then(() => sdk.client.session.revert(input))
.then(() => {
prompt.set(value)
})
.catch(fail)
}
const restore = (id: string) => {
const sessionID = params.id
if (!sessionID || ui.restoring) return
const next = userMessages().find((item) => item.id > id)
setUi("restoring", id)
const task = !next
? halt(sessionID)
.then(() => sdk.client.session.unrevert({ sessionID }))
.then(() => {
prompt.reset()
})
: halt(sessionID)
.then(() =>
sdk.client.session.revert({
sessionID,
messageID: next.id,
}),
)
.then(() => {
prompt.set(draft(next.id))
})
return task.catch(fail).finally(() => {
setUi("restoring", (value) => (value === id ? undefined : value))
})
}
const rolled = createMemo(() => {
const id = revertMessageID()
if (!id) return []
return userMessages()
.filter((item) => item.id >= id)
.map((item) => ({ id: item.id, text: line(item.id) }))
})
const actions = { fork, revert }
createResizeObserver(
() => promptDock,
({ height }) => {
@ -1268,6 +1374,7 @@ export default function Page() {
loadingClass: "px-4 py-4 text-text-weak",
emptyClass: "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6",
})}
actions={actions}
scroll={ui.scroll}
onResumeScroll={resumeScroll}
setScrollRef={setScrollRef}
@ -1333,6 +1440,15 @@ export default function Page() {
resumeScroll()
}}
onResponseSubmit={resumeScroll}
revert={
rolled().length > 0
? {
items: rolled(),
restoring: ui.restoring,
onRestore: restore,
}
: undefined
}
setPromptDockRef={(el) => {
promptDock = el
}}