fix(app): optimistic revert/restore

This commit is contained in:
Adam 2026-03-12 15:04:27 -05:00
parent 02c75821a8
commit f0542fae7a
No known key found for this signature in database
GPG Key ID: 9CB48779AF150E75
4 changed files with 98 additions and 28 deletions

View File

@ -1,6 +1,7 @@
import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import {
batch,
onCleanup,
Show,
Match,
@ -291,6 +292,7 @@ export default function Page() {
git: false,
pendingMessage: undefined as string | undefined,
restoring: undefined as string | undefined,
reverting: false,
reviewSnap: false,
scrollGesture: 0,
scroll: {
@ -1243,6 +1245,24 @@ export default function Page() {
})
}
const merge = (next: NonNullable<ReturnType<typeof info>>) =>
sync.set("session", (list) => {
const idx = list.findIndex((item) => item.id === next.id)
if (idx < 0) return list
const out = list.slice()
out[idx] = next
return out
})
const roll = (sessionID: string, next: NonNullable<ReturnType<typeof info>>["revert"]) =>
sync.set("session", (list) => {
const idx = list.findIndex((item) => item.id === sessionID)
if (idx < 0) return list
const out = list.slice()
out[idx] = { ...out[idx], revert: next }
return out
})
const busy = (sessionID: string) => {
if (sync.data.session_status[sessionID]?.type !== "idle") return true
return (sync.data.message[sessionID] ?? []).some(
@ -1275,41 +1295,76 @@ export default function Page() {
}
const revert = (input: { sessionID: string; messageID: string }) => {
if (ui.reverting || ui.restoring) return
const prev = prompt.current().slice()
const last = info()?.revert
const value = draft(input.messageID)
return halt(input.sessionID)
.then(() => sdk.client.session.revert(input))
.then(() => {
batch(() => {
setUi("reverting", true)
roll(input.sessionID, { messageID: input.messageID })
prompt.set(value)
})
.catch(fail)
return halt(input.sessionID)
.then(() => sdk.client.session.revert(input))
.then((result) => {
if (result.data) merge(result.data)
})
.catch((err) => {
batch(() => {
roll(input.sessionID, last)
prompt.set(prev)
})
fail(err)
})
.finally(() => {
setUi("reverting", false)
})
}
const restore = (id: string) => {
const sessionID = params.id
if (!sessionID || ui.restoring) return
if (!sessionID || ui.restoring || ui.reverting) return
const next = userMessages().find((item) => item.id > id)
setUi("restoring", id)
const prev = prompt.current().slice()
const last = info()?.revert
const task = !next
? halt(sessionID)
.then(() => sdk.client.session.unrevert({ sessionID }))
.then(() => {
batch(() => {
setUi("restoring", id)
setUi("reverting", true)
roll(sessionID, next ? { messageID: next.id } : undefined)
if (next) {
prompt.set(draft(next.id))
return
}
prompt.reset()
})
: halt(sessionID)
.then(() =>
const task = !next
? halt(sessionID).then(() => sdk.client.session.unrevert({ sessionID }))
: halt(sessionID).then(() =>
sdk.client.session.revert({
sessionID,
messageID: next.id,
}),
)
.then(() => {
prompt.set(draft(next.id))
})
return task.catch(fail).finally(() => {
return task
.then((result) => {
if (result.data) merge(result.data)
})
.catch((err) => {
batch(() => {
roll(sessionID, last)
prompt.set(prev)
})
fail(err)
})
.finally(() => {
batch(() => {
setUi("restoring", (value) => (value === id ? undefined : value))
setUi("reverting", false)
})
})
}
@ -1487,6 +1542,7 @@ export default function Page() {
? {
items: rolled(),
restoring: ui.restoring,
disabled: ui.reverting,
onRestore: restore,
}
: undefined

View File

@ -24,6 +24,7 @@ export function SessionComposerRegion(props: {
revert?: {
items: { id: string; text: string }[]
restoring?: string
disabled?: boolean
onRestore: (id: string) => void
}
setPromptDockRef: (el: HTMLDivElement) => void
@ -156,6 +157,7 @@ export function SessionComposerRegion(props: {
<SessionRevertDock
items={revert.items}
restoring={revert.restoring}
disabled={revert.disabled}
onRestore={revert.onRestore}
/>
</div>
@ -195,7 +197,12 @@ export function SessionComposerRegion(props: {
"margin-top": `${-36 * value()}px`,
}}
>
<SessionRevertDock items={revert.items} restoring={revert.restoring} onRestore={revert.onRestore} />
<SessionRevertDock
items={revert.items}
restoring={revert.restoring}
disabled={revert.disabled}
onRestore={revert.onRestore}
/>
</div>
)}
</Show>

View File

@ -1,4 +1,4 @@
import { For, Show, createMemo } from "solid-js"
import { For, Show, createEffect, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button"
import { DockTray } from "@opencode-ai/ui/dock-surface"
@ -8,11 +8,18 @@ import { useLanguage } from "@/context/language"
export function SessionRevertDock(props: {
items: { id: string; text: string }[]
restoring?: string
disabled?: boolean
onRestore: (id: string) => void
}) {
const language = useLanguage()
const [store, setStore] = createStore({
collapsed: false,
collapsed: true,
})
createEffect(() => {
props.items.length
props.items[0]?.id
setStore("collapsed", true)
})
const toggle = () => setStore("collapsed", (value) => !value)
@ -77,7 +84,7 @@ export function SessionRevertDock(props: {
size="small"
variant="secondary"
class="shrink-0"
disabled={!!props.restoring}
disabled={props.disabled || !!props.restoring}
onClick={() => props.onRestore(item.id)}
>
{language.t("session.revertDock.restore")}

View File

@ -128,7 +128,7 @@ export const dict: Record<string, string> = {
"ui.message.copy": "Copy",
"ui.message.copyMessage": "Copy message",
"ui.message.forkMessage": "Fork to new session",
"ui.message.revertMessage": "Reset to this point",
"ui.message.revertMessage": "Revert message",
"ui.message.copyResponse": "Copy response",
"ui.message.copied": "Copied",
"ui.message.interrupted": "Interrupted",