fix(desktop): derive layout eligibility from install state
This commit is contained in:
parent
588288ce87
commit
9ecfb847fa
15 changed files with 85 additions and 258 deletions
|
|
@ -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<T>(promise: Promise<T>, onTimeout: () => void, timeoutMs = 10_000) {
|
||||
return new Promise<T>((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<typeof setTimeout> | undefined }
|
||||
const wslState = {
|
||||
current: undefined as ReturnType<NonNullable<typeof platform.wslServers>["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 (
|
||||
<Show
|
||||
when={settings.general.layoutTransitionClassified()}
|
||||
fallback={
|
||||
<div class="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-background-base">
|
||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
// 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: {
|
|||
<GlobalProvider>
|
||||
<SettingsProvider>
|
||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck} startup={props.startup}>
|
||||
<LayoutTransitionGate>
|
||||
<Show when={useSettings().general.newLayoutDesigns().toString()} keyed>
|
||||
<Dynamic
|
||||
component={props.router ?? Router}
|
||||
root={(routerProps) => (
|
||||
<TabsProvider>
|
||||
<NotificationProvider>
|
||||
<ServerShell>
|
||||
<Show when={useSettings().general.newLayoutDesigns()} fallback={routerProps.children}>
|
||||
<NewAppLayout serverScoped={props.serverScoped}>{routerProps.children}</NewAppLayout>
|
||||
</Show>
|
||||
</ServerShell>
|
||||
</NotificationProvider>
|
||||
</TabsProvider>
|
||||
)}
|
||||
>
|
||||
<Routes serverScoped={props.serverScoped} />
|
||||
</Dynamic>
|
||||
</Show>
|
||||
</LayoutTransitionGate>
|
||||
<Show when={useSettings().general.newLayoutDesigns().toString()} keyed>
|
||||
<Dynamic
|
||||
component={props.router ?? Router}
|
||||
root={(routerProps) => (
|
||||
<TabsProvider>
|
||||
<NotificationProvider>
|
||||
<ServerShell>
|
||||
<Show when={useSettings().general.newLayoutDesigns()} fallback={routerProps.children}>
|
||||
<NewAppLayout serverScoped={props.serverScoped}>{routerProps.children}</NewAppLayout>
|
||||
</Show>
|
||||
</ServerShell>
|
||||
</NotificationProvider>
|
||||
</TabsProvider>
|
||||
)}
|
||||
>
|
||||
<Routes serverScoped={props.serverScoped} />
|
||||
</Dynamic>
|
||||
</Show>
|
||||
</ConnectionGate>
|
||||
</SettingsProvider>
|
||||
</GlobalProvider>
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ type ServerProjectState = {
|
|||
lastProject: Record<string, string>
|
||||
recentlyClosed: Record<string, string[]>
|
||||
}
|
||||
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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> {
|
||||
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<Settings>(defaultSettings),
|
||||
)
|
||||
const [store, setStore, _, ready] = persisted("settings.v3", createStore<Settings>(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),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
19
packages/desktop/src/main/install-state.test.ts
Normal file
19
packages/desktop/src/main/install-state.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
8
packages/desktop/src/main/install-state.ts
Normal file
8
packages/desktop/src/main/install-state.ts
Normal 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"
|
||||
})
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue