diff --git a/.changeset/restore-cli-terminal-startup.md b/.changeset/restore-cli-terminal-startup.md new file mode 100644 index 00000000000..ef0132aeda5 --- /dev/null +++ b/.changeset/restore-cli-terminal-startup.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Prevent the CLI from opening to a blank terminal on startup. diff --git a/packages/core/src/kilocode/pty/smoke.ts b/packages/core/src/kilocode/pty/smoke.ts index fa5a4e01bef..47dee8ebba4 100644 --- a/packages/core/src/kilocode/pty/smoke.ts +++ b/packages/core/src/kilocode/pty/smoke.ts @@ -4,6 +4,56 @@ import { spawn } from "#pty" const TIMEOUT = 15_000 +async function render() { + const proc = spawn(process.execPath, ["--pure"], { + name: "xterm-256color", + cwd: process.cwd(), + env: { + ...process.env, + TERM: "xterm-256color", + KILO_TERMINAL: "1", + KILO_NO_DAEMON: "1", + KILO_DISABLE_AUTOUPDATE: "1", + KILO_DISABLE_MODELS_FETCH: "1", + KILO_DISABLE_PROJECT_CONFIG: "1", + KILO_DISABLE_DEFAULT_PLUGINS: "1", + KILO_DISABLE_TERMINAL_TITLE: "0", + KILO_CONFIG_CONTENT: "{}", + KILO_AUTH_CONTENT: "{}", + } as Record, + cols: 100, + rows: 40, + }) + const state = { output: "", exited: false } + const ready = Promise.withResolvers() + const data = proc.onData((chunk) => { + state.output = (state.output + chunk).slice(-20_000) + if (state.output.includes("Ask anything...")) ready.resolve() + }) + const exit = proc.onExit((event) => { + state.exited = true + ready.reject(new Error(`TUI exited before rendering (code ${event.exitCode}): ${JSON.stringify(state.output)}`)) + }) + const timeout = AbortSignal.timeout(TIMEOUT) + + try { + await Promise.race([ + ready.promise, + new Promise((_, reject) => + timeout.addEventListener( + "abort", + () => reject(new Error(`TUI produced no rendered frame within ${TIMEOUT}ms: ${JSON.stringify(state.output)}`)), + { once: true }, + ), + ), + ]) + } finally { + data.dispose() + exit.dispose() + if (!state.exited) await KiloPtyTermination.terminate(proc) + } +} + export async function smoke() { const proc = spawn(Shell.preferred(), [], { name: "xterm-256color", @@ -17,8 +67,7 @@ export async function smoke() { const exited = Promise.withResolvers() const data = proc.onData((chunk) => { state.output += chunk - const lines = state.output.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").split(/\r?\n/) - if (lines.some((line) => line.trim() === "KILO_PTY_READY")) output.resolve() + if (/(?:^|[\r\n])KILO_PTY_READY(?:\r?\n|$)/.test(state.output)) output.resolve() }) const exit = proc.onExit((event) => { state.exited = true @@ -67,6 +116,8 @@ export async function smoke() { } finally { if (!stopped) active.kill() } + + await render() } export * as PtySmoke from "./smoke" diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 6de30f0c948..b28e2ee1967 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -24,6 +24,7 @@ import { stageBubblewrap } from "./kilocode/bubblewrap" import { LanceDBRuntime } from "../src/kilocode/lancedb" import { KiloSandboxWorker } from "./kilocode/kilo-sandbox-worker" import { KiloSandboxNetwork } from "./kilocode/kilo-sandbox-network" +import { KiloCliSmoke } from "./kilocode/cli-smoke" // kilocode_change end const singleFlag = process.argv.includes("--single") @@ -312,8 +313,9 @@ for (const item of targets) { // kilocode_change end format: "esm", minify: true, - // Bun 1.4 fixes the cross-chunk re-export codegen bug from Bun 1.3.14. - splitting: true, // kilocode_change + // kilocode_change start - keep the compiled OpenTUI/Solid graph in one chunk to avoid blank startup frames. + splitting: false, + // kilocode_change end compile: { autoloadBunfig: false, autoloadDotenv: false, @@ -394,6 +396,9 @@ for (const item of targets) { console.log("Models snapshot smoke test passed") await KiloSandboxWorker.smoke(binaryPath) console.log("Kilo sandbox mutation worker smoke test passed") + console.log(`Running smoke test: ${binaryPath} --pure __pty-smoke`) + await KiloCliSmoke.pty(binaryPath) + console.log("Packaged TUI smoke test passed") // kilocode_change end // kilocode_change start } catch (e) { diff --git a/packages/opencode/script/kilocode/cli-smoke.ts b/packages/opencode/script/kilocode/cli-smoke.ts new file mode 100644 index 00000000000..7fdf834b4cb --- /dev/null +++ b/packages/opencode/script/kilocode/cli-smoke.ts @@ -0,0 +1,40 @@ +import fs from "fs" +import os from "os" +import path from "path" + +export namespace KiloCliSmoke { + export async function pty(binary: string) { + const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "kilo-pty-")) + const env = { ...process.env } + delete env.KILO_MODELS_PATH + delete env.KILO_MODELS_URL + delete env.KILO_CONFIG + delete env.KILO_CONFIG_DIR + + try { + const proc = Bun.spawn([binary, "--pure", "__pty-smoke"], { + env: { + ...env, + HOME: root, + XDG_DATA_HOME: path.join(root, "data"), + XDG_CACHE_HOME: path.join(root, "cache"), + XDG_CONFIG_HOME: path.join(root, "config"), + XDG_STATE_HOME: path.join(root, "state"), + KILO_PTY_SMOKE: "1", + KILO_NO_DAEMON: "1", + KILO_DISABLE_MODELS_FETCH: "1", + KILO_DISABLE_PROJECT_CONFIG: "1", + KILO_CONFIG_CONTENT: "{}", + KILO_AUTH_CONTENT: "{}", + }, + stdout: "inherit", + stderr: "inherit", + windowsHide: true, + }) + const code = await proc.exited + if (code !== 0) throw new Error(`Compiled TUI smoke test exited with code ${code}`) + } finally { + await fs.promises.rm(root, { recursive: true, force: true }) + } + } +}