fix(app): optimistic revert/restore
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
|
import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import {
|
import {
|
||||||
|
batch,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
Show,
|
Show,
|
||||||
Match,
|
Match,
|
||||||
@@ -291,6 +292,7 @@ export default function Page() {
|
|||||||
git: false,
|
git: false,
|
||||||
pendingMessage: undefined as string | undefined,
|
pendingMessage: undefined as string | undefined,
|
||||||
restoring: undefined as string | undefined,
|
restoring: undefined as string | undefined,
|
||||||
|
reverting: false,
|
||||||
reviewSnap: false,
|
reviewSnap: false,
|
||||||
scrollGesture: 0,
|
scrollGesture: 0,
|
||||||
scroll: {
|
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) => {
|
const busy = (sessionID: string) => {
|
||||||
if (sync.data.session_status[sessionID]?.type !== "idle") return true
|
if (sync.data.session_status[sessionID]?.type !== "idle") return true
|
||||||
return (sync.data.message[sessionID] ?? []).some(
|
return (sync.data.message[sessionID] ?? []).some(
|
||||||
@@ -1275,42 +1295,77 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const revert = (input: { sessionID: string; messageID: string }) => {
|
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)
|
const value = draft(input.messageID)
|
||||||
|
batch(() => {
|
||||||
|
setUi("reverting", true)
|
||||||
|
roll(input.sessionID, { messageID: input.messageID })
|
||||||
|
prompt.set(value)
|
||||||
|
})
|
||||||
return halt(input.sessionID)
|
return halt(input.sessionID)
|
||||||
.then(() => sdk.client.session.revert(input))
|
.then(() => sdk.client.session.revert(input))
|
||||||
.then(() => {
|
.then((result) => {
|
||||||
prompt.set(value)
|
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 restore = (id: string) => {
|
||||||
const sessionID = params.id
|
const sessionID = params.id
|
||||||
if (!sessionID || ui.restoring) return
|
if (!sessionID || ui.restoring || ui.reverting) return
|
||||||
|
|
||||||
const next = userMessages().find((item) => item.id > id)
|
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
|
const task = !next
|
||||||
? halt(sessionID)
|
? halt(sessionID).then(() => sdk.client.session.unrevert({ sessionID }))
|
||||||
.then(() => sdk.client.session.unrevert({ sessionID }))
|
: halt(sessionID).then(() =>
|
||||||
.then(() => {
|
sdk.client.session.revert({
|
||||||
prompt.reset()
|
sessionID,
|
||||||
})
|
messageID: next.id,
|
||||||
: halt(sessionID)
|
}),
|
||||||
.then(() =>
|
)
|
||||||
sdk.client.session.revert({
|
|
||||||
sessionID,
|
|
||||||
messageID: next.id,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
prompt.set(draft(next.id))
|
|
||||||
})
|
|
||||||
|
|
||||||
return task.catch(fail).finally(() => {
|
return task
|
||||||
setUi("restoring", (value) => (value === id ? undefined : value))
|
.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(() => {
|
const rolled = createMemo(() => {
|
||||||
@@ -1487,6 +1542,7 @@ export default function Page() {
|
|||||||
? {
|
? {
|
||||||
items: rolled(),
|
items: rolled(),
|
||||||
restoring: ui.restoring,
|
restoring: ui.restoring,
|
||||||
|
disabled: ui.reverting,
|
||||||
onRestore: restore,
|
onRestore: restore,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export function SessionComposerRegion(props: {
|
|||||||
revert?: {
|
revert?: {
|
||||||
items: { id: string; text: string }[]
|
items: { id: string; text: string }[]
|
||||||
restoring?: string
|
restoring?: string
|
||||||
|
disabled?: boolean
|
||||||
onRestore: (id: string) => void
|
onRestore: (id: string) => void
|
||||||
}
|
}
|
||||||
setPromptDockRef: (el: HTMLDivElement) => void
|
setPromptDockRef: (el: HTMLDivElement) => void
|
||||||
@@ -156,6 +157,7 @@ export function SessionComposerRegion(props: {
|
|||||||
<SessionRevertDock
|
<SessionRevertDock
|
||||||
items={revert.items}
|
items={revert.items}
|
||||||
restoring={revert.restoring}
|
restoring={revert.restoring}
|
||||||
|
disabled={revert.disabled}
|
||||||
onRestore={revert.onRestore}
|
onRestore={revert.onRestore}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,7 +197,12 @@ export function SessionComposerRegion(props: {
|
|||||||
"margin-top": `${-36 * value()}px`,
|
"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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -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 { createStore } from "solid-js/store"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { DockTray } from "@opencode-ai/ui/dock-surface"
|
import { DockTray } from "@opencode-ai/ui/dock-surface"
|
||||||
@@ -8,11 +8,18 @@ import { useLanguage } from "@/context/language"
|
|||||||
export function SessionRevertDock(props: {
|
export function SessionRevertDock(props: {
|
||||||
items: { id: string; text: string }[]
|
items: { id: string; text: string }[]
|
||||||
restoring?: string
|
restoring?: string
|
||||||
|
disabled?: boolean
|
||||||
onRestore: (id: string) => void
|
onRestore: (id: string) => void
|
||||||
}) {
|
}) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const [store, setStore] = createStore({
|
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)
|
const toggle = () => setStore("collapsed", (value) => !value)
|
||||||
@@ -77,7 +84,7 @@ export function SessionRevertDock(props: {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
disabled={!!props.restoring}
|
disabled={props.disabled || !!props.restoring}
|
||||||
onClick={() => props.onRestore(item.id)}
|
onClick={() => props.onRestore(item.id)}
|
||||||
>
|
>
|
||||||
{language.t("session.revertDock.restore")}
|
{language.t("session.revertDock.restore")}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export const dict: Record<string, string> = {
|
|||||||
"ui.message.copy": "Copy",
|
"ui.message.copy": "Copy",
|
||||||
"ui.message.copyMessage": "Copy message",
|
"ui.message.copyMessage": "Copy message",
|
||||||
"ui.message.forkMessage": "Fork to new session",
|
"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.copyResponse": "Copy response",
|
||||||
"ui.message.copied": "Copied",
|
"ui.message.copied": "Copied",
|
||||||
"ui.message.interrupted": "Interrupted",
|
"ui.message.interrupted": "Interrupted",
|
||||||
|
|||||||
Reference in New Issue
Block a user