diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index d690f4dbb0..86263d172d 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -28,7 +28,6 @@ import { type ParentProps, Show, } from "solid-js" -import { createStore } from "solid-js/store" import { Dynamic } from "solid-js/web" import { CommandProvider, useCommand, type CommandOption } from "@/context/command" import { CommentsProvider } from "@/context/comments" @@ -45,7 +44,7 @@ import { PermissionProvider } from "@/context/permission" import { usePlatform } from "@/context/platform" import { PromptProvider } from "@/context/prompt" import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server" -import { hasMeaningfulLayoutData, SettingsProvider, useSettings } from "@/context/settings" +import { SettingsProvider, useSettings } from "@/context/settings" import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs" import { SDKProvider, useSDK } from "@/context/sdk" import { WslServersProvider } from "@/wsl/context" @@ -270,133 +269,6 @@ function BodyDesignClass() { return null } -function layoutClassificationRequest(promise: Promise, onTimeout: () => void, timeoutMs = 10_000) { - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - onTimeout() - reject(new Error("Layout classification timed out")) - }, timeoutMs) - promise.then( - (value) => { - clearTimeout(timeout) - resolve(value) - }, - (error) => { - clearTimeout(timeout) - reject(error) - }, - ) - }) -} - -function LayoutTransitionGate(props: ParentProps) { - const settings = useSettings() - const server = useServer() - const global = useGlobal() - const platform = usePlatform() - const [state, setState] = createStore({ started: false, retry: 0 }) - const retry = { current: undefined as ReturnType | undefined } - const wslState = { - current: undefined as ReturnType["getState"]> | undefined, - } - - const readWslState = () => { - if (!platform.wslServers) return Promise.resolve(undefined) - if (wslState.current) return wslState.current - const request = platform.wslServers.getState() - wslState.current = request - void request.catch(() => { - if (wslState.current === request) wslState.current = undefined - }) - return request - } - - const scheduleRetry = () => { - if (retry.current !== undefined) return - retry.current = setTimeout(() => { - retry.current = undefined - setState({ started: false, retry: state.retry + 1 }) - }, 10_000) - } - - onCleanup(() => { - if (retry.current !== undefined) clearTimeout(retry.current) - }) - - createEffect(() => { - void state.retry - if (state.started || settings.general.layoutTransitionClassified()) return - if (!settings.ready() || !server.ready()) return - - const input = { - settings: settings.general.layoutTransitionSettingsPresent(), - server: server.hasPersistedData(), - wsl: false, - projects: false, - sessions: false, - } - if (hasMeaningfulLayoutData(input)) { - setState("started", true) - settings.general.classifyLayoutTransition(true) - return - } - const conn = server.current - if (!conn) return - setState("started", true) - const client = global.ensureServerCtx(conn).sdk.client - const abort = new AbortController() - const pendingWsl = readWslState() - void pendingWsl.then( - (wsl) => { - if ((wsl?.servers.length ?? 0) > 0) settings.general.classifyLayoutTransition(true) - }, - () => undefined, - ) - const wsl = layoutClassificationRequest(pendingWsl, () => {}, 5_000).then( - (value) => ({ known: true as const, value }), - () => ({ known: false as const, value: undefined }), - ) - void layoutClassificationRequest( - Promise.all([ - client.project.list(undefined, { signal: abort.signal, throwOnError: true }), - client.session.list({ limit: 1 }, { signal: abort.signal, throwOnError: true }), - wsl, - ]), - () => abort.abort(), - ) - .then(([projects, sessions, wsl]) => { - const existing = hasMeaningfulLayoutData({ - ...input, - wsl: (wsl.value?.servers.length ?? 0) > 0, - projects: (projects.data?.length ?? 0) > 0, - sessions: (sessions.data?.length ?? 0) > 0, - }) - if (!existing && !wsl.known) { - scheduleRetry() - return - } - settings.general.classifyLayoutTransition(existing) - }) - .catch((error) => { - console.error("[layout-transition] failed to classify local data", error) - scheduleRetry() - }) - }) - - return ( - - - - } - > - {props.children} - - ) -} - // Server-agnostic providers shared across every route. These live in the shared // shell (router root) so they stay mounted regardless of the active server/route. function SharedProviders(props: ParentProps) { @@ -682,26 +554,24 @@ export function AppInterface(props: { - - - ( - - - - - {routerProps.children} - - - - - )} - > - - - - + + ( + + + + + {routerProps.children} + + + + + )} + > + + + diff --git a/packages/app/src/context/server.test.ts b/packages/app/src/context/server.test.ts index 87e2c0edf1..d78880ecf1 100644 --- a/packages/app/src/context/server.test.ts +++ b/packages/app/src/context/server.test.ts @@ -3,7 +3,6 @@ import { createRoot, createSignal } from "solid-js" import { createStore } from "solid-js/store" import { createServerProjects, - hasPersistedServerData, migrateCanonicalLocalServerState, nextServerAfterRemoval, resolveServerList, @@ -62,23 +61,6 @@ describe("resolveServerList", () => { }) }) -describe("hasPersistedServerData", () => { - const empty = () => ({ list: [], projects: {}, lastProject: {}, recentlyClosed: {} }) - - test("ignores a blank persisted server store", () => { - expect(hasPersistedServerData(empty())).toBe(false) - }) - - test("recognizes configured servers and remembered project activity", () => { - expect(hasPersistedServerData({ ...empty(), list: ["https://example.com"] })).toBe(true) - expect(hasPersistedServerData({ ...empty(), projects: { local: [{ worktree: "/code", expanded: true }] } })).toBe( - true, - ) - expect(hasPersistedServerData({ ...empty(), lastProject: { local: "/code" } })).toBe(true) - expect(hasPersistedServerData({ ...empty(), recentlyClosed: { local: ["/code"] } })).toBe(true) - }) -}) - test("treats WSL sidecars as remote server connections", () => { expect( ServerConnection.local({ diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx index d6ba573990..450129f458 100644 --- a/packages/app/src/context/server.tsx +++ b/packages/app/src/context/server.tsx @@ -12,7 +12,6 @@ type ServerProjectState = { lastProject: Record recentlyClosed: Record } -type PersistedServerState = ServerProjectState & { list: StoredServer[] } const HEALTH_POLL_INTERVAL_MS = 10_000 // The store retains more history than is displayed. Consumers filter recently closed entries // against the live project list (dropping deleted projects) and then cap the visible count via @@ -179,13 +178,6 @@ export function resolveServerList(input: { return [...deduped.values()] } -export function hasPersistedServerData(store: PersistedServerState) { - if (store.list.length > 0) return true - if (Object.values(store.projects).some((projects) => projects.length > 0)) return true - if (Object.values(store.lastProject).some(Boolean)) return true - return Object.values(store.recentlyClosed).some((projects) => projects.length > 0) -} - export namespace ServerConnection { type Base = { displayName?: string; label?: string } @@ -339,12 +331,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext( () => allServers().find((s) => ServerConnection.key(s) === state.active) ?? allServers()[0], ) const isLocal = createMemo(() => ServerConnection.local(current())) - const hasPersistedData = createMemo(() => hasPersistedServerData(store)) return { ready: isReady, isLocal, - hasPersistedData, get key() { return state.active }, diff --git a/packages/app/src/context/settings.test.ts b/packages/app/src/context/settings.test.ts index cf54e2a2cf..052563d5be 100644 --- a/packages/app/src/context/settings.test.ts +++ b/packages/app/src/context/settings.test.ts @@ -1,36 +1,15 @@ import { describe, expect, test } from "bun:test" import { - hasMeaningfulLayoutData, layoutTransitionState, maximumSunsetTimeout, - migrateSettings, newLayoutDesignsDefault, nextSunsetCheckDelay, - resolveLayoutTransitionClassification, resolveNewLayoutDesigns, } from "./settings" describe("layout transition", () => { test("blank profiles default to the new layout", () => { expect(newLayoutDesignsDefault).toBe(true) - expect( - hasMeaningfulLayoutData({ settings: false, server: false, wsl: false, projects: false, sessions: false }), - ).toBe(false) - }) - - test("recognizes each source of meaningful prior use", () => { - const blank = { settings: false, server: false, wsl: false, projects: false, sessions: false } - expect(hasMeaningfulLayoutData({ ...blank, settings: true })).toBe(true) - expect(hasMeaningfulLayoutData({ ...blank, server: true })).toBe(true) - expect(hasMeaningfulLayoutData({ ...blank, wsl: true })).toBe(true) - expect(hasMeaningfulLayoutData({ ...blank, projects: true })).toBe(true) - expect(hasMeaningfulLayoutData({ ...blank, sessions: true })).toBe(true) - }) - - test("allows late evidence to promote but never downgrade a cohort", () => { - expect(resolveLayoutTransitionClassification(undefined, false)).toBe(false) - expect(resolveLayoutTransitionClassification(false, true)).toBe(true) - expect(resolveLayoutTransitionClassification(true, false)).toBe(true) }) test("hides the transition until a sunset is scheduled", () => { @@ -38,9 +17,6 @@ describe("layout transition", () => { }) test("existing profiles can switch before sunset", () => { - expect(migrateSettings({ general: { newLayoutDesigns: false } })).toEqual({ - general: { newLayoutDesigns: false, layoutTransitionSettingsPresent: true }, - }) expect(layoutTransitionState(true, true, false, false)).toEqual({ available: true, notice: false }) }) @@ -61,9 +37,4 @@ describe("layout transition", () => { expect(nextSunsetCheckDelay(10_000, 9_000)).toBe(1_000) expect(nextSunsetCheckDelay(9_000, 10_000)).toBe(0) }) - - test("migration does not reclassify fresh profiles", () => { - const settings = { general: { newLayoutDesigns: true, layoutTransitionEligible: false } } - expect(migrateSettings(settings)).toBe(settings) - }) }) diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index 923bc61fe0..35adaf0e88 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -35,7 +35,6 @@ export interface Settings { mobileTitlebarPosition: "top" | "bottom" newLayoutDesigns?: boolean layoutTransitionEligible?: boolean - layoutTransitionSettingsPresent?: boolean newInterfaceNoticeDismissed?: boolean } appearance: { @@ -60,27 +59,6 @@ export const newLayoutDesignsDefault = true // Existing users can switch layouts until local midnight on this date. Set new Date(YYYY, M-1, D) to show. export const oldInterfaceSunset = null as Date | null -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value) -} - -export function migrateSettings(value: unknown) { - if (!isRecord(value)) return value - const general = isRecord(value.general) ? value.general : {} - if ( - typeof general.layoutTransitionEligible === "boolean" || - typeof general.layoutTransitionSettingsPresent === "boolean" - ) - return value - return { - ...value, - general: { - ...general, - layoutTransitionSettingsPresent: true, - }, - } -} - export function layoutTransitionState(scheduled: boolean, eligible: boolean, retired: boolean, dismissed: boolean) { return { available: scheduled && eligible && !retired, @@ -99,21 +77,6 @@ export function resolveNewLayoutDesigns(retired: boolean, preference: boolean | return preference ?? fallback } -export function hasMeaningfulLayoutData(input: { - settings: boolean - server: boolean - wsl: boolean - projects: boolean - sessions: boolean -}) { - return input.settings || input.server || input.wsl || input.projects || input.sessions -} - -export function resolveLayoutTransitionClassification(current: boolean | undefined, existing: boolean) { - if (current === true || existing) return true - return false -} - const monoFallback = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' @@ -212,10 +175,7 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont name: "Settings", gate: false, init: () => { - const [store, setStore, _, ready] = persisted( - { key: "settings.v3", migrate: migrateSettings }, - createStore(defaultSettings), - ) + const [store, setStore, _, ready] = persisted("settings.v3", createStore(defaultSettings)) const showFileTree = withFallback(() => store.general?.showFileTree, defaultSettings.general.showFileTree) const showSearch = withFallback(() => store.general?.showSearch, defaultSettings.general.showSearch) const showStatus = withFallback(() => store.general?.showStatus, defaultSettings.general.showStatus) @@ -229,10 +189,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont () => typeof store.general?.layoutTransitionEligible === "boolean", ) const layoutTransitionEligible = withFallback(() => store.general?.layoutTransitionEligible, false) - const layoutTransitionSettingsPresent = withFallback( - () => store.general?.layoutTransitionSettingsPresent, - false, - ) const newInterfaceNoticeDismissed = withFallback(() => store.general?.newInterfaceNoticeDismissed, false) const layoutTransition = createMemo(() => layoutTransitionState(!!sunset, layoutTransitionEligible(), oldInterfaceRetired(), newInterfaceNoticeDismissed()), @@ -361,12 +317,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont setStore("general", "newLayoutDesigns", oldInterfaceRetired() ? true : value) }, layoutTransitionClassified, - layoutTransitionSettingsPresent, - classifyLayoutTransition(existing: boolean) { + setOldLayoutEligible(eligible: boolean) { const current = store.general?.layoutTransitionEligible - const next = resolveLayoutTransitionClassification(current, existing) - if (current === next) return - setStore("general", "layoutTransitionEligible", next) + if (typeof current === "boolean") return + setStore("general", "layoutTransitionEligible", eligible) }, layoutTransitionAvailable: createMemo(() => ready() && layoutTransition().available), newInterfaceNoticeVisible: createMemo(() => ready() && layoutTransition().notice), diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index ced001cecc..d18e05fc9c 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -3,6 +3,7 @@ export { useLayout } from "./context/layout" export { useServerSDK } from "./context/server-sdk" export { useServerSync } from "./context/server-sync" export { useServer } from "./context/server" +export { useSettings } from "./context/settings" export { useTabs } from "./context/tabs" export { useProviders } from "./hooks/use-providers" export { ACCEPTED_FILE_EXTENSIONS, ACCEPTED_FILE_TYPES, filePickerFilters } from "./constants/file-picker" diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index acceabfef0..c21d77fc24 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -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), diff --git a/packages/desktop/src/main/install-state.test.ts b/packages/desktop/src/main/install-state.test.ts new file mode 100644 index 0000000000..e85d11815e --- /dev/null +++ b/packages/desktop/src/main/install-state.test.ts @@ -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) + }) +}) diff --git a/packages/desktop/src/main/install-state.ts b/packages/desktop/src/main/install-state.ts new file mode 100644 index 0000000000..32f1df3640 --- /dev/null +++ b/packages/desktop/src/main/install-state.ts @@ -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" + }) +} diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts index d6bc5bb3a8..1103401c49 100644 --- a/packages/desktop/src/main/ipc.ts +++ b/packages/desktop/src/main/ipc.ts @@ -29,6 +29,7 @@ type Deps = { setDefaultServerUrl: (url: string | null) => Promise | void isFirstLaunchOnboardingPending: () => Promise | boolean finishFirstLaunchOnboarding: (createDefaultProject: boolean) => Promise | string | null + isOldLayoutEligible: () => Promise | boolean getDisplayBackend: () => Promise setDisplayBackend: (backend: string | null) => Promise | void parseMarkdown: (markdown: string) => Promise | 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), diff --git a/packages/desktop/src/main/onboarding.ts b/packages/desktop/src/main/onboarding.ts index 926506e7b9..b8172728b1 100644 --- a/packages/desktop/src/main/onboarding.ts +++ b/packages/desktop/src/main/onboarding.ts @@ -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 }) diff --git a/packages/desktop/src/main/store-keys.ts b/packages/desktop/src/main/store-keys.ts index 506924b74a..f6f4030dba 100644 --- a/packages/desktop/src/main/store-keys.ts +++ b/packages/desktop/src/main/store-keys.ts @@ -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" diff --git a/packages/desktop/src/preload/index.ts b/packages/desktop/src/preload/index.ts index a12864c559..d14dd0d00a 100644 --- a/packages/desktop/src/preload/index.ts +++ b/packages/desktop/src/preload/index.ts @@ -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), diff --git a/packages/desktop/src/preload/types.ts b/packages/desktop/src/preload/types.ts index b94acd6615..da10f7008d 100644 --- a/packages/desktop/src/preload/types.ts +++ b/packages/desktop/src/preload/types.ts @@ -52,6 +52,7 @@ export type ElectronAPI = { setDefaultServerUrl: (url: string | null) => Promise isFirstLaunchOnboardingPending: () => Promise finishFirstLaunchOnboarding: (createDefaultProject: boolean) => Promise + isOldLayoutEligible: () => Promise getDisplayBackend: () => Promise setDisplayBackend: (backend: LinuxDisplayBackend | null) => Promise parseMarkdownCommand: (markdown: string) => Promise diff --git a/packages/desktop/src/renderer/onboarding.tsx b/packages/desktop/src/renderer/onboarding.tsx index 47758bc788..916116a55b 100644 --- a/packages/desktop/src/renderer/onboarding.tsx +++ b/packages/desktop/src/renderer/onboarding.tsx @@ -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()