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 }