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>) => + 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>["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)) - }) + ? halt(sessionID).then(() => sdk.client.session.unrevert({ sessionID })) + : halt(sessionID).then(() => + sdk.client.session.revert({ + sessionID, + messageID: next.id, + }), + ) - return task.catch(fail).finally(() => { - setUi("restoring", (value) => (value === id ? undefined : value)) - }) + 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: { @@ -195,7 +197,12 @@ export function SessionComposerRegion(props: { "margin-top": `${-36 * value()}px`, }} > - + )} 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 = { "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",