diff --git a/.changeset/background-process-ports.md b/.changeset/background-process-ports.md new file mode 100644 index 0000000000..dfaaddcc67 --- /dev/null +++ b/.changeset/background-process-ports.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Show detected ports for tracked background processes in the TUI sidebar and process detail dialog. diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index b22270458d..a5c08d899c 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -654,6 +654,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ if (!next[item.sessionID]) next[item.sessionID] = [] next[item.sessionID].push(item) } + for (const list of Object.values(next)) list.sort((a, b) => a.id.localeCompare(b.id)) setStore("background_process", reconcile(next)) }), // kilocode_change end diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 5103acb00a..bd09dd36cb 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -276,18 +276,36 @@ export function Session() { const editor = useEditorContext() // kilocode_change start - background processes are scoped to the visible session + function processGroup(sessionID: string) { + const info = sync.session.get(sessionID) + return info?.parentID ?? info?.id ?? sessionID + } + + function processSessions(sessionID: string) { + const group = processGroup(sessionID) + const ids = new Set([sessionID, group]) + for (const item of sync.data.session) { + if (item.id === group || item.parentID === group) ids.add(item.id) + } + return Array.from(ids) + } + function stopProcesses(sessionID: string) { - void sdk.client.backgroundProcess.stopSession({ sessionID }).catch((err) => { - Log.Default.warn("failed to stop session background processes", { sessionID, err }) - }) + for (const id of processSessions(sessionID)) { + void sdk.client.backgroundProcess.stopSession({ sessionID: id }).catch((err) => { + Log.Default.warn("failed to stop session background processes", { sessionID: id, err }) + }) + } } let processSessionID = route.sessionID createEffect(() => { const next = route.sessionID if (processSessionID === next) return - stopProcesses(processSessionID) + const prev = processSessionID processSessionID = next + if (processGroup(prev) === processGroup(next)) return + stopProcesses(prev) }) onCleanup(() => { stopProcesses(processSessionID) diff --git a/packages/opencode/src/kilocode/background-process/index.ts b/packages/opencode/src/kilocode/background-process/index.ts index b1e31029b0..77ef3cfc34 100644 --- a/packages/opencode/src/kilocode/background-process/index.ts +++ b/packages/opencode/src/kilocode/background-process/index.ts @@ -14,6 +14,7 @@ import { Context, Effect, Layer, Schema, Types } from "effect" import net from "net" import path from "path" import z from "zod" +import * as Ports from "./ports" export namespace BackgroundProcess { const log = Log.create({ service: "background-process" }) @@ -21,6 +22,7 @@ export namespace BackgroundProcess { const KILL_MS = 3_000 const READY_MS = 30_000 const PUBLISH_MS = 500 + const PORT_MS = 2_000 const idSchema = Schema.String.annotate({ [ZodOverride]: z.string().startsWith("bgp") }).pipe( Schema.brand("BackgroundProcessID"), @@ -61,6 +63,7 @@ export namespace BackgroundProcess { command: Schema.String, cwd: Schema.String, description: optionalOmitUndefined(Schema.String), + ports: Schema.mutable(Schema.Array(PositiveInt)), status: Status, ready: Schema.Boolean, exitCode: optionalOmitUndefined(Schema.NullOr(NonNegativeInt)), @@ -122,6 +125,9 @@ export namespace BackgroundProcess { pattern?: RegExp resolve?: (ready: boolean) => void notify?: ReturnType + poll?: ReturnType + scan?: Promise + disposed?: boolean } type State = { @@ -137,6 +143,7 @@ export namespace BackgroundProcess { function clone(info: Info): Info { return { ...info, + ports: [...info.ports], time: { ...info.time }, } } @@ -153,7 +160,26 @@ export namespace BackgroundProcess { return buf.subarray(start).toString("utf-8") } - function publish(active: Active) { + function same(a: number[], b: number[]) { + return a.length === b.length && a.every((port, index) => port === b[index]) + } + + async function refresh(active: Active) { + const pid = active.proc.pid + if (!pid || terminal(active.info.status)) { + const changed = active.info.ports.length > 0 + active.info.ports = [] + return changed + } + const fallback = active.info.ready && active.start.ready?.port ? [active.start.ready.port] : [] + const next = Array.from(new Set([...(await Ports.list(pid)), ...fallback])).toSorted((a, b) => a - b) + if (same(active.info.ports, next)) return false + active.info.ports = next + active.info.time.updated = Date.now() + return true + } + + function emit(active: Active) { Instance.restore(active.ctx, () => { void Bus.publish(Event.Updated, { info: clone(active.info) }).catch((err) => { log.warn("failed to publish process update", { err, id: active.info.id }) @@ -161,7 +187,50 @@ export namespace BackgroundProcess { }) } + function publish(active: Active) { + if (active.disposed) return + active.scan = (active.scan ?? refresh(active)) + .then(() => { + active.scan = undefined + emit(active) + poll(active) + return false + }) + .catch((err) => { + active.scan = undefined + log.debug("failed to refresh process ports", { err, id: active.info.id }) + emit(active) + poll(active) + return false + }) + } + + function poll(active: Active) { + if (active.disposed) return + if (terminal(active.info.status)) return + if (active.poll) return + active.poll = setTimeout(() => { + active.poll = undefined + if (active.disposed) return + if (terminal(active.info.status)) return + active.scan = (active.scan ?? refresh(active)) + .then((changed) => { + active.scan = undefined + if (changed) emit(active) + poll(active) + return changed + }) + .catch((err) => { + active.scan = undefined + log.debug("failed to refresh process ports", { err, id: active.info.id }) + poll(active) + return false + }) + }, PORT_MS) + } + function schedule(active: Active) { + if (active.disposed) return if (active.notify) return active.notify = setTimeout(() => { active.notify = undefined @@ -170,6 +239,7 @@ export namespace BackgroundProcess { } function ready(active: Active) { + if (active.disposed) return if (active.info.ready) return active.info.ready = true active.info.status = "ready" @@ -180,6 +250,7 @@ export namespace BackgroundProcess { } function append(active: Active, chunk: string) { + if (active.disposed) return active.info.output = clamp(active.info.output + chunk) active.info.time.updated = Date.now() if (active.pattern?.test(active.info.output)) ready(active) @@ -187,11 +258,15 @@ export namespace BackgroundProcess { } function exited(active: Active, code: number | null, signal: NodeJS.Signals | null) { + if (active.disposed) return if (terminal(active.info.status)) return if (active.notify) clearTimeout(active.notify) + if (active.poll) clearTimeout(active.poll) active.notify = undefined + active.poll = undefined active.info.exitCode = code active.info.signal = signal + active.info.ports = [] active.info.ready = active.info.ready && code === 0 active.info.status = active.info.status === "stopping" ? "stopped" : code === 0 ? "exited" : "failed" active.info.time.updated = Date.now() @@ -202,10 +277,20 @@ export namespace BackgroundProcess { } function failed(active: Active, err: unknown) { + if (active.disposed) return append(active, `\n${err instanceof Error ? err.message : String(err)}\n`) exited(active, 1, null) } + function pattern(input?: string) { + if (!input) return + try { + return new RegExp(input) + } catch (err) { + throw new Error(`Invalid ready pattern: ${err instanceof Error ? err.message : String(err)}`) + } + } + function connected(port: number) { return new Promise((resolve) => { const socket = net.createConnection({ port, host: "127.0.0.1" }) @@ -325,8 +410,10 @@ export namespace BackgroundProcess { if (!terminal(active.info.status)) exited(active, active.proc.exitCode, active.proc.signalCode) } if (!opts?.remove) return + active.disposed = true state.processes.delete(active.info.id) if (active.notify) clearTimeout(active.notify) + if (active.poll) clearTimeout(active.poll) active.resolve?.(false) active.resolve = undefined if (opts.silent) return @@ -340,7 +427,7 @@ export namespace BackgroundProcess { async function launch(state: State, input: StartInput, id = ID.ascending()) { const sh = Shell.acceptable() const cwd = path.resolve(state.dir, input.cwd ?? state.dir) - const pattern = input.ready?.pattern ? new RegExp(input.ready.pattern) : undefined + const readyPattern = pattern(input.ready?.pattern) const args = Shell.args(sh, input.command, cwd) const proc = spawn(sh, args, { cwd, @@ -359,6 +446,7 @@ export namespace BackgroundProcess { command: input.command, cwd, description: input.description, + ports: [], status: input.ready ? "starting" : "running", ready: false, output: "", @@ -369,7 +457,7 @@ export namespace BackgroundProcess { }, proc, start: { ...input, cwd }, - pattern, + pattern: readyPattern, } state.processes.set(id, active) proc.stdout?.on("data", (chunk) => append(active, chunk.toString("utf-8"))) @@ -393,7 +481,9 @@ export namespace BackgroundProcess { yield* Effect.addFinalizer(() => Effect.promise(async () => { await Promise.all( - Array.from(state.processes.values()).map((active) => terminate(state, active, { silent: true })), + Array.from(state.processes.values()).map((active) => + terminate(state, active, { remove: true, silent: true }), + ), ) state.processes.clear() }), diff --git a/packages/opencode/src/kilocode/background-process/ports.ts b/packages/opencode/src/kilocode/background-process/ports.ts new file mode 100644 index 0000000000..7afa14e11e --- /dev/null +++ b/packages/opencode/src/kilocode/background-process/ports.ts @@ -0,0 +1,138 @@ +import { Process } from "@/util/process" +import fs from "fs/promises" +import path from "path" + +function sorted(items: Iterable) { + return Array.from(items).toSorted((a, b) => a - b) +} + +function parse(addr: string) { + const raw = addr.split(":").at(-1) + if (!raw) return + const port = Number.parseInt(raw, 16) + if (!Number.isFinite(port) || port <= 0) return + return port +} + +async function ppid(pid: number) { + const text = await fs.readFile(`/proc/${pid}/stat`, "utf8") + const match = text.match(/^\d+ \(.+\) \S+ (\d+)/) + if (!match) return + return Number(match[1]) +} + +async function tree(root: number) { + const names = await fs.readdir("/proc") + const rows = await Promise.all( + names + .filter((name) => /^\d+$/.test(name)) + .map(async (name) => { + const pid = Number(name) + const parent = await ppid(pid).catch(() => undefined) + if (!parent) return + return { pid, parent } + }), + ) + const children = new Map() + for (const row of rows) { + if (!row) continue + children.set(row.parent, [...(children.get(row.parent) ?? []), row.pid]) + } + const result = new Set([root]) + const stack = [root] + while (stack.length > 0) { + const pid = stack.pop() + if (!pid) continue + for (const child of children.get(pid) ?? []) { + if (result.has(child)) continue + result.add(child) + stack.push(child) + } + } + return result +} + +async function sockets(pids: Set) { + const all = await Promise.all( + Array.from(pids).map(async (pid) => { + const dir = `/proc/${pid}/fd` + const files = await fs.readdir(dir).catch(() => []) + const links = await Promise.allSettled(files.map((file) => fs.readlink(path.join(dir, file)))) + return links.flatMap((item) => { + if (item.status !== "fulfilled") return [] + const match = item.value.match(/^socket:\[(\d+)\]$/) + return match ? [match[1]] : [] + }) + }), + ) + return new Set(all.flat()) +} + +async function file(name: string, inodes: Set) { + const text = await fs.readFile(name, "utf8").catch(() => "") + return text + .trim() + .split(/\r?\n/) + .slice(1) + .flatMap((line) => { + const parts = line.trim().split(/\s+/) + if (parts[3] !== "0A") return [] + if (!inodes.has(parts[9])) return [] + const port = parse(parts[1]) + return port ? [port] : [] + }) +} + +async function linux(root: number) { + const pids = await tree(root) + const found = await sockets(pids) + if (found.size === 0) return [] + const ports = await Promise.all([file("/proc/net/tcp", found), file("/proc/net/tcp6", found)]) + return sorted(new Set(ports.flat())) +} + +async function ps(root: number) { + const rows = await Process.lines(["ps", "-axo", "pid=,ppid="], { nothrow: true }) + const children = new Map() + for (const row of rows) { + const [pid, parent] = row.trim().split(/\s+/).map(Number) + if (!pid || !parent) continue + children.set(parent, [...(children.get(parent) ?? []), pid]) + } + const result = new Set([root]) + const stack = [root] + while (stack.length > 0) { + const pid = stack.pop() + if (!pid) continue + for (const child of children.get(pid) ?? []) { + if (result.has(child)) continue + result.add(child) + stack.push(child) + } + } + return result +} + +async function lsof(root: number) { + const pids = await ps(root).catch(() => new Set([root])) + const rows = await Process.lines(["lsof", "-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", Array.from(pids).join(",")], { + nothrow: true, + }) + return sorted( + new Set( + rows.flatMap((row) => { + const match = row.match(/:(\d+)\s+\(LISTEN\)$/) + return match ? [Number(match[1])] : [] + }), + ), + ) +} + +export async function list(root: number) { + if (process.platform === "linux") { + const ports = await linux(root).catch(() => []) + if (ports.length > 0) return ports + } + if (process.platform === "win32") return [] + return lsof(root).catch(() => []) +} diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/component/dialog-process-list.tsx b/packages/opencode/src/kilocode/cli/cmd/tui/component/dialog-process-list.tsx index 568d6094b7..4984e0ca7b 100644 --- a/packages/opencode/src/kilocode/cli/cmd/tui/component/dialog-process-list.tsx +++ b/packages/opencode/src/kilocode/cli/cmd/tui/component/dialog-process-list.tsx @@ -23,7 +23,7 @@ type Scope = "session" | "all" type Kind = "stop" | "restart" type Theme = ReturnType["theme"] -const stopKey = Keybind.parse("ctrl+x")[0] +const stopKey = Keybind.parse("ctrl+o")[0] const restartKey = Keybind.parse("ctrl+r")[0] const allKey = Keybind.parse("ctrl+a")[0] @@ -49,14 +49,14 @@ function label(item: Info) { return item.description?.trim() || item.command } -function last(item: Info) { - return stripAnsi(item.output).trim().split(/\r?\n/).filter(Boolean).at(-1) ?? "" -} - function short(text: string, max = 64) { return Locale.truncate(text, max) } +function ports(item: Info) { + return item.ports.length > 0 ? item.ports.join(", ") : "none" +} + function useActions() { const sdk = useSDK() const toast = useToast() @@ -142,21 +142,14 @@ export function DialogProcessList() { const options = createMemo[]>(() => { const busy = actions.busy() return list().map((item) => { - const title = label(item) - const tail = last(item) const note = mode() === "all" ? session(sync, item.sessionID) : undefined - const footer = - busy?.id === item.id - ? `${busy.kind === "stop" ? "stopping" : "restarting"}...` - : `${item.status}${item.pid ? ` pid:${item.pid}` : ""}` - const command = note ? `(${short(note, 28)}) ${short(item.command, 40)}` : short(item.command, 58) + const footer = busy?.id === item.id ? `${busy.kind === "stop" ? "stopping" : "restarting"}...` : item.pid?.toString() + const title = `${note ? `(${note}) ` : ""}${label(item)} - ${item.command}` return { - title: short(title, 61), + title: short(title, 92), value: item.id, - description: tail ? `${command} > ${short(tail, 32)}` : command, footer, - category: item.status, gutter: () => , } }) @@ -239,7 +232,7 @@ function DialogProcessDetail(props: { id: string; back: () => void }) { props.back() return } - if (keybind.match("ctrl+x", evt)) { + if (keybind.match("ctrl+o", evt)) { evt.preventDefault() evt.stopPropagation() if (proc) void actions.run("stop", proc) @@ -278,23 +271,31 @@ function DialogProcessDetail(props: { id: string; back: () => void }) { {(proc) => ( <> - - status{" "} - {proc().status} - {proc().pid ? ` pid ${proc().pid}` : ""} - {proc().exitCode !== undefined ? ` exit ${proc().exitCode}` : ""} - {proc().signal ? ` signal ${proc().signal}` : ""} + + Name: {label(proc())} - started {Locale.datetime(proc().time.started)} - updated {Locale.datetime(proc().time.updated)} + + Status:{" "} + {proc().status} + + PID: {proc().pid ?? "none"} + Ports: {ports(proc())} + + Exit: {proc().exitCode} + + + {(signal) => Signal: {signal()}} + + Started: {Locale.datetime(proc().time.started)} + Updated: {Locale.datetime(proc().time.updated)} - {(ended) => ended {Locale.datetime(ended())}} + {(ended) => Ended: {Locale.datetime(ended())}} - cwd {proc().cwd} + CWD: {proc().cwd} - command {proc().command} + Command: {proc().command} diff --git a/packages/opencode/src/kilocode/plugins/sidebar-background-processes.tsx b/packages/opencode/src/kilocode/plugins/sidebar-background-processes.tsx index 87fe1e1c41..ddd8a3e9ff 100644 --- a/packages/opencode/src/kilocode/plugins/sidebar-background-processes.tsx +++ b/packages/opencode/src/kilocode/plugins/sidebar-background-processes.tsx @@ -8,10 +8,6 @@ function short(text: string, max = 34) { return text.slice(0, max - 3) + "..." } -function last(text: string) { - return text.trim().split(/\r?\n/).filter(Boolean).at(-1) ?? "" -} - function tone(item: TuiSidebarBackgroundProcessItem, api: TuiPluginApi) { const theme = api.theme.current if (item.status === "ready" || item.status === "running") return theme.success @@ -21,7 +17,7 @@ function tone(item: TuiSidebarBackgroundProcessItem, api: TuiPluginApi) { } function label(item: TuiSidebarBackgroundProcessItem) { - return item.description || item.command + return item.description?.trim() || item.command } function View(props: { api: TuiPluginApi; session_id: string }) { @@ -47,13 +43,12 @@ function View(props: { api: TuiPluginApi; session_id: string }) { {short(label(item))} - - {item.status} - {item.pid ? ` pid:${item.pid}` : ""} - {short(item.command)} - - {"> " + short(last(item.output))} + + {(pid) => PID: {pid()}} + + 0}> + PORTS: {item.ports.join(", ")} )} diff --git a/packages/opencode/src/kilocode/server/httpapi/groups/background-process.ts b/packages/opencode/src/kilocode/server/httpapi/groups/background-process.ts index 227220ab0f..28e2be8a52 100644 --- a/packages/opencode/src/kilocode/server/httpapi/groups/background-process.ts +++ b/packages/opencode/src/kilocode/server/httpapi/groups/background-process.ts @@ -11,7 +11,6 @@ const root = "/background-process" export const BackgroundProcessPaths = { list: root, - create: root, get: `${root}/:processID`, logs: `${root}/:processID/logs`, stop: `${root}/:processID/stop`, @@ -35,17 +34,6 @@ export const BackgroundProcessApi = HttpApi.make("background-process") description: "List tracked background processes for the current instance.", }), ), - HttpApiEndpoint.post("create", BackgroundProcessPaths.create, { - payload: BackgroundProcess.StartInput, - success: described(BackgroundProcess.Info, "Created background process"), - error: HttpApiError.BadRequest, - }).annotateMerge( - OpenApi.annotations({ - identifier: "backgroundProcess.create", - summary: "Create background process", - description: "Start a tracked background process.", - }), - ), HttpApiEndpoint.get("get", BackgroundProcessPaths.get, { params: { processID: BackgroundProcess.ID }, success: described(BackgroundProcess.Info, "Background process info"), diff --git a/packages/opencode/src/kilocode/server/httpapi/handlers/background-process.ts b/packages/opencode/src/kilocode/server/httpapi/handlers/background-process.ts index 2782dcd201..5c4ab90e2f 100644 --- a/packages/opencode/src/kilocode/server/httpapi/handlers/background-process.ts +++ b/packages/opencode/src/kilocode/server/httpapi/handlers/background-process.ts @@ -12,12 +12,6 @@ export const backgroundProcessHandlers = HttpApiBuilder.group(InstanceHttpApi, " return yield* Effect.promise(() => BackgroundProcess.list()) }) - const create = Effect.fn("BackgroundProcessHttpApi.create")(function* (ctx: { - payload: BackgroundProcess.StartInput - }) { - return yield* Effect.promise(() => BackgroundProcess.start(ctx.payload)) - }) - const get = Effect.fn("BackgroundProcessHttpApi.get")(function* (ctx: { params: { processID: BackgroundProcess.ID } }) { @@ -59,7 +53,6 @@ export const backgroundProcessHandlers = HttpApiBuilder.group(InstanceHttpApi, " return handlers .handle("list", list) - .handle("create", create) .handle("get", get) .handle("logs", logs) .handle("stop", stop) diff --git a/packages/opencode/src/kilocode/server/httpapi/instance.ts b/packages/opencode/src/kilocode/server/httpapi/instance.ts index b3697b0024..2e823ed1e4 100644 --- a/packages/opencode/src/kilocode/server/httpapi/instance.ts +++ b/packages/opencode/src/kilocode/server/httpapi/instance.ts @@ -14,7 +14,6 @@ type Handler = (request: Request, context: Context.Context) => Promise< export function register(app: Hono, handler: Handler, context: Context.Context) { app.get(BackgroundProcessPaths.list, (c) => handler(c.req.raw, context)) - app.post(BackgroundProcessPaths.create, (c) => handler(c.req.raw, context)) app.get(BackgroundProcessPaths.get, (c) => handler(c.req.raw, context)) app.get(BackgroundProcessPaths.logs, (c) => handler(c.req.raw, context)) app.post(BackgroundProcessPaths.stop, (c) => handler(c.req.raw, context)) diff --git a/packages/opencode/src/kilocode/server/routes/background-process.ts b/packages/opencode/src/kilocode/server/routes/background-process.ts index 445c2f749e..081bc7f1ca 100644 --- a/packages/opencode/src/kilocode/server/routes/background-process.ts +++ b/packages/opencode/src/kilocode/server/routes/background-process.ts @@ -24,23 +24,6 @@ export const BackgroundProcessRoutes = lazy(() => }), async (c) => c.json(await BackgroundProcess.list()), ) - .post( - "/", - describeRoute({ - summary: "Create background process", - description: "Start a tracked background process.", - operationId: "backgroundProcess.create", - responses: { - 200: { - description: "Created background process", - content: { "application/json": { schema: resolver(BackgroundProcess.Info.zod) } }, - }, - ...errors(400), - }, - }), - validator("json", BackgroundProcess.StartInput.zod), - async (c) => c.json(await BackgroundProcess.start(c.req.valid("json") as BackgroundProcess.StartInput)), - ) .get( "/:processID", describeRoute({ diff --git a/packages/opencode/src/kilocode/tool/background-process.ts b/packages/opencode/src/kilocode/tool/background-process.ts index a9b41b935c..16c9428252 100644 --- a/packages/opencode/src/kilocode/tool/background-process.ts +++ b/packages/opencode/src/kilocode/tool/background-process.ts @@ -70,6 +70,15 @@ function invalid(action: Action, message: string) { } } +function pattern(ready?: BackgroundProcess.Ready) { + if (!ready?.pattern) return + try { + new RegExp(ready.pattern) + } catch (err) { + return `Invalid ready pattern: ${err instanceof Error ? err.message : String(err)}` + } +} + export const BackgroundProcessTool = Tool.define( "background_process", Effect.succeed({ @@ -121,6 +130,8 @@ export const BackgroundProcessTool = Tool.define 0), diff: opts.state?.session?.diff ?? (() => []), todo: opts.state?.session?.todo ?? (() => []), - processes: opts.state?.session?.processes ?? (() => []), + processes: opts.state?.session?.processes ?? (() => []), // kilocode_change messages: opts.state?.session?.messages ?? (() => []), status: opts.state?.session?.status ?? (() => undefined), permission: opts.state?.session?.permission ?? (() => []), diff --git a/packages/opencode/test/kilocode/background-process.test.ts b/packages/opencode/test/kilocode/background-process.test.ts index 4eef503f55..4dc156945c 100644 --- a/packages/opencode/test/kilocode/background-process.test.ts +++ b/packages/opencode/test/kilocode/background-process.test.ts @@ -89,4 +89,30 @@ describe("BackgroundProcess", () => { } }), ) + + it.instance("rejects invalid readiness patterns before launching", () => + Effect.gen(function* () { + const test = yield* TestInstance + const sessionID = SessionID.descending() + + const err = yield* Effect.promise(async () => { + try { + await BackgroundProcess.start({ + sessionID, + command: "printf 'ready\n'", + cwd: test.directory, + ready: { pattern: "[", timeout: 1_000 }, + }) + } catch (err) { + return err + } + }) + + expect(err).toBeInstanceOf(Error) + expect((err as Error).message).toContain("Invalid ready pattern") + + const list = yield* Effect.promise(() => BackgroundProcess.list({ sessionID })) + expect(list).toEqual([]) + }), + ) }) diff --git a/packages/opencode/test/kilocode/server/httpapi-bridge.test.ts b/packages/opencode/test/kilocode/server/httpapi-bridge.test.ts index 1f8933316e..9dfa4b8e2a 100644 --- a/packages/opencode/test/kilocode/server/httpapi-bridge.test.ts +++ b/packages/opencode/test/kilocode/server/httpapi-bridge.test.ts @@ -81,7 +81,6 @@ describe("Kilo HttpApi bridge", () => { const effect = new Set(openApiRouteKeys(effectOpenApi())) const kilo = [ `GET ${BackgroundProcessPaths.list}`, - `POST ${BackgroundProcessPaths.create}`, "GET /background-process/{processID}", "GET /background-process/{processID}/logs", "POST /background-process/{processID}/stop", @@ -119,6 +118,8 @@ describe("Kilo HttpApi bridge", () => { expect(kilo.filter((route) => !hono.has(route))).toEqual([]) expect(kilo.filter((route) => !effect.has(route))).toEqual([]) + expect(hono.has("POST /background-process")).toBe(false) + expect(effect.has("POST /background-process")).toBe(false) expect(effect.has("GET /indexing/status")).toBe(true) }) diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index 23472e8b9a..13cc904055 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -316,7 +316,7 @@ export type TuiSidebarTodoItem = Pick export type TuiSidebarBackgroundProcessItem = Pick< BackgroundProcessInfo, - "id" | "pid" | "command" | "cwd" | "description" | "status" | "output" + "id" | "pid" | "command" | "cwd" | "description" | "ports" | "status" | "output" > export type TuiSidebarFileItem = { diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 38e5b035ce..94076650f4 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -13,8 +13,6 @@ import type { AuthRemoveResponses, AuthSetErrors, AuthSetResponses, - BackgroundProcessCreateErrors, - BackgroundProcessCreateResponses, BackgroundProcessGetErrors, BackgroundProcessGetResponses, BackgroundProcessListResponses, @@ -22,7 +20,6 @@ import type { BackgroundProcessLogsResponses, BackgroundProcessRestartErrors, BackgroundProcessRestartResponses, - BackgroundProcessStartInput, BackgroundProcessStopErrors, BackgroundProcessStopResponses, BackgroundProcessStopSessionResponses, @@ -5006,47 +5003,6 @@ export class BackgroundProcess extends HeyApiClient { }) } - /** - * Create background process - * - * Start a tracked background process. - */ - public create( - parameters?: { - directory?: string - workspace?: string - backgroundProcessStartInput?: BackgroundProcessStartInput - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { key: "backgroundProcessStartInput", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - BackgroundProcessCreateResponses, - BackgroundProcessCreateErrors, - ThrowOnError - >({ - url: "/background-process", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - /** * Get background process * diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index d695bfe8ff..f54bbec62b 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -269,6 +269,7 @@ export type BackgroundProcessInfo = { command: string cwd: string description?: string + ports: Array status: "starting" | "running" | "ready" | "exited" | "failed" | "stopping" | "stopped" ready: boolean exitCode?: number @@ -1953,23 +1954,6 @@ export type Workspace = { projectID: string } -export type BackgroundProcessReady = { - pattern?: string - port?: number - timeout?: number -} - -export type BackgroundProcessStartInput = { - sessionID: string - /** - * Command to run in the configured shell - */ - command: string - cwd?: string - description?: string - ready?: BackgroundProcessReady -} - export type BackgroundProcessLogs = { id: string sessionID: string @@ -7323,34 +7307,6 @@ export type BackgroundProcessListResponses = { export type BackgroundProcessListResponse = BackgroundProcessListResponses[keyof BackgroundProcessListResponses] -export type BackgroundProcessCreateData = { - body?: BackgroundProcessStartInput - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/background-process" -} - -export type BackgroundProcessCreateErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type BackgroundProcessCreateError = BackgroundProcessCreateErrors[keyof BackgroundProcessCreateErrors] - -export type BackgroundProcessCreateResponses = { - /** - * Created background process - */ - 200: BackgroundProcessInfo -} - -export type BackgroundProcessCreateResponse = BackgroundProcessCreateResponses[keyof BackgroundProcessCreateResponses] - export type BackgroundProcessGetData = { body?: never path: { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 0358b6132d..d92cf37edb 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8995,67 +8995,6 @@ "source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.backgroundProcess.list({\n ...\n})" } ] - }, - "post": { - "tags": ["background-process"], - "operationId": "backgroundProcess.create", - "parameters": [ - { - "name": "directory", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "workspace", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Created background process", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BackgroundProcessInfo" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "description": "Start a tracked background process.", - "summary": "Create background process", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BackgroundProcessStartInput" - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.backgroundProcess.create({\n ...\n})" - } - ] } }, "/background-process/{processID}": { @@ -13246,6 +13185,13 @@ "description": { "type": "string" }, + "ports": { + "type": "array", + "items": { + "type": "integer", + "exclusiveMinimum": 0 + } + }, "status": { "type": "string", "enum": ["starting", "running", "ready", "exited", "failed", "stopping", "stopped"] @@ -13283,7 +13229,7 @@ "additionalProperties": false } }, - "required": ["id", "sessionID", "command", "cwd", "status", "ready", "output", "time"], + "required": ["id", "sessionID", "command", "cwd", "ports", "status", "ready", "output", "time"], "additionalProperties": false }, "SnapshotFileDiff": { @@ -18132,46 +18078,6 @@ "required": ["id", "type", "name", "branch", "directory", "extra", "projectID"], "additionalProperties": false }, - "BackgroundProcessReady": { - "type": "object", - "properties": { - "pattern": { - "type": "string" - }, - "port": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "timeout": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "additionalProperties": false - }, - "BackgroundProcessStartInput": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - }, - "command": { - "type": "string", - "description": "Command to run in the configured shell" - }, - "cwd": { - "type": "string" - }, - "description": { - "type": "string" - }, - "ready": { - "$ref": "#/components/schemas/BackgroundProcessReady" - } - }, - "required": ["sessionID", "command"], - "additionalProperties": false - }, "BackgroundProcessLogs": { "type": "object", "properties": {