Quellcode durchsuchen

fix(app): optimistic revert/restore

Adam vor 6 Monaten
Ursprung
Commit
f0542fae7a

+ 80 - 24
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

+ 8 - 1
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>

+ 10 - 3
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")}

+ 1 - 1
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",