mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-08-21 06:13:26 +00:00
fix(restore): migrate legacy OpenCode startup state
Treat the persisted V1 default command 'opencode' as 'opencode2' so restored and newly selected workspaces use the required V2 service instead of waiting for the obsolete binary to time out. Keep the folder selection view mounted while startup restore is active and feed it the existing loading state, preventing the renderer from appearing blank while saved workspaces are recreated. Cover the legacy binary setting and restore loading gate with regression tests. Server/UI typechecks, targeted restore tests, the UI production build, and a real lifecycle smoke from an 'opencode' configuration pass.
This commit is contained in:
parent
c66aac7967
commit
e94d788bf6
6 changed files with 30 additions and 19 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
</>
|
||||
}
|
||||
>
|
||||
<Show when={shouldShowEmptyAppHome(loadedRestorableSession())}>
|
||||
<FolderSelectionView
|
||||
onSelectFolder={handleSelectFolder}
|
||||
onSelectExistingInstance={handleSelectExistingInstance}
|
||||
isLoading={isSelectingFolder()}
|
||||
onOpenSidecar={handleOpenSidecarPicker}
|
||||
/>
|
||||
</Show>
|
||||
<FolderSelectionView
|
||||
onSelectFolder={handleSelectFolder}
|
||||
onSelectExistingInstance={handleSelectExistingInstance}
|
||||
isLoading={isSelectingFolder() || shouldShowAppRestoreLoading(loadedRestorableSession())}
|
||||
onOpenSidecar={handleOpenSidecarPicker}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={shouldShowAppHomeOverlay(showFolderSelection(), appTabs().length)}>
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue