feat(desktop): show migration progress (#42495)

This commit is contained in:
Luke Parker
2026-08-14 13:40:18 +10:00
committed by GitHub
Unverified
parent ae14e78ba6
commit 72d37cda81
3 changed files with 113 additions and 2 deletions
+1
View File
@@ -579,6 +579,7 @@ export const dict = {
"toast.session.listFailed.title": "Failed to load sessions for {{project}}",
"toast.project.reloadFailed.title": "Failed to reload {{project}}",
"toast.migration.failed.title": "Data migration failed",
"toast.update.title": "Update available",
"toast.update.description": "A new version of OpenCode ({{version}}) is now available to install.",
+6 -2
View File
@@ -29,6 +29,7 @@ import { DesktopFirstLaunchOnboarding } from "./onboarding"
import { resetZoom, setPinchZoomEnabled, webviewZoom, zoomIn, zoomOut } from "./webview-zoom"
import { windowFullscreen } from "./window-fullscreen"
import { availableStartupServer, readyWslConnections } from "./wsl/connections"
import { MigrationStatus } from "./migration-status"
import "./styles.css"
import { Splash } from "@opencode-ai/ui/logo"
import { useTheme } from "@opencode-ai/ui/theme/context"
@@ -357,7 +358,7 @@ function DesktopRoot(props: { windowState: DesktopWindowState }) {
)
const onboarding = Promise.withResolvers<void>()
function Inner() {
function DesktopEffects() {
const cmd = useCommand()
menuTrigger = (id) => cmd.trigger(id)
@@ -412,7 +413,10 @@ function DesktopRoot(props: { windowState: DesktopWindowState }) {
onLoaded={onboarding.resolve}
serverKey={key}
/>
<Inner />
<DesktopEffects />
<Show when={initializationData(sidecar)} keyed>
{(server) => <MigrationStatus server={server} />}
</Show>
</AppInterface>
)}
</Show>
@@ -0,0 +1,106 @@
import { OpenCode, type MigrationV1StatusOutput } from "@opencode-ai/client/promise"
import { useLanguage } from "@opencode-ai/app"
import { LoaderV2 } from "@opencode-ai/ui/v2/loader-v2"
import { showToastV2, toasterV2, ToastV2 } from "@opencode-ai/ui/v2/toast-v2"
import { createRoot, createSignal, onCleanup, onMount } from "solid-js"
import type { ServerReadyData } from "../preload/types"
type Progress = Extract<MigrationV1StatusOutput, { status: "running" }>["progress"]
export function MigrationStatus(props: { server: ServerReadyData }) {
const language = useLanguage()
const [progress, setProgress] = createSignal<Progress>()
const abort = new AbortController()
let toastID: number | undefined
let disposeToast: (() => void) | undefined
const hide = () => {
if (toastID !== undefined) toasterV2.dismiss(toastID)
toastID = undefined
disposeToast?.()
disposeToast = undefined
}
const show = () => {
if (toastID !== undefined) return
toastID = toasterV2.show(
({ toastId }) =>
createRoot((dispose) => {
disposeToast?.()
disposeToast = dispose
return (
<ToastV2 toastId={toastId}>
<div data-slot="toast-v2-header" class="col-span-full">
<ToastV2.Icon>
<LoaderV2 />
</ToastV2.Icon>
<ToastV2.Content>
<ToastV2.Title dir="auto">{format(progress())}</ToastV2.Title>
</ToastV2.Content>
</div>
</ToastV2>
)
}),
{ persistent: true },
)
}
onMount(async () => {
await wait(1_000, abort.signal)
if (abort.signal.aborted) return
const client = OpenCode.make({
baseUrl: props.server.url,
headers: props.server.password
? { Authorization: `Basic ${btoa(`${props.server.username ?? "opencode"}:${props.server.password}`)}` }
: undefined,
})
void (async () => {
while (true) {
const status = await client.migration.v1.status({ signal: abort.signal })
setProgress(status.status === "running" ? status.progress : undefined)
if (status.status === "running") show()
else hide()
if (status.status === "completed") return
if (status.status === "error") throw new Error(status.error)
await wait(1_000, abort.signal)
}
})().catch((error) => {
if (abort.signal.aborted) return
hide()
showToastV2({
variant: "error",
title: language.t("toast.migration.failed.title"),
description: error instanceof Error ? error.message : String(error),
duration: 10_000,
})
})
})
onCleanup(() => {
abort.abort()
hide()
})
return null
}
function format(progress: Progress | undefined) {
if (!progress) return ""
if (progress.numerator === undefined) return progress.label
if (progress.denominator === undefined) return `${progress.label} ${progress.numerator}`
return `${progress.label} ${progress.numerator}/${progress.denominator}`
}
function wait(delay: number, signal: AbortSignal) {
return new Promise<void>((resolve) => {
const timer = setTimeout(done, delay)
signal.addEventListener("abort", done, { once: true })
function done() {
clearTimeout(timer)
signal.removeEventListener("abort", done)
resolve()
}
})
}