chore: merge dev into v2 (#36918)
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: James Long <jlongster@users.noreply.github.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: Victor Navarro <vn4varro@gmail.com> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Nabs <nabil@instafork.com> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: AidenGeunGeun <eastlandwyvern@gmail.com> Co-authored-by: Mark <geraint0923@users.noreply.github.com> Co-authored-by: Jay <air@live.ca>
This commit is contained in:
co-authored by
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Aiden Cline
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Luke Parker
usrnk1
Aarav Sareen
Brendan Allan
Jack
David Hill
Jay
Brendan Allan
Simon Klee
opencode
Aiden Cline
Adam
James Long
James Long
Dustin Deus
冯基魁
Frank
Aiden Cline
Kit Langton
Victor Navarro
Dax Raad
Dax
Nabs
Vladimir Glafirov
AidenGeunGeun
Mark
Jay
parent
a32c480d45
commit
0d4f9797f4
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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,11 +1,28 @@
|
||||
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"
|
||||
const DEFAULT_PROJECT_DIR = "Default 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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -36,6 +36,7 @@ protocol.registerSchemesAsPrivileged([
|
||||
secure: true,
|
||||
standard: true,
|
||||
supportFetchAPI: true,
|
||||
stream: true,
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -266,7 +267,10 @@ export function registerRendererProtocol() {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await net.fetch(pathToFileURL(file).toString())
|
||||
const range = request.headers.get("range")
|
||||
const response = await net.fetch(pathToFileURL(file).toString(), {
|
||||
headers: range ? { range } : undefined,
|
||||
})
|
||||
if (response.status >= 400) {
|
||||
writeLog(
|
||||
"protocol",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
import {
|
||||
ServerConnection,
|
||||
useLayout,
|
||||
useProviders,
|
||||
useServer,
|
||||
useServerSDK,
|
||||
useServerSync,
|
||||
useTabs,
|
||||
} from "@opencode-ai/app"
|
||||
import { onMount, startTransition } from "solid-js"
|
||||
import { ServerConnection, useServer, useSettings, useTabs } from "@opencode-ai/app"
|
||||
import { onMount } from "solid-js"
|
||||
|
||||
export function DesktopFirstLaunchOnboarding(props: { initialUrl: string; onLoaded: () => void }) {
|
||||
const server = useServer()
|
||||
const serverSDK = useServerSDK()
|
||||
const serverSync = useServerSync()
|
||||
const layout = useLayout()
|
||||
const providers = useProviders()
|
||||
const settings = useSettings()
|
||||
const tabs = useTabs()
|
||||
|
||||
onMount(() => {
|
||||
@@ -24,39 +13,26 @@ export function DesktopFirstLaunchOnboarding(props: { initialUrl: string; onLoad
|
||||
async function runFirstLaunchOnboarding() {
|
||||
try {
|
||||
await Promise.all(
|
||||
[server.ready.promise, layout.ready.promise, tabs.ready.promise, tabs.recentReady.promise].map(
|
||||
(p) => p ?? Promise.resolve(),
|
||||
),
|
||||
[server.ready.promise, tabs.ready.promise, tabs.recentReady.promise].map((p) => p ?? Promise.resolve()),
|
||||
)
|
||||
const existingInstall = await window.api.isOldLayoutEligible()
|
||||
settings.general.setOldLayoutEligible(existingInstall)
|
||||
if (!server.isLocal()) return
|
||||
|
||||
const pending = await window.api.isFirstLaunchOnboardingPending()
|
||||
if (!pending) return
|
||||
|
||||
const sessions = await serverSDK()
|
||||
.client.session.list()
|
||||
.then((x) => x.data ?? [])
|
||||
.catch(() => undefined)
|
||||
const connectedProviders = providers.connected()
|
||||
const paidProviders = providers.paid()
|
||||
const persistedProjects = layout.projects.list()
|
||||
const shouldTrigger =
|
||||
!existingInstall &&
|
||||
props.initialUrl === "/" &&
|
||||
sessions?.length === 0 &&
|
||||
paidProviders.length === 0 &&
|
||||
persistedProjects.length === 0 &&
|
||||
tabs.store.length === 0 &&
|
||||
server.list.every(ServerConnection.builtin)
|
||||
|
||||
console.info("[desktop-onboarding] first launch onboarding evaluated", {
|
||||
pending,
|
||||
shouldTrigger,
|
||||
existingInstall,
|
||||
initialUrl: props.initialUrl,
|
||||
sessions: sessions?.length,
|
||||
connectedProviders: connectedProviders.length,
|
||||
paidProviders: paidProviders.length,
|
||||
serverProjects: serverSync().data.project.length,
|
||||
persistedProjects: persistedProjects.length,
|
||||
tabs: tabs.store.length,
|
||||
servers: server.list.map(ServerConnection.key),
|
||||
})
|
||||
@@ -67,9 +43,7 @@ export function DesktopFirstLaunchOnboarding(props: { initialUrl: string; onLoad
|
||||
console.info("[desktop-onboarding] starting first launch draft", { directory })
|
||||
server.projects.open(directory)
|
||||
server.projects.touch(directory)
|
||||
await startTransition(() => {
|
||||
tabs.newDraft({ server: server.key, directory })
|
||||
})
|
||||
tabs.select(await tabs.newDraft({ server: server.key, directory }))
|
||||
} catch (error) {
|
||||
console.error("[desktop-onboarding] first launch onboarding failed", error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user