summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-03-12 15:04:27 -0500
committerAdam <[email protected]>2026-03-12 15:04:27 -0500
commitf0542fae7a917fabb9e943c3112a3d0b4b03302d (patch)
tree58bf65d9becf087a10efe33b27a5497f5475dee5
parent02c75821a897657a3512ad157e9666a830b76253 (diff)
downloadopencode-f0542fae7a917fabb9e943c3112a3d0b4b03302d.tar.gz
opencode-f0542fae7a917fabb9e943c3112a3d0b4b03302d.zip
fix(app): optimistic revert/restore
-rw-r--r--packages/app/src/pages/session.tsx104
-rw-r--r--packages/app/src/pages/session/composer/session-composer-region.tsx9
-rw-r--r--packages/app/src/pages/session/composer/session-revert-dock.tsx13
-rw-r--r--packages/ui/src/i18n/en.ts2
4 files changed, 99 insertions, 29 deletions
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 7642ac165..b8d32fed4 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -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,42 +1295,77 @@ 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)
+ batch(() => {
+ setUi("reverting", true)
+ roll(input.sessionID, { messageID: input.messageID })
+ prompt.set(value)
+ })
return halt(input.sessionID)
.then(() => sdk.client.session.revert(input))
- .then(() => {
- prompt.set(value)
+ .then((result) => {
+ if (result.data) merge(result.data)
+ })
+ .catch((err) => {
+ batch(() => {
+ roll(input.sessionID, last)
+ prompt.set(prev)
+ })
+ fail(err)
+ })
+ .finally(() => {
+ setUi("reverting", false)
})
- .catch(fail)
}
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
+
+ batch(() => {
+ setUi("restoring", id)
+ setUi("reverting", true)
+ roll(sessionID, next ? { messageID: next.id } : undefined)
+ if (next) {
+ prompt.set(draft(next.id))
+ return
+ }
+ prompt.reset()
+ })
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))
- })
+ ? halt(sessionID).then(() => sdk.client.session.unrevert({ sessionID }))
+ : halt(sessionID).then(() =>
+ sdk.client.session.revert({
+ sessionID,
+ messageID: next.id,
+ }),
+ )
+
+ 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)
+ })
+ })
}
const rolled = createMemo(() => {
@@ -1487,6 +1542,7 @@ export default function Page() {
? {
items: rolled(),
restoring: ui.restoring,
+ disabled: ui.reverting,
onRestore: restore,
}
: undefined
diff --git a/packages/app/src/pages/session/composer/session-composer-region.tsx b/packages/app/src/pages/session/composer/session-composer-region.tsx
index 6d60d81b5..0cca90180 100644
--- a/packages/app/src/pages/session/composer/session-composer-region.tsx
+++ b/packages/app/src/pages/session/composer/session-composer-region.tsx
@@ -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>
diff --git a/packages/app/src/pages/session/composer/session-revert-dock.tsx b/packages/app/src/pages/session/composer/session-revert-dock.tsx
index ce7644b67..fd4b39ee7 100644
--- a/packages/app/src/pages/session/composer/session-revert-dock.tsx
+++ b/packages/app/src/pages/session/composer/session-revert-dock.tsx
@@ -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")}
diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts
index 3551f889d..2b41bc03d 100644
--- a/packages/ui/src/i18n/en.ts
+++ b/packages/ui/src/i18n/en.ts
@@ -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",