feat(desktop): add layout transition switch (#36667)

Co-authored-by: Brendan Allan <git@brendonovich.dev>
This commit is contained in:
usrnk1 2026-07-14 16:50:20 +02:00 committed by GitHub
commit 265a93927d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 550 additions and 100 deletions

View file

@ -19,7 +19,12 @@ import { forwardInitializationFailure } from "./initialization"
import { exportDebugLogs, initCrashReporter, initLogging, startNetLog, write as writeLog } from "./logging"
import { parseMarkdown } from "./markdown"
import { createMenu } from "./menu"
import { finishFirstLaunchOnboarding, isFirstLaunchOnboardingPending } from "./onboarding"
import {
finishFirstLaunchOnboarding,
initializeOldLayoutEligibility,
isFirstLaunchOnboardingPending,
isOldLayoutEligible,
} from "./onboarding"
import {
getDefaultServerUrl,
preferAppEnv,
@ -137,6 +142,7 @@ const main = Effect.gen(function* () {
onboardingTestRoot ? join(onboardingTestRoot, "desktop") : join(app.getPath("appData"), appId),
)
if (onboardingTestRoot) app.setPath("sessionData", join(onboardingTestRoot, "session"))
initializeOldLayoutEligibility(app.getPath("userData"))
logger = initLogging()
initCrashReporter()
@ -280,6 +286,7 @@ const main = Effect.gen(function* () {
setDefaultServerUrl: (url) => setDefaultServerUrl(url),
isFirstLaunchOnboardingPending,
finishFirstLaunchOnboarding,
isOldLayoutEligible,
getDisplayBackend: async () => null,
setDisplayBackend: async () => undefined,
parseMarkdown: async (markdown) => parseMarkdown(markdown),

View file

@ -0,0 +1,19 @@
import { describe, expect, test } from "bun:test"
import { hasExistingAppState } from "./install-state"
const file = (name: string) => ({ name, isDirectory: () => false })
const directory = (name: string) => ({ name, isDirectory: () => true })
describe("hasExistingAppState", () => {
test("ignores files Electron may create on a fresh install", () => {
expect(hasExistingAppState([])).toBe(false)
expect(hasExistingAppState([file("Local State"), directory("Crashpad")])).toBe(false)
})
test("recognizes state written by an earlier OpenCode launch", () => {
expect(hasExistingAppState([file("opencode.settings")])).toBe(true)
expect(hasExistingAppState([file("opencode.global.dat")])).toBe(true)
expect(hasExistingAppState([file("window-state-abc.json")])).toBe(true)
expect(hasExistingAppState([directory("opencode")])).toBe(true)
})
})

View file

@ -0,0 +1,8 @@
export function hasExistingAppState(entries: Array<{ name: string; isDirectory: () => boolean }>) {
return entries.some((entry) => {
if (entry.name === "opencode.settings") return true
if (entry.name.endsWith(".dat")) return true
if (/^window-state-.+\.json$/.test(entry.name)) return true
return entry.isDirectory() && entry.name === "opencode"
})
}

View file

@ -29,6 +29,7 @@ type Deps = {
setDefaultServerUrl: (url: string | null) => Promise<void> | void
isFirstLaunchOnboardingPending: () => Promise<boolean> | boolean
finishFirstLaunchOnboarding: (createDefaultProject: boolean) => Promise<string | null> | string | null
isOldLayoutEligible: () => Promise<boolean> | boolean
getDisplayBackend: () => Promise<string | null>
setDisplayBackend: (backend: string | null) => Promise<void> | void
parseMarkdown: (markdown: string) => Promise<string> | string
@ -56,6 +57,7 @@ export function registerIpcHandlers(deps: Deps) {
ipcMain.handle("finish-first-launch-onboarding", (_event: IpcMainInvokeEvent, createDefaultProject: boolean) =>
deps.finishFirstLaunchOnboarding(createDefaultProject),
)
ipcMain.handle("is-old-layout-eligible", () => deps.isOldLayoutEligible())
ipcMain.handle("get-display-backend", () => deps.getDisplayBackend())
ipcMain.handle("set-display-backend", (_event: IpcMainInvokeEvent, backend: string | null) =>
deps.setDisplayBackend(backend),

View file

@ -1,12 +1,29 @@
import { existsSync, readdirSync } from "node:fs"
import { mkdir } from "node:fs/promises"
import { join } from "node:path"
import { app } from "electron"
import { getStore } from "./store"
import { FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY } from "./store-keys"
import { FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY, OLD_LAYOUT_ELIGIBLE_KEY } from "./store-keys"
import { write as writeLog } from "./logging"
import { hasExistingAppState } from "./install-state"
const DEFAULT_PROJECT_DIR = "New OpenCode Project"
export function initializeOldLayoutEligibility(userDataPath: string) {
const entries = existsSync(userDataPath) ? readdirSync(userDataPath, { withFileTypes: true }) : []
const store = getStore()
const current = store.get(OLD_LAYOUT_ELIGIBLE_KEY)
if (typeof current === "boolean") return current
const eligible = hasExistingAppState(entries)
store.set(OLD_LAYOUT_ELIGIBLE_KEY, eligible)
return eligible
}
export function isOldLayoutEligible() {
return getStore().get(OLD_LAYOUT_ELIGIBLE_KEY) === true
}
export function isFirstLaunchOnboardingPending() {
const pending = getStore().get(FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY) !== true
writeLog("onboarding", "first launch onboarding pending checked", { pending })

View file

@ -1,6 +1,7 @@
export const SETTINGS_STORE = "opencode.settings"
export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl"
export const FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY = "firstLaunchOnboardingComplete"
export const OLD_LAYOUT_ELIGIBLE_KEY = "oldLayoutEligible"
export const WSL_SERVERS_KEY = "wslServers"
export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled"
export const WINDOW_IDS_KEY = "windowIds"

View file

@ -62,6 +62,7 @@ const api: ElectronAPI = {
isFirstLaunchOnboardingPending: () => ipcRenderer.invoke("is-first-launch-onboarding-pending"),
finishFirstLaunchOnboarding: (createDefaultProject) =>
ipcRenderer.invoke("finish-first-launch-onboarding", createDefaultProject),
isOldLayoutEligible: () => ipcRenderer.invoke("is-old-layout-eligible"),
getDisplayBackend: () => ipcRenderer.invoke("get-display-backend"),
setDisplayBackend: (backend) => ipcRenderer.invoke("set-display-backend", backend),
parseMarkdownCommand: (markdown) => ipcRenderer.invoke("parse-markdown", markdown),

View file

@ -52,6 +52,7 @@ export type ElectronAPI = {
setDefaultServerUrl: (url: string | null) => Promise<void>
isFirstLaunchOnboardingPending: () => Promise<boolean>
finishFirstLaunchOnboarding: (createDefaultProject: boolean) => Promise<string | null>
isOldLayoutEligible: () => Promise<boolean>
getDisplayBackend: () => Promise<LinuxDisplayBackend | null>
setDisplayBackend: (backend: LinuxDisplayBackend | null) => Promise<void>
parseMarkdownCommand: (markdown: string) => Promise<string>

View file

@ -5,6 +5,7 @@ import {
useServer,
useServerSDK,
useServerSync,
useSettings,
useTabs,
} from "@opencode-ai/app"
import { onMount, startTransition } from "solid-js"
@ -13,6 +14,7 @@ export function DesktopFirstLaunchOnboarding(props: { initialUrl: string; onLoad
const server = useServer()
const serverSDK = useServerSDK()
const serverSync = useServerSync()
const settings = useSettings()
const layout = useLayout()
const providers = useProviders()
const tabs = useTabs()
@ -28,6 +30,7 @@ export function DesktopFirstLaunchOnboarding(props: { initialUrl: string; onLoad
(p) => p ?? Promise.resolve(),
),
)
settings.general.setOldLayoutEligible(await window.api.isOldLayoutEligible())
if (!server.isLocal()) return
const pending = await window.api.isFirstLaunchOnboardingPending()