feat: add local server transcript and terminal action

This commit is contained in:
LukeParkerDev
2026-04-16 14:30:25 +10:00
parent ec1a5c261e
commit c0e449d3ac
6 changed files with 70 additions and 5 deletions

View File

@@ -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),

View File

@@ -27,6 +27,7 @@ type Deps = {
setLocalServerConfig: (config: LocalServerConfig) => Promise<void> | void
runLocalServerStep: (step: LocalServerStep) => Promise<void> | void
cancelLocalServerJob: () => Promise<void> | void
openLocalServerTerminal: () => Promise<void> | void
onLocalServerEvent: (listener: (event: LocalServerEvent) => void) => () => void
getDefaultServerUrl: () => Promise<string | null> | string | null
setDefaultServerUrl: (url: string | null) => Promise<void> | 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),

View File

@@ -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<LocalServerTranscriptLine, "at">) => {
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 ?? [],
}
}

View File

@@ -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<void>((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()

View File

@@ -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)

View File

@@ -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<void>
runStep: (step: LocalServerStep) => Promise<void>
cancelJob: () => Promise<void>
openTerminal: () => Promise<void>
subscribe: (cb: (event: LocalServerEvent) => void) => () => void
}