feat: interactive update flow for non-patch releases (#18662)

This commit is contained in:
Dax
2026-03-22 19:12:40 -04:00
committed by GitHub
parent 32f9dc6383
commit e2d03ce38c
9 changed files with 317 additions and 132 deletions
+45 -5
View File
@@ -5,8 +5,8 @@ import { MouseButton, TextAttributes } from "@opentui/core"
import { RouteProvider, useRoute } from "@tui/context/route"
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js"
import { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "./win32"
import { Installation } from "@/installation"
import { Flag } from "@/flag/flag"
import semver from "semver"
import { DialogProvider, useDialog } from "@tui/ui/dialog"
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
import { SDKProvider, useSDK } from "@tui/context/sdk"
@@ -29,6 +29,7 @@ import { PromptHistoryProvider } from "./component/prompt/history"
import { FrecencyProvider } from "./component/prompt/frecency"
import { PromptStashProvider } from "./component/prompt/stash"
import { DialogAlert } from "./ui/dialog-alert"
import { DialogConfirm } from "./ui/dialog-confirm"
import { ToastProvider, useToast } from "./ui/toast"
import { ExitProvider, useExit } from "./context/exit"
import { Session as SessionApi } from "@/session"
@@ -103,6 +104,7 @@ async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
}
import type { EventSource } from "./context/sdk"
import { Installation } from "@/installation"
export function tui(input: {
url: string
@@ -729,13 +731,51 @@ function App() {
})
})
sdk.event.on(Installation.Event.UpdateAvailable.type, (evt) => {
sdk.event.on("installation.update-available", async (evt) => {
const version = evt.properties.version
const skipped = kv.get("skipped_version")
if (skipped && !semver.gt(version, skipped)) return
const choice = await DialogConfirm.show(
dialog,
`Update Available`,
`A new release v${version} is available. Would you like to update now?`,
"skip",
)
if (choice === false) {
kv.set("skipped_version", version)
return
}
if (choice !== true) return
toast.show({
variant: "info",
title: "Update Available",
message: `OpenCode v${evt.properties.version} is available. Run 'opencode upgrade' to update manually.`,
duration: 10000,
message: `Updating to v${version}...`,
duration: 30000,
})
const result = await sdk.client.global.upgrade({ target: version })
if (result.error || !result.data?.success) {
toast.show({
variant: "error",
title: "Update Failed",
message: "Update failed",
duration: 10000,
})
return
}
await DialogAlert.show(
dialog,
"Update Complete",
`Successfully updated to OpenCode v${result.data.version}. Please restart the application.`,
)
exit()
})
return (
@@ -11,8 +11,11 @@ export type DialogConfirmProps = {
message: string
onConfirm?: () => void
onCancel?: () => void
label?: string
}
export type DialogConfirmResult = boolean | undefined
export function DialogConfirm(props: DialogConfirmProps) {
const dialog = useDialog()
const { theme } = useTheme()
@@ -45,7 +48,7 @@ export function DialogConfirm(props: DialogConfirmProps) {
<text fg={theme.textMuted}>{props.message}</text>
</box>
<box flexDirection="row" justifyContent="flex-end" paddingBottom={1}>
<For each={["cancel", "confirm"]}>
<For each={["cancel", "confirm"] as const}>
{(key) => (
<box
paddingLeft={1}
@@ -58,7 +61,7 @@ export function DialogConfirm(props: DialogConfirmProps) {
}}
>
<text fg={key === store.active ? theme.selectedListItemText : theme.textMuted}>
{Locale.titlecase(key)}
{Locale.titlecase(key === "cancel" ? (props.label ?? key) : key)}
</text>
</box>
)}
@@ -68,8 +71,8 @@ export function DialogConfirm(props: DialogConfirmProps) {
)
}
DialogConfirm.show = (dialog: DialogContext, title: string, message: string) => {
return new Promise<boolean>((resolve) => {
DialogConfirm.show = (dialog: DialogContext, title: string, message: string, label?: string) => {
return new Promise<DialogConfirmResult>((resolve) => {
dialog.replace(
() => (
<DialogConfirm
@@ -77,9 +80,10 @@ DialogConfirm.show = (dialog: DialogContext, title: string, message: string) =>
message={message}
onConfirm={() => resolve(true)}
onCancel={() => resolve(false)}
label={label}
/>
),
() => resolve(false),
() => resolve(undefined),
)
})
}