mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-22 03:45:23 +00:00
review
This commit is contained in:
@@ -5,10 +5,11 @@ import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { createEffect, createMemo, For, Match, onCleanup, Show, Switch } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import type { WslServerStep } from "@/context/platform"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useWslServers } from "@/context/wsl-servers"
|
||||
|
||||
type WslServerStep = "wsl" | "distro" | "opencode"
|
||||
|
||||
const STEPS: WslServerStep[] = ["wsl", "distro", "opencode"]
|
||||
|
||||
function isHiddenDistro(name: string) {
|
||||
|
||||
@@ -33,7 +33,6 @@ import { Persist, persisted } from "@/utils/persist"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { createSessionTabs } from "@/pages/session/helpers"
|
||||
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
||||
@@ -113,7 +112,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const dialog = useDialog()
|
||||
const providers = useProviders()
|
||||
const command = useCommand()
|
||||
const server = useServer()
|
||||
const permission = usePermission()
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
@@ -1256,9 +1254,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
|
||||
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
|
||||
queries: [
|
||||
loadAgentsQuery(sdk.directory, server.key),
|
||||
loadProvidersQuery(null, server.key),
|
||||
loadProvidersQuery(sdk.directory, server.key),
|
||||
loadAgentsQuery(sdk.directory),
|
||||
loadProvidersQuery(null),
|
||||
loadProvidersQuery(sdk.directory),
|
||||
],
|
||||
}))
|
||||
|
||||
|
||||
@@ -194,11 +194,11 @@ export const Terminal = (props: TerminalProps) => {
|
||||
const server = useServer()
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const url = sdk.url
|
||||
const auth = server.current?.http
|
||||
const username = auth?.username ?? "opencode"
|
||||
const password = auth?.password ?? ""
|
||||
const currentUrl = () => server.current?.http.url ?? sdk.url
|
||||
const sameOrigin = () => new URL(currentUrl(), location.href).origin === location.origin
|
||||
const sameOrigin = new URL(url, location.href).origin === location.origin
|
||||
let container!: HTMLDivElement
|
||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
|
||||
const id = local.pty.id
|
||||
@@ -525,18 +525,11 @@ export const Terminal = (props: TerminalProps) => {
|
||||
if (disposed) return
|
||||
drop?.()
|
||||
|
||||
const baseUrl = currentUrl()
|
||||
if (sdk.url !== baseUrl) {
|
||||
console.error(
|
||||
`[terminal panic] sdk.url mismatch id=${id} serverKey=${server.key ?? ""} directory=${directory} sdkUrl=${sdk.url} currentUrl=${baseUrl}`,
|
||||
)
|
||||
}
|
||||
|
||||
const next = new URL(baseUrl + `/pty/${id}/connect`)
|
||||
const next = new URL(url + `/pty/${id}/connect`)
|
||||
next.searchParams.set("directory", directory)
|
||||
next.searchParams.set("cursor", String(seek))
|
||||
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
|
||||
if (!sameOrigin() && password) {
|
||||
if (!sameOrigin && password) {
|
||||
next.searchParams.set("auth_token", btoa(`${username}:${password}`))
|
||||
// For same-origin requests, let the browser reuse the page's existing auth.
|
||||
next.username = username
|
||||
@@ -549,7 +542,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
directory,
|
||||
restoreLength: restore.length,
|
||||
sdkUrl: sdk.url,
|
||||
currentUrl: baseUrl,
|
||||
currentUrl: url,
|
||||
wsUrl: next.toString(),
|
||||
})
|
||||
|
||||
@@ -564,7 +557,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
id,
|
||||
serverKey: server.key ?? null,
|
||||
directory,
|
||||
currentUrl: baseUrl,
|
||||
currentUrl: url,
|
||||
})
|
||||
// Paint the saved buffer now that we've confirmed the pty really
|
||||
// exists on the current sidecar. Fire-and-forget: write()'s own
|
||||
@@ -630,7 +623,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
directory,
|
||||
code: event.code,
|
||||
reason: event.reason || null,
|
||||
currentUrl: baseUrl,
|
||||
currentUrl: url,
|
||||
})
|
||||
retry(new Error(language.t("terminal.connectionLost.abnormalClose", { code: event.code })))
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import type { InitError } from "../pages/error"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { useServer } from "./server"
|
||||
import { bootstrapDirectory, bootstrapGlobal, clearProviderRev } from "./global-sync/bootstrap"
|
||||
import { createChildStoreManager } from "./global-sync/child-store"
|
||||
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./global-sync/event-reducer"
|
||||
@@ -41,12 +40,11 @@ type GlobalStore = {
|
||||
reload: undefined | "pending" | "complete"
|
||||
}
|
||||
|
||||
export const loadSessionsQuery = (directory: string, serverKey: string | undefined) =>
|
||||
queryOptions<null>({ queryKey: [serverKey, directory, "loadSessions"], queryFn: skipToken })
|
||||
export const loadSessionsQuery = (directory: string) =>
|
||||
queryOptions<null>({ queryKey: [directory, "loadSessions"], queryFn: skipToken })
|
||||
|
||||
function createGlobalSync() {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const server = useServer()
|
||||
const language = useLanguage()
|
||||
const owner = getOwner()
|
||||
if (!owner) throw new Error("GlobalSync must be created within owner")
|
||||
@@ -171,7 +169,7 @@ function createGlobalSync() {
|
||||
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
||||
const promise = queryClient
|
||||
.fetchQuery({
|
||||
...loadSessionsQuery(directory, server.key),
|
||||
...loadSessionsQuery(directory),
|
||||
queryFn: () =>
|
||||
loadRootSessionsWithFallback({
|
||||
directory,
|
||||
@@ -237,7 +235,6 @@ function createGlobalSync() {
|
||||
const sdk = sdkFor(directory)
|
||||
await bootstrapDirectory({
|
||||
directory,
|
||||
serverKey: server.key,
|
||||
global: {
|
||||
config: globalStore.config,
|
||||
path: globalStore.path,
|
||||
@@ -337,7 +334,6 @@ function createGlobalSync() {
|
||||
try {
|
||||
await bootstrapGlobal({
|
||||
globalSDK: globalSDK.client,
|
||||
serverKey: server.key,
|
||||
requestFailedTitle: language.t("common.requestFailed"),
|
||||
translate: language.t,
|
||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||
|
||||
@@ -68,7 +68,6 @@ function runAll(list: Array<() => Promise<unknown>>) {
|
||||
|
||||
export async function bootstrapGlobal(input: {
|
||||
globalSDK: OpencodeClient
|
||||
serverKey: string | undefined
|
||||
requestFailedTitle: string
|
||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||
formatMoreCount: (count: number) => string
|
||||
@@ -87,7 +86,7 @@ export async function bootstrapGlobal(input: {
|
||||
const slow = [
|
||||
() =>
|
||||
input.queryClient.fetchQuery({
|
||||
...loadProvidersQuery(null, input.serverKey),
|
||||
...loadProvidersQuery(null),
|
||||
queryFn: () =>
|
||||
retry(() =>
|
||||
input.globalSDK.provider.list().then((x) => {
|
||||
@@ -180,17 +179,16 @@ function warmSessions(input: {
|
||||
).then(() => undefined)
|
||||
}
|
||||
|
||||
export const loadProvidersQuery = (directory: string | null, serverKey: string | undefined) =>
|
||||
queryOptions<null>({ queryKey: [serverKey, directory, "providers"], queryFn: skipToken })
|
||||
export const loadProvidersQuery = (directory: string | null) =>
|
||||
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
|
||||
|
||||
export const loadAgentsQuery = (
|
||||
directory: string | null,
|
||||
serverKey: string | undefined,
|
||||
sdk?: OpencodeClient,
|
||||
transform?: (x: Awaited<ReturnType<OpencodeClient["app"]["agents"]>>) => void,
|
||||
) =>
|
||||
queryOptions<null>({
|
||||
queryKey: [serverKey, directory, "agents"],
|
||||
queryKey: [directory, "agents"],
|
||||
queryFn:
|
||||
sdk && transform
|
||||
? () =>
|
||||
@@ -224,7 +222,6 @@ export const loadPathQuery = (
|
||||
|
||||
export async function bootstrapDirectory(input: {
|
||||
directory: string
|
||||
serverKey: string | undefined
|
||||
sdk: OpencodeClient
|
||||
store: Store<State>
|
||||
setStore: SetStoreFunction<State>
|
||||
@@ -266,9 +263,7 @@ export async function bootstrapDirectory(input: {
|
||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||
() =>
|
||||
input.queryClient.ensureQueryData(
|
||||
loadAgentsQuery(input.directory, input.serverKey, input.sdk, (x) =>
|
||||
input.setStore("agent", normalizeAgentList(x.data)),
|
||||
),
|
||||
loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))),
|
||||
),
|
||||
() =>
|
||||
retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))),
|
||||
@@ -354,7 +349,7 @@ export async function bootstrapDirectory(input: {
|
||||
),
|
||||
() =>
|
||||
input.queryClient.ensureQueryData({
|
||||
...loadProvidersQuery(input.directory, input.serverKey),
|
||||
...loadProvidersQuery(input.directory),
|
||||
queryFn: () =>
|
||||
retry(() => input.sdk.provider.list())
|
||||
.then((x) => {
|
||||
|
||||
@@ -9,17 +9,13 @@ type OpenFilePickerOptions = { title?: string; multiple?: boolean; accept?: stri
|
||||
type SaveFilePickerOptions = { title?: string; defaultPath?: string }
|
||||
type UpdateInfo = { updateAvailable: boolean; version?: string }
|
||||
|
||||
export type WslServerStep = "wsl" | "distro" | "opencode"
|
||||
|
||||
export type WslRuntimeCheck = {
|
||||
available: boolean
|
||||
version: string | null
|
||||
status: string | null
|
||||
error: string | null
|
||||
}
|
||||
export type WslInstalledDistro = {
|
||||
name: string
|
||||
state: string | null
|
||||
version: number | null
|
||||
isDefault: boolean
|
||||
}
|
||||
@@ -32,7 +28,6 @@ export type WslDistroProbe = {
|
||||
canExecute: boolean
|
||||
hasBash: boolean
|
||||
hasCurl: boolean
|
||||
username: string | null
|
||||
isRoot: boolean | null
|
||||
error: string | null
|
||||
}
|
||||
@@ -95,8 +90,6 @@ export type WslServersPlatform = {
|
||||
addServer(distro: string): Promise<WslServerConfig>
|
||||
removeServer(id: string): Promise<void>
|
||||
startServer(id: string): Promise<void>
|
||||
stopServer(id: string): Promise<void>
|
||||
cancelJob(): Promise<void>
|
||||
}
|
||||
|
||||
export type Platform = {
|
||||
|
||||
@@ -228,19 +228,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
})
|
||||
}
|
||||
|
||||
const check = (conn: ServerConnection.Any) =>
|
||||
checkServerHealth(conn.http).then((x) => {
|
||||
if (!x.healthy) {
|
||||
// Electron's console-message bridge only preserves the first
|
||||
// console argument, so pre-stringify everything into one string.
|
||||
console.warn(
|
||||
`[server health] unhealthy key=${ServerConnection.key(conn)} url=${conn.http.url} hasAuth=${!!(
|
||||
conn.http.username || conn.http.password
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
return x.healthy
|
||||
})
|
||||
const check = (conn: ServerConnection.Any) => checkServerHealth(conn.http).then((x) => x.healthy)
|
||||
|
||||
createEffect(() => {
|
||||
const key = state.active
|
||||
@@ -275,7 +263,6 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
return
|
||||
}
|
||||
setState("healthy", undefined)
|
||||
console.log(`[server health] start polling key=${ServerConnection.key(current_)} url=${current_.http.url}`)
|
||||
onCleanup(startHealthPolling(current_))
|
||||
})
|
||||
|
||||
|
||||
@@ -4,23 +4,22 @@ import { createEffect, onCleanup } from "solid-js"
|
||||
import type { WslServersPlatform, WslServersState } from "./platform"
|
||||
import { usePlatform } from "./platform"
|
||||
|
||||
export const wslServersQueryKey = ["platform", "wslServers"] as const
|
||||
|
||||
export function wslServersQueryOptions(api: WslServersPlatform | undefined) {
|
||||
return queryOptions<WslServersState>({
|
||||
queryKey: wslServersQueryKey,
|
||||
queryFn: api ? () => api.getState() : skipToken,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
gcTime: Number.POSITIVE_INFINITY,
|
||||
})
|
||||
}
|
||||
const wslServersQueryKey = ["platform", "wslServers"] as const
|
||||
|
||||
export const { use: useWslServers, provider: WslServersProvider } = createSimpleContext({
|
||||
name: "WslServers",
|
||||
init: () => {
|
||||
const platform = usePlatform()
|
||||
const queryClient = useQueryClient()
|
||||
const query = useQuery(() => ({ ...wslServersQueryOptions(platform.wslServers) }))
|
||||
const query = useQuery(() => {
|
||||
const api = platform.wslServers
|
||||
return queryOptions<WslServersState>({
|
||||
queryKey: wslServersQueryKey,
|
||||
queryFn: api ? () => api.getState() : skipToken,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
gcTime: Number.POSITIVE_INFINITY,
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const api = platform.wslServers
|
||||
|
||||
@@ -16,7 +16,6 @@ export {
|
||||
type WslServersEvent,
|
||||
type WslServersPlatform,
|
||||
type WslServersState,
|
||||
type WslServerStep,
|
||||
} from "./context/platform"
|
||||
export { ServerConnection } from "./context/server"
|
||||
export { handleNotificationClick } from "./utils/notification-click"
|
||||
|
||||
@@ -16,7 +16,6 @@ import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { type LocalProject } from "@/context/layout"
|
||||
import { loadSessionsQuery, useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useServer } from "@/context/server"
|
||||
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
||||
import { sortedRootSessions, workspaceKey } from "./helpers"
|
||||
import { useQuery } from "@tanstack/solid-query"
|
||||
@@ -320,19 +319,12 @@ export const SortableWorkspace = (props: {
|
||||
})
|
||||
const slug = createMemo(() => base64Encode(props.directory))
|
||||
const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow()))
|
||||
// Guard against `props.project` being transiently undefined during a
|
||||
// server-switch cascade. The parent renders
|
||||
// <For each={workspaces()}>{(dir) => <SortableWorkspace project={project()!} ... />}</For>
|
||||
// where `project()` can flip to undefined while the enclosing <Show when={project()}>
|
||||
// gate hasn't yet unmounted this child. Bootstrap's setStore can then fire
|
||||
// these memos with stale props.
|
||||
const local = createMemo(() => props.directory === (props.project?.worktree ?? ""))
|
||||
const local = createMemo(() => props.directory === props.project.worktree)
|
||||
const active = createMemo(() => workspaceKey(props.ctx.currentDir()) === workspaceKey(props.directory))
|
||||
const server = useServer()
|
||||
const workspaceValue = createMemo(() => {
|
||||
const branch = workspaceStore.vcs?.branch
|
||||
const name = branch ?? getFilename(props.directory)
|
||||
const projectId = props.project?.id
|
||||
const projectId = props.project.id
|
||||
if (!projectId) return name
|
||||
return props.ctx.workspaceName(props.directory, projectId, branch) ?? name
|
||||
})
|
||||
@@ -340,7 +332,7 @@ export const SortableWorkspace = (props: {
|
||||
const boot = createMemo(() => open() || active())
|
||||
const count = createMemo(() => sessions()?.length ?? 0)
|
||||
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
|
||||
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree, server.key) }))
|
||||
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
|
||||
const busy = createMemo(() => props.ctx.isBusy(props.directory))
|
||||
const loading = () => query.isLoading && count() === 0
|
||||
const touch = createMediaQuery("(hover: none)")
|
||||
@@ -364,7 +356,7 @@ export const SortableWorkspace = (props: {
|
||||
InlineEditor={props.ctx.InlineEditor}
|
||||
renameWorkspace={props.ctx.renameWorkspace}
|
||||
setEditor={props.ctx.setEditor}
|
||||
projectId={props.project?.id ?? ""}
|
||||
projectId={props.project.id ?? ""}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -433,7 +425,7 @@ export const SortableWorkspace = (props: {
|
||||
openEditor={props.ctx.openEditor}
|
||||
showResetWorkspaceDialog={props.ctx.showResetWorkspaceDialog}
|
||||
showDeleteWorkspaceDialog={props.ctx.showDeleteWorkspaceDialog}
|
||||
root={props.project?.worktree ?? props.directory}
|
||||
root={props.project.worktree}
|
||||
clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
|
||||
navigateToNewSession={() => navigate(`/${slug()}/session`)}
|
||||
/>
|
||||
@@ -467,33 +459,20 @@ export const LocalWorkspace = (props: {
|
||||
}): JSX.Element => {
|
||||
const globalSync = useGlobalSync()
|
||||
const language = useLanguage()
|
||||
const server = useServer()
|
||||
// Same guard pattern as SortableWorkspace: the parent passes
|
||||
// `project={project()!}` but `project()` can transiently flip to
|
||||
// undefined during a server-switch cascade before this component
|
||||
// unmounts, so every reactive memo reading props.project has to
|
||||
// tolerate undefined.
|
||||
const worktree = createMemo(() => props.project?.worktree ?? "")
|
||||
const worktree = createMemo(() => props.project.worktree)
|
||||
const workspace = createMemo(() => {
|
||||
const dir = worktree()
|
||||
if (!dir) return undefined
|
||||
const [store, setStore] = globalSync.child(dir)
|
||||
const [store, setStore] = globalSync.child(worktree())
|
||||
return { store, setStore }
|
||||
})
|
||||
const slug = createMemo(() => (worktree() ? base64Encode(worktree()) : ""))
|
||||
const sessions = createMemo(() => {
|
||||
const store = workspace()?.store
|
||||
return store ? sortedRootSessions(store, props.sortNow()) : []
|
||||
})
|
||||
const booted = createMemo((prev) => prev || workspace()?.store.status === "complete", false)
|
||||
const slug = createMemo(() => base64Encode(worktree()))
|
||||
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
|
||||
const count = createMemo(() => sessions()?.length ?? 0)
|
||||
const query = useQuery(() => ({ ...loadSessionsQuery(worktree(), server.key) }))
|
||||
const query = useQuery(() => ({ ...loadSessionsQuery(worktree()) }))
|
||||
const loading = createMemo(() => query.isPending && count() === 0)
|
||||
const hasMore = createMemo(() => (workspace()?.store.sessionTotal ?? 0) > count())
|
||||
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
|
||||
const loadMore = async () => {
|
||||
const dir = worktree()
|
||||
if (!dir) return
|
||||
workspace()?.setStore("limit", (limit) => (limit ?? 0) + 5)
|
||||
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
||||
await globalSync.project.loadSessions(dir)
|
||||
}
|
||||
|
||||
|
||||
@@ -65,21 +65,6 @@ function retryable(error: unknown, signal?: AbortSignal) {
|
||||
return /network|fetch|econnreset|econnrefused|enotfound|timedout/i.test(error.message)
|
||||
}
|
||||
|
||||
function serializeError(error: unknown): unknown {
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
function stringifyLog(label: string, value: unknown) {
|
||||
return `${label} ${JSON.stringify(value)}`
|
||||
}
|
||||
|
||||
export async function checkServerHealth(
|
||||
server: ServerConnection.HttpBase,
|
||||
fetch: typeof globalThis.fetch,
|
||||
@@ -89,19 +74,7 @@ export async function checkServerHealth(
|
||||
const signal = opts?.signal ?? timeout?.signal
|
||||
const retryCount = opts?.retryCount ?? defaultRetryCount
|
||||
const retryDelayMs = opts?.retryDelayMs ?? defaultRetryDelayMs
|
||||
const logFailure = (phase: string, count: number, error: unknown) => {
|
||||
console.error(
|
||||
stringifyLog("[server health] request failed", {
|
||||
phase,
|
||||
attempt: count + 1,
|
||||
url: server.url,
|
||||
hasAuth: !!server.password,
|
||||
error: serializeError(error),
|
||||
}),
|
||||
)
|
||||
}
|
||||
const next = (count: number, error: unknown) => {
|
||||
logFailure("retry", count, error)
|
||||
if (count >= retryCount || !retryable(error, signal)) return Promise.resolve({ healthy: false } as const)
|
||||
return wait(retryDelayMs * (count + 1), signal)
|
||||
.then(() => attempt(count + 1))
|
||||
|
||||
@@ -181,18 +181,11 @@ async function initialize() {
|
||||
const hostname = "127.0.0.1"
|
||||
const url = `http://${hostname}:${port}`
|
||||
const password = randomUUID()
|
||||
const key = "local:windows"
|
||||
|
||||
const startupData: ServerReadyData = {
|
||||
url,
|
||||
username: "opencode",
|
||||
password,
|
||||
local: {
|
||||
key,
|
||||
url,
|
||||
username: "opencode",
|
||||
password,
|
||||
},
|
||||
}
|
||||
const loadingTask = (async () => {
|
||||
logger.log("sidecar connection started", { url })
|
||||
@@ -371,8 +364,6 @@ registerIpcHandlers({
|
||||
wslServersAddServer: (distro) => wslServers.addServer(distro),
|
||||
wslServersRemoveServer: (id) => wslServers.removeServer(id),
|
||||
wslServersStartServer: (id) => wslServers.startServer(id),
|
||||
wslServersStopServer: (id) => wslServers.stopServer(id),
|
||||
wslServersCancelJob: () => wslServers.cancelJob(),
|
||||
getWindowConfig: () => ({ updaterEnabled: UPDATER_ENABLED }),
|
||||
consumeInitialDeepLinks: () => pendingDeepLinks.splice(0),
|
||||
getDefaultServerUrl: () => getDefaultServerUrl(),
|
||||
|
||||
@@ -37,8 +37,6 @@ type Deps = {
|
||||
wslServersAddServer: (distro: string) => Promise<WslServerConfig> | WslServerConfig
|
||||
wslServersRemoveServer: (id: string) => Promise<void> | void
|
||||
wslServersStartServer: (id: string) => Promise<void> | void
|
||||
wslServersStopServer: (id: string) => Promise<void> | void
|
||||
wslServersCancelJob: () => Promise<void> | void
|
||||
getWindowConfig: () => Promise<WindowConfig> | WindowConfig
|
||||
consumeInitialDeepLinks: () => Promise<string[]> | string[]
|
||||
getDefaultServerUrl: () => Promise<string | null> | string | null
|
||||
@@ -57,13 +55,6 @@ type Deps = {
|
||||
}
|
||||
|
||||
export function registerIpcHandlers(deps: Deps) {
|
||||
const debugStore = (op: string, name: string, key: string, meta?: Record<string, unknown>) => {
|
||||
if (app.isPackaged) return
|
||||
if (!name.startsWith("opencode.workspace.")) return
|
||||
if (!key.includes("terminal")) return
|
||||
console.log(`[store ${op}] ${JSON.stringify({ name, key, ...meta })}`)
|
||||
}
|
||||
|
||||
const requireString = (name: string, value: unknown) => {
|
||||
if (typeof value === "string" && value.length > 0) return value
|
||||
throw new Error(`Invalid ${name}`)
|
||||
@@ -165,10 +156,6 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
handle("wsl-servers-start", (_event: IpcMainInvokeEvent, id: string) =>
|
||||
deps.wslServersStartServer(requireString("server id", id)),
|
||||
)
|
||||
handle("wsl-servers-stop", (_event: IpcMainInvokeEvent, id: string) =>
|
||||
deps.wslServersStopServer(requireString("server id", id)),
|
||||
)
|
||||
handle("wsl-servers-cancel", () => deps.wslServersCancelJob())
|
||||
handle("get-window-config", () => deps.getWindowConfig())
|
||||
handle("consume-initial-deep-links", () => deps.consumeInitialDeepLinks())
|
||||
handle("get-default-server-url", () => deps.getDefaultServerUrl())
|
||||
@@ -195,24 +182,13 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => {
|
||||
const store = getStore(name)
|
||||
const value = store.get(key)
|
||||
debugStore("get", name, key, {
|
||||
found: value !== undefined && value !== null,
|
||||
length:
|
||||
typeof value === "string"
|
||||
? value.length
|
||||
: value === undefined || value === null
|
||||
? 0
|
||||
: JSON.stringify(value).length,
|
||||
})
|
||||
if (value === undefined || value === null) return null
|
||||
return typeof value === "string" ? value : JSON.stringify(value)
|
||||
})
|
||||
handle("store-set", (_event: IpcMainInvokeEvent, name: string, key: string, value: string) => {
|
||||
debugStore("set", name, key, { length: value.length })
|
||||
getStore(name).set(key, value)
|
||||
})
|
||||
handle("store-delete", (_event: IpcMainInvokeEvent, name: string, key: string) => {
|
||||
debugStore("delete", name, key)
|
||||
getStore(name).delete(key)
|
||||
})
|
||||
handle("store-clear", (_event: IpcMainInvokeEvent, name: string) => {
|
||||
|
||||
@@ -120,14 +120,11 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
|
||||
}
|
||||
|
||||
const refreshDistroLists = async (opts: { signal?: AbortSignal }) => {
|
||||
const [installedResult, onlineResult] = await Promise.allSettled([
|
||||
const [installed, online] = await Promise.all([
|
||||
listInstalledWslDistros(opts),
|
||||
listOnlineWslDistros(opts),
|
||||
])
|
||||
return {
|
||||
installed: installedResult.status === "fulfilled" ? installedResult.value : [],
|
||||
online: onlineResult.status === "fulfilled" ? onlineResult.value : [],
|
||||
}
|
||||
return { installed, online }
|
||||
}
|
||||
|
||||
const nextStartAttempt = (id: string) => {
|
||||
@@ -316,12 +313,6 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
|
||||
await openWslTerminal(name)
|
||||
},
|
||||
|
||||
async cancelJob() {
|
||||
jobAbort?.abort()
|
||||
jobAbort = undefined
|
||||
setState({ job: null })
|
||||
},
|
||||
|
||||
async addServer(distro: string): Promise<WslServerConfig> {
|
||||
const id = wslServerIdForDistro(distro)
|
||||
if (state.servers.some((item) => item.config.id === id)) {
|
||||
@@ -349,12 +340,6 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
|
||||
|
||||
startServer,
|
||||
|
||||
async stopServer(id: string) {
|
||||
invalidateStartAttempt(id)
|
||||
await stopServerInternal(id)
|
||||
setRuntime(id, { kind: "stopped" })
|
||||
},
|
||||
|
||||
stopAll() {
|
||||
for (const item of state.servers) invalidateStartAttempt(item.config.id)
|
||||
for (const existing of sidecars.values()) {
|
||||
|
||||
@@ -277,16 +277,13 @@ export async function probeWslRuntime(opts?: RunWslOptions): Promise<WslRuntimeC
|
||||
return {
|
||||
available: false,
|
||||
version: null,
|
||||
status: null,
|
||||
error: summarize(version.stderr || version.stdout) || "WSL is unavailable",
|
||||
}
|
||||
}
|
||||
|
||||
const status = await runWsl(["--status"], opts).catch(() => undefined)
|
||||
return {
|
||||
available: true,
|
||||
version: firstLine(version.stdout),
|
||||
status: status?.code === 0 ? summarize(status.stdout) : null,
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
@@ -351,7 +348,6 @@ export async function probeWslDistro(name: string, opts?: RunWslOptions): Promis
|
||||
canExecute: false,
|
||||
hasBash: false,
|
||||
hasCurl: false,
|
||||
username: null,
|
||||
isRoot: null,
|
||||
error: summarize(executable.stderr || executable.stdout) || "Cannot execute commands in distro",
|
||||
}
|
||||
@@ -369,7 +365,6 @@ export async function probeWslDistro(name: string, opts?: RunWslOptions): Promis
|
||||
canExecute: true,
|
||||
hasBash: bash.code === 0 && summarize(bash.stdout) === "yes",
|
||||
hasCurl: curl.code === 0 && summarize(curl.stdout) === "yes",
|
||||
username: username || null,
|
||||
isRoot: username ? username === "root" : null,
|
||||
error: null,
|
||||
}
|
||||
@@ -472,14 +467,13 @@ function parseInstalledDistros(output: string) {
|
||||
return output.split(/\r?\n/g).flatMap((line) => {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed) return []
|
||||
const match = line.match(/^\s*(\*)?\s*(.*?)\s{2,}(\S+)\s+(\d+)\s*$/)
|
||||
const match = line.match(/^\s*(\*)?\s*(.*?)\s{2,}\S+\s+(\d+)\s*$/)
|
||||
if (!match) return []
|
||||
const [, marker, name, state, version] = match
|
||||
const [, marker, name, version] = match
|
||||
if (!name || /^name$/i.test(name)) return []
|
||||
return [
|
||||
{
|
||||
name: name.trim(),
|
||||
state: state || null,
|
||||
version: Number.isNaN(Number.parseInt(version, 10)) ? null : Number.parseInt(version, 10),
|
||||
isDefault: marker === "*",
|
||||
} satisfies WslInstalledDistro,
|
||||
|
||||
@@ -33,8 +33,6 @@ const api: ElectronAPI = {
|
||||
addServer: (distro) => ipcRenderer.invoke("wsl-servers-add", distro),
|
||||
removeServer: (id) => ipcRenderer.invoke("wsl-servers-remove", id),
|
||||
startServer: (id) => ipcRenderer.invoke("wsl-servers-start", id),
|
||||
stopServer: (id) => ipcRenderer.invoke("wsl-servers-stop", id),
|
||||
cancelJob: () => ipcRenderer.invoke("wsl-servers-cancel"),
|
||||
},
|
||||
getWindowConfig: () => ipcRenderer.invoke("get-window-config"),
|
||||
consumeInitialDeepLinks: () => ipcRenderer.invoke("consume-initial-deep-links"),
|
||||
|
||||
@@ -4,12 +4,6 @@ export type ServerReadyData = {
|
||||
url: string
|
||||
username: string | null
|
||||
password: string | null
|
||||
local: {
|
||||
key: string
|
||||
url: string
|
||||
username: string | null
|
||||
password: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" }
|
||||
@@ -17,12 +11,10 @@ export type SqliteMigrationProgress = { type: "InProgress"; value: number } | {
|
||||
export type WslRuntimeCheck = {
|
||||
available: boolean
|
||||
version: string | null
|
||||
status: string | null
|
||||
error: string | null
|
||||
}
|
||||
export type WslInstalledDistro = {
|
||||
name: string
|
||||
state: string | null
|
||||
version: number | null
|
||||
isDefault: boolean
|
||||
}
|
||||
@@ -35,7 +27,6 @@ export type WslDistroProbe = {
|
||||
canExecute: boolean
|
||||
hasBash: boolean
|
||||
hasCurl: boolean
|
||||
username: string | null
|
||||
isRoot: boolean | null
|
||||
error: string | null
|
||||
}
|
||||
@@ -98,8 +89,6 @@ export type WslServersAPI = {
|
||||
addServer: (distro: string) => Promise<WslServerConfig>
|
||||
removeServer: (id: string) => Promise<void>
|
||||
startServer: (id: string) => Promise<void>
|
||||
stopServer: (id: string) => Promise<void>
|
||||
cancelJob: () => Promise<void>
|
||||
}
|
||||
|
||||
export type LinuxDisplayBackend = "wayland" | "auto"
|
||||
|
||||
@@ -134,13 +134,13 @@ const createPlatform = (): Platform => {
|
||||
const wslHome = async () => {
|
||||
const distro = activeWslDistro()
|
||||
if (!distro) return undefined
|
||||
return window.api.wslPath("~", "windows", distro).catch(() => undefined)
|
||||
return window.api.wslPath("~", "windows", distro)
|
||||
}
|
||||
|
||||
const handleWslPicker = async <T extends string | string[] | null>(result: T): Promise<T> => {
|
||||
const distro = activeWslDistro()
|
||||
if (!result || !distro) return result
|
||||
const convert = (path: string) => window.api.wslPath(path, "linux", distro).catch(() => path)
|
||||
const convert = (path: string) => window.api.wslPath(path, "linux", distro)
|
||||
if (Array.isArray(result)) {
|
||||
return (await Promise.all(result.map(convert))) as T
|
||||
}
|
||||
@@ -217,10 +217,7 @@ const createPlatform = (): Platform => {
|
||||
const resolvedApp = app ? await window.api.resolveAppPath(app).catch(() => null) : null
|
||||
const resolvedPath = await (async () => {
|
||||
const distro = activeWslDistro()
|
||||
if (distro) {
|
||||
const converted = await window.api.wslPath(path, "windows", distro).catch(() => null)
|
||||
if (converted) return converted
|
||||
}
|
||||
if (distro) return window.api.wslPath(path, "windows", distro)
|
||||
return path
|
||||
})()
|
||||
return window.api.openPath(resolvedPath, resolvedApp ?? undefined)
|
||||
@@ -404,9 +401,9 @@ render(() => {
|
||||
type: "sidecar",
|
||||
variant: "base",
|
||||
http: {
|
||||
url: data.local.url,
|
||||
username: data.local.username ?? undefined,
|
||||
password: data.local.password ?? undefined,
|
||||
url: data.url,
|
||||
username: data.username ?? undefined,
|
||||
password: data.password ?? undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -434,7 +431,7 @@ render(() => {
|
||||
|
||||
return (
|
||||
<AppInterface
|
||||
defaultServer={defaultServer.latest ?? ServerConnection.Key.make(startup.latest?.sidecar?.local.key ?? "local:windows")}
|
||||
defaultServer={defaultServer.latest ?? ServerConnection.Key.make("local:windows")}
|
||||
serversReady={!platform.wslServers || !wslServers.isPending}
|
||||
servers={servers()}
|
||||
router={MemoryRouter}
|
||||
|
||||
Reference in New Issue
Block a user