diff --git a/MIGRATION_V2.md b/MIGRATION_V2.md index 79c2cad7..024f95a9 100644 --- a/MIGRATION_V2.md +++ b/MIGRATION_V2.md @@ -24,6 +24,7 @@ The migration removes the V1 compatibility layer rather than maintaining both in - Remove V1 plugin communication channels and per-workspace runtime management. - Replace the custom background-process implementation with native V2 Shell and PTY APIs. - Replace per-workspace OpenCode binary selection with one global `opencode2` binary. +- Migrate the persisted V1 default command `opencode` to `opencode2` during workspace launch. - Remove message and part deletion controls because V2 currently has no equivalent API. - Keep Git mutation operations on the CodeNomad server where V2 does not yet provide sufficient parity. @@ -59,6 +60,7 @@ The migration removes the V1 compatibility layer rather than maintaining both in - The pinned client and installed `opencode2` CLI are aligned on `0.0.0-next-17353`. - The final critical/high security gate has no unresolved proxy, authentication, event-isolation, or process-ownership finding. - A real `opencode2@0.0.0-next-17353` lifecycle smoke test passed: authenticated discovery, workspace location validation, and confirmed service shutdown. +- Startup restore now renders its loading state immediately instead of leaving a blank renderer while saved workspaces launch. - The migration remains a Draft until the GitHub build matrix completes. ## Remaining Work diff --git a/packages/server/src/settings/binaries.test.ts b/packages/server/src/settings/binaries.test.ts index 9ae8234f..f8260d5c 100644 --- a/packages/server/src/settings/binaries.test.ts +++ b/packages/server/src/settings/binaries.test.ts @@ -25,4 +25,15 @@ describe("BinaryResolver", () => { } as unknown as SettingsService assert.equal(new BinaryResolver(settings).resolveDefault().path, "opencode2") }) + + it("upgrades the legacy bare opencode default to opencode2", () => { + const settings = { + getOwner(scope: string, owner: string) { + if (scope === "config" && owner === "server") return { opencodeBinary: "opencode" } + return {} + }, + } as unknown as SettingsService + + assert.equal(new BinaryResolver(settings).resolveDefault().path, "opencode2") + }) }) diff --git a/packages/server/src/settings/binaries.ts b/packages/server/src/settings/binaries.ts index 5c85b300..ce76b6ae 100644 --- a/packages/server/src/settings/binaries.ts +++ b/packages/server/src/settings/binaries.ts @@ -42,7 +42,7 @@ export class BinaryResolver { resolveDefault(): ResolvedBinary { const binaries = this.list() const configuredDefault = readDefaultBinaryPath(this.settings) - const path = configuredDefault ?? "opencode2" + const path = !configuredDefault || configuredDefault === "opencode" ? "opencode2" : configuredDefault const entry = binaries.find((b) => b.path === path) return { diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 6557e6fb..24fff08a 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -20,7 +20,7 @@ import { useCommands } from "./lib/hooks/use-commands" import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle" import { useAppSessionRestore } from "./lib/hooks/use-app-session-restore" import { loadedRestorableSession } from "./stores/client-state" -import { shouldShowAppHomeOverlay, shouldShowEmptyAppHome } from "./stores/app-session-restore-gate" +import { shouldShowAppHomeOverlay, shouldShowAppRestoreLoading } from "./stores/app-session-restore-gate" import { getLogger } from "./lib/logger" import { launchError, showLaunchError, clearLaunchError } from "./stores/launch-errors" import { formatLaunchErrorMessage, isMissingBinaryMessage } from "./lib/launch-errors" @@ -755,14 +755,12 @@ const App: Component = () => { } > - - - + diff --git a/packages/ui/src/stores/app-session-restore-gate.test.ts b/packages/ui/src/stores/app-session-restore-gate.test.ts index 66279b33..b4989693 100644 --- a/packages/ui/src/stores/app-session-restore-gate.test.ts +++ b/packages/ui/src/stores/app-session-restore-gate.test.ts @@ -1,15 +1,15 @@ import assert from "node:assert/strict" import { it } from "node:test" -import { shouldShowAppHomeOverlay, shouldShowEmptyAppHome } from "./app-session-restore-gate.ts" +import { shouldShowAppHomeOverlay, shouldShowAppRestoreLoading } from "./app-session-restore-gate.ts" const tab = { kind: "sidecar" as const, sidecarId: "preview" } -it("hides the empty-app home while saved tabs are restoring", () => { - assert.equal(shouldShowEmptyAppHome({ tabs: [tab], activeTabIndex: 0 }, true), false) - assert.equal(shouldShowEmptyAppHome({ tabs: [tab], activeTabIndex: 0, homeActive: true }, true), true) - assert.equal(shouldShowEmptyAppHome({ tabs: [], activeTabIndex: -1 }, true), true) - assert.equal(shouldShowEmptyAppHome({ tabs: [tab], activeTabIndex: 0 }, false), true) +it("shows loading while saved tabs are restoring", () => { + assert.equal(shouldShowAppRestoreLoading({ tabs: [tab], activeTabIndex: 0 }, true), true) + assert.equal(shouldShowAppRestoreLoading({ tabs: [tab], activeTabIndex: 0, homeActive: true }, true), false) + assert.equal(shouldShowAppRestoreLoading({ tabs: [], activeTabIndex: -1 }, true), false) + assert.equal(shouldShowAppRestoreLoading({ tabs: [tab], activeTabIndex: 0 }, false), false) }) it("mounts the requested home overlay only when tabs exist", () => { diff --git a/packages/ui/src/stores/app-session-restore-gate.ts b/packages/ui/src/stores/app-session-restore-gate.ts index 8b0d16e4..994146ce 100644 --- a/packages/ui/src/stores/app-session-restore-gate.ts +++ b/packages/ui/src/stores/app-session-restore-gate.ts @@ -7,12 +7,12 @@ function releaseAppSessionRestoreGate(): void { setAppSessionRestoreGateActive(false) } -function shouldShowEmptyAppHome(snapshot: RestorableSessionState | null, restoreActive = appSessionRestoreGateActive()): boolean { - return !restoreActive || !snapshot?.tabs.length || snapshot.homeActive === true +function shouldShowAppRestoreLoading(snapshot: RestorableSessionState | null, restoreActive = appSessionRestoreGateActive()): boolean { + return restoreActive && Boolean(snapshot?.tabs.length) && snapshot?.homeActive !== true } function shouldShowAppHomeOverlay(requested: boolean, tabCount: number): boolean { return requested && tabCount > 0 } -export { appSessionRestoreGateActive, releaseAppSessionRestoreGate, shouldShowAppHomeOverlay, shouldShowEmptyAppHome } +export { appSessionRestoreGateActive, releaseAppSessionRestoreGate, shouldShowAppHomeOverlay, shouldShowAppRestoreLoading }