|
|
@@ -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
|
|
|
}}
|