From c0e449d3ac2bd9755e6bb004781ea3faf145cdf8 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:30:25 +1000 Subject: [PATCH] feat: add local server transcript and terminal action --- packages/desktop-electron/src/main/index.ts | 1 + packages/desktop-electron/src/main/ipc.ts | 6 +++ .../desktop-electron/src/main/local-server.ts | 45 ++++++++++++++++--- packages/desktop-electron/src/main/wsl.ts | 15 +++++++ .../desktop-electron/src/preload/index.ts | 1 + .../desktop-electron/src/preload/types.ts | 7 +++ 6 files changed, 70 insertions(+), 5 deletions(-) diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index 53675dcebc..aa2be07e6f 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -255,6 +255,7 @@ registerIpcHandlers({ setLocalServerConfig: (config) => localServer.setConfig(config), runLocalServerStep: (step) => localServer.runStep(step), cancelLocalServerJob: () => localServer.cancelJob(), + openLocalServerTerminal: () => localServer.openTerminal(), onLocalServerEvent: (listener) => localServer.subscribe(listener), getDefaultServerUrl: () => getDefaultServerUrl(), setDefaultServerUrl: (url) => setDefaultServerUrl(url), diff --git a/packages/desktop-electron/src/main/ipc.ts b/packages/desktop-electron/src/main/ipc.ts index e8642bacfc..7021921600 100644 --- a/packages/desktop-electron/src/main/ipc.ts +++ b/packages/desktop-electron/src/main/ipc.ts @@ -27,6 +27,7 @@ type Deps = { setLocalServerConfig: (config: LocalServerConfig) => Promise | void runLocalServerStep: (step: LocalServerStep) => Promise | void cancelLocalServerJob: () => Promise | void + openLocalServerTerminal: () => Promise | void onLocalServerEvent: (listener: (event: LocalServerEvent) => void) => () => void getDefaultServerUrl: () => Promise | string | null setDefaultServerUrl: (url: string | null) => Promise | void @@ -61,6 +62,11 @@ export function registerIpcHandlers(deps: Deps) { ipcMain.handle("local-server-set-config", (_event: IpcMainInvokeEvent, config: LocalServerConfig) => deps.setLocalServerConfig(config), ) + ipcMain.handle("local-server-run-step", (_event: IpcMainInvokeEvent, step: LocalServerStep) => + deps.runLocalServerStep(step), + ) + ipcMain.handle("local-server-cancel-job", () => deps.cancelLocalServerJob()) + ipcMain.handle("local-server-open-terminal", () => deps.openLocalServerTerminal()) ipcMain.handle("get-default-server-url", () => deps.getDefaultServerUrl()) ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) => deps.setDefaultServerUrl(url), diff --git a/packages/desktop-electron/src/main/local-server.ts b/packages/desktop-electron/src/main/local-server.ts index e16b881d7a..48e9c44378 100644 --- a/packages/desktop-electron/src/main/local-server.ts +++ b/packages/desktop-electron/src/main/local-server.ts @@ -4,10 +4,11 @@ import type { LocalServerEvent, LocalServerState, LocalServerStep, + LocalServerTranscriptLine, } from "../preload/types" import { LOCAL_SERVER_KEY } from "./constants" import { store } from "./store" -import { listInstalledWslDistros, listOnlineWslDistros, probeWslDistro, probeWslRuntime } from "./wsl" +import { listInstalledWslDistros, listOnlineWslDistros, openWslTerminal, probeWslDistro, probeWslRuntime } from "./wsl" export function defaultLocalServerConfig(): LocalServerConfig { return { @@ -39,6 +40,20 @@ export function createLocalServerController() { emit({ type: "state", state }) } + const appendTranscript = (line: Omit) => { + update({ + ...state, + transcript: [...state.transcript, { ...line, at: Date.now() }], + }) + } + + const clearTranscript = () => { + update({ + ...state, + transcript: [], + }) + } + return { getState() { return state @@ -59,6 +74,8 @@ export function createLocalServerController() { jobAbort?.abort() const abort = new AbortController() jobAbort = abort + clearTranscript() + appendTranscript({ stream: "system", text: `Running local server step: ${step}` }) update({ ...state, job: { step, startedAt: Date.now() }, @@ -67,7 +84,10 @@ export function createLocalServerController() { try { if (step === "wsl") { - const wsl = await probeWslRuntime({ signal: abort.signal }) + const wsl = await probeWslRuntime({ + signal: abort.signal, + onLine: (line) => appendTranscript(line), + }) if (jobAbort !== abort) return update({ ...state, @@ -85,15 +105,24 @@ export function createLocalServerController() { if (step === "distro") { const [installedResult, onlineResult] = await Promise.allSettled([ - listInstalledWslDistros({ signal: abort.signal }), - listOnlineWslDistros({ signal: abort.signal }), + listInstalledWslDistros({ + signal: abort.signal, + onLine: (line) => appendTranscript(line), + }), + listOnlineWslDistros({ + signal: abort.signal, + onLine: (line) => appendTranscript(line), + }), ]) if (jobAbort !== abort) return const installed = installedResult.status === "fulfilled" ? installedResult.value : [] const online = onlineResult.status === "fulfilled" ? onlineResult.value : [] const selected = state.config.distro - ? await probeWslDistro(state.config.distro, { signal: abort.signal }) + ? await probeWslDistro(state.config.distro, { + signal: abort.signal, + onLine: (line) => appendTranscript(line), + }) : null if (jobAbort !== abort) return @@ -144,12 +173,17 @@ export function createLocalServerController() { cancelJob() { jobAbort?.abort() jobAbort = undefined + appendTranscript({ stream: "system", text: "Canceled local server job" }) update({ ...state, job: null, status: { kind: "idle" }, }) }, + async openTerminal() { + if (!state.config.distro) throw new Error("No WSL distro selected") + await openWslTerminal(state.config.distro) + }, setRuntime(runtime: LocalServerState["runtime"]) { update({ ...state, @@ -176,6 +210,7 @@ function toState(config: LocalServerConfig, current?: LocalServerState): LocalSe status: current?.status ?? { kind: "idle" }, job: current?.job ?? null, checks: current?.checks ?? { wsl: null, distro: null }, + transcript: current?.transcript ?? [], } } diff --git a/packages/desktop-electron/src/main/wsl.ts b/packages/desktop-electron/src/main/wsl.ts index e11918d188..4ec38b9201 100644 --- a/packages/desktop-electron/src/main/wsl.ts +++ b/packages/desktop-electron/src/main/wsl.ts @@ -191,6 +191,21 @@ export async function upgradeWslOpencode(target: string, distro: string, opts?: return runWslBash(`opencode upgrade ${shellEscape(target)}`, distro, opts) } +export function openWslTerminal(distro?: string | null) { + return new Promise((resolve, reject) => { + const child = spawn("cmd.exe", ["/c", "start", "", "wsl", ...(distro ? ["-d", distro] : [])], { + detached: true, + stdio: "ignore", + windowsHide: true, + }) + child.once("error", reject) + child.once("spawn", () => { + child.unref() + resolve() + }) + }) +} + function parseInstalledDistros(output: string) { return output.split(/\r?\n/g).flatMap((line) => { const trimmed = line.trim() diff --git a/packages/desktop-electron/src/preload/index.ts b/packages/desktop-electron/src/preload/index.ts index c9dcc9a062..321d0fabeb 100644 --- a/packages/desktop-electron/src/preload/index.ts +++ b/packages/desktop-electron/src/preload/index.ts @@ -16,6 +16,7 @@ const api: ElectronAPI = { setConfig: (config) => ipcRenderer.invoke("local-server-set-config", config), runStep: (step) => ipcRenderer.invoke("local-server-run-step", step), cancelJob: () => ipcRenderer.invoke("local-server-cancel-job"), + openTerminal: () => ipcRenderer.invoke("local-server-open-terminal"), subscribe: (cb) => { const handler = (_: unknown, event: LocalServerEvent) => cb(event) ipcRenderer.on("local-server-event", handler) diff --git a/packages/desktop-electron/src/preload/types.ts b/packages/desktop-electron/src/preload/types.ts index 688fc6824d..81a9de3cb2 100644 --- a/packages/desktop-electron/src/preload/types.ts +++ b/packages/desktop-electron/src/preload/types.ts @@ -45,6 +45,11 @@ export type LocalServerDistroCheck = { selected: LocalServerDistroProbe | null error: string | null } +export type LocalServerTranscriptLine = { + stream: "stdout" | "stderr" | "system" + text: string + at: number +} export type LocalServerConfig = { mode: LocalServerMode distro: string | null @@ -76,6 +81,7 @@ export type LocalServerState = { wsl: LocalServerWslCheck | null distro: LocalServerDistroCheck | null } + transcript: LocalServerTranscriptLine[] } export type LocalServerEvent = { type: "state" @@ -86,6 +92,7 @@ export type LocalServerAPI = { setConfig: (config: LocalServerConfig) => Promise runStep: (step: LocalServerStep) => Promise cancelJob: () => Promise + openTerminal: () => Promise subscribe: (cb: (event: LocalServerEvent) => void) => () => void }