Merge branch 'feature/am-pr-view' of https://github.com/Kilo-Org/kilocode into feature/am-pr-view

This commit is contained in:
cmanu
2026-08-07 08:53:54 -07:00
27 changed files with 175 additions and 52 deletions
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Surface process exit signals and structured spawn failure details in server startup diagnostics
@@ -6,7 +6,7 @@ import { watchFontSizeConfig } from "./kilo-provider/font-size"
import { mapSSEEventToWebviewMessage } from "./kilo-provider-utils"
import { resolvePanelProjectDirectory } from "./project-directory"
import { seedSessionStatuses } from "./session-status"
import type { KiloConnectionService } from "./services/cli-backend"
import { type KiloConnectionService, ServerStartupError } from "./services/cli-backend"
import { MarketplaceService } from "./services/marketplace"
import {
fetchMarketplaceData,
@@ -123,7 +123,15 @@ export class MarketplacePanelProvider implements vscode.Disposable {
)
this.subscriptions.push(
this.connection.onStateChange((state, err) => {
this.post({ type: "connectionState", state, ...(err ? { error: err.message } : {}) })
this.post({
type: "connectionState",
state,
...(err ? { error: err.message } : {}),
...(err instanceof ServerStartupError && {
userMessage: err.userMessage,
userDetails: err.userDetails,
}),
})
if (state === "connected") void this.sync(false)
}),
this.connection.onLanguageChanged((locale) => this.post({ type: "languageChanged", locale })),
@@ -155,7 +163,15 @@ export class MarketplacePanelProvider implements vscode.Disposable {
await this.connection.connect(this.directory())
await this.sync(this.statuses.size === 0)
} catch (err) {
this.post({ type: "connectionState", state: "error", error: err instanceof Error ? err.message : String(err) })
this.post({
type: "connectionState",
state: "error",
error: err instanceof Error ? err.message : String(err),
...(err instanceof ServerStartupError && {
userMessage: err.userMessage,
userDetails: err.userDetails,
}),
})
}
}
@@ -282,3 +282,25 @@ describe("KiloConnectionService drainPendingPrompts", () => {
expect(cleared).toBe(0)
})
})
describe("KiloConnectionService server exit handling", () => {
test("reports signal name when process is killed by signal", () => {
const service = new KiloConnectionService({} as any)
let stateErr: Error | undefined
service.onStateChange((state, err) => {
if (state === "error") stateErr = err
})
;(service as any).handleServerExit(null, "SIGSEGV")
expect(stateErr?.message).toBe("CLI background process exited with signal SIGSEGV. Retry to reconnect.")
})
test("reports exit code when process exits normally with code", () => {
const service = new KiloConnectionService({} as any)
let stateErr: Error | undefined
service.onStateChange((state, err) => {
if (state === "error") stateErr = err
})
;(service as any).handleServerExit(1, null)
expect(stateErr?.message).toBe("CLI background process exited with code 1. Retry to reconnect.")
})
})
@@ -138,7 +138,7 @@ export class KiloConnectionService {
update: async () => undefined,
} satisfies Pick<vscode.Memento, "get" | "update">)
this.sandboxPreference = new SandboxPreference(state)
this.serverManager = new ServerManager(context, (code) => this.handleServerExit(code))
this.serverManager = new ServerManager(context, (code, signal) => this.handleServerExit(code, signal))
this.active = vscode.window.state.focused
this.windowStateDisposable = vscode.window.onDidChangeWindowState((ws) => {
this.active = ws.focused
@@ -790,13 +790,11 @@ export class KiloConnectionService {
this.questionRevision += 1
}
private handleServerExit(code: number | null): void {
console.warn("[Kilo New] ConnectionService: CLI background process exited:", code)
private handleServerExit(code: number | null, signal: NodeJS.Signals | null): void {
const reason = signal ? `signal ${signal}` : `code ${code ?? "unknown"}`
console.warn(`[Kilo New] ConnectionService: CLI background process exited with ${reason}`)
this.resetConnection()
this.setState(
"error",
new Error(`CLI background process exited with code ${code ?? "unknown"}. Retry to reconnect.`),
)
this.setState("error", new Error(`CLI background process exited with ${reason}. Retry to reconnect.`))
}
private async doConnect(workspaceDir: string): Promise<void> {
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "انتهت عملية CLI بالرمز {{code}} قبل بدء الخادم",
"server.startupTimeout": "انتهت مهلة بدء تشغيل الخادم بعد {{seconds}} ثانية",
"server.processExited": "تم إنهاء عملية CLI بالرمز {{code}} قبل بدء الخادم",
"server.processSignaled": "تم إنهاء عملية CLI بواسطة الإشارة {{signal}} قبل بدء الخادم",
"server.spawnFailed": "فشل في تشغيل الملف الثنائي لـ CLI ({{code}})",
"server.startupTimeout": "انتهت مهلة بدء تشغيل الخادم بعد {{seconds}} ثوانٍ",
"remote.connected": "Kilo Remote: متصل",
"remote.connecting": "Kilo Remote: جارٍ الاتصال\u2026",
} as const
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "O processo da CLI foi encerrado com o código {{code}} antes que o servidor fosse iniciado",
"server.startupTimeout": "Tempo limite de inicialização do servidor esgotado após {{seconds}} segundos",
"server.processExited": "O processo da CLI foi encerrado com o código {{code}} antes da inicialização do servidor",
"server.processSignaled": "O processo da CLI foi finalizado pelo sinal {{signal}} antes da inicialização do servidor",
"server.spawnFailed": "Falha ao gerar o binário da CLI ({{code}})",
"server.startupTimeout": "Tempo limite de inicialização do servidor atingido após {{seconds}} segundos",
"remote.connected": "Kilo Remote: Conectado",
"remote.connecting": "Kilo Remote: Conectando\u2026",
} as const
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "CLI proces je izašao sa kodom {{code}} prije nego što se server pokrenuo",
"server.startupTimeout": "Vrijeme pokretanja servera je isteklo nakon {{seconds}} sekundi",
"server.processExited": "CLI proces je izašao s kodom {{code}} prije nego što je server pokrenut",
"server.processSignaled": "CLI proces je prekinut signalom {{signal}} prije pokretanja servera",
"server.spawnFailed": "Neuspjelo pokretanje CLI binarne datoteke ({{code}})",
"server.startupTimeout": "Isteklo je vrijeme za pokretanje servera nakon {{seconds}} sekundi",
"remote.connected": "Kilo Remote: Povezano",
"remote.connecting": "Kilo Remote: Povezivanje\u2026",
} as const
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "CLI-processen afsluttede med kode {{code}} før serveren startede",
"server.startupTimeout": "Serverens opstartstid udløb efter {{seconds}} sekunder",
"server.processExited": "CLI-processen afsluttedes med kode {{code}}, før serveren startede",
"server.processSignaled": "CLI-processen blev afbrudt af signal {{signal}}, før serveren startede",
"server.spawnFailed": "Kunne ikke starte CLI-binærfil ({{code}})",
"server.startupTimeout": "Timeout for serverstart efter {{seconds}} sekunder",
"remote.connected": "Kilo Remote: Forbundet",
"remote.connecting": "Kilo Remote: Forbinder\u2026",
} as const
@@ -1,5 +1,8 @@
export const dict = {
"server.processExited": "Der CLI-Prozess wurde mit dem Code {{code}} beendet, bevor der Server gestartet wurde",
"server.processSignaled":
"Der CLI-Prozess wurde durch das Signal {{signal}} beendet, bevor der Server gestartet wurde",
"server.spawnFailed": "Fehler beim Starten der CLI-Binärdatei ({{code}})",
"server.startupTimeout": "Zeitüberschreitung beim Serverstart nach {{seconds}} Sekunden",
"remote.connected": "Kilo Remote: Verbunden",
"remote.connecting": "Kilo Remote: Verbindung wird hergestellt\u2026",
@@ -1,5 +1,7 @@
export const dict = {
"server.processExited": "CLI process exited with code {{code}} before server started",
"server.processSignaled": "CLI process terminated by signal {{signal}} before server started",
"server.spawnFailed": "Failed to spawn CLI binary ({{code}})",
"server.startupTimeout": "Server startup timeout after {{seconds}} seconds",
"remote.connected": "Kilo Remote: Connected",
"remote.connecting": "Kilo Remote: Connecting\u2026",
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "El proceso de la CLI finalizó con el código {{code}} antes de que se iniciara el servidor",
"server.startupTimeout": "Tiempo de espera de inicio del servidor agotado después de {{seconds}} segundos",
"server.processExited": "El proceso CLI finalizó con el código {{code}} antes de que se iniciara el servidor",
"server.processSignaled": "El proceso CLI fue terminado por la señal {{signal}} antes de que se iniciara el servidor",
"server.spawnFailed": "Error al generar el binario CLI ({{code}})",
"server.startupTimeout": "Tiempo de espera agotado para el inicio del servidor después de {{seconds}} segundos",
"remote.connected": "Kilo Remote: Conectado",
"remote.connecting": "Kilo Remote: Conectando\u2026",
} as const
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "فرآیند CLI با کد {{code}} قبل از راه‌اندازی سرور خاتمه یافت",
"server.processExited": "فرایند CLI قبل از شروع سرور با کد {{code}} خارج شد",
"server.processSignaled": "فرایند CLI با سیگنال {{signal}} قبل از شروع سرور متوقف شد",
"server.spawnFailed": "اجرای فایل باینری CLI ناموفق بود ({{code}})",
"server.startupTimeout": "زمان راه‌اندازی سرور پس از {{seconds}} ثانیه به پایان رسید",
"remote.connected": "Kilo Remote: متصل شد",
"remote.connecting": "Kilo Remote: در حال اتصال",
"remote.connecting": "Kilo Remote: در حال اتصال\u2026",
} as const
+6 -4
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "Le processus CLI s'est terminé avec le code {{code}} avant le démarrage du serveur",
"server.startupTimeout": "Délai de démarrage du serveur dépassé après {{seconds}} secondes",
"remote.connected": "Kilo Remote\u00a0: Connecté",
"remote.connecting": "Kilo Remote\u00a0: Connexion\u2026",
"server.processExited": "Le processus CLI a quitté avec le code {{code}} avant le démarrage du serveur",
"server.processSignaled": "Le processus CLI a été arrêté par le signal {{signal}} avant le démarrage du serveur",
"server.spawnFailed": "Échec du lancement du binaire CLI ({{code}})",
"server.startupTimeout": "Délai d'attente du démarrage du serveur dépassé après {{seconds}} secondes",
"remote.connected": "Kilo Remote : Connecté",
"remote.connecting": "Kilo Remote : Connexion en cours\u2026",
} as const
+5 -3
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "Il processo CLI è uscito con codice {{code}} prima dell'avvio del server",
"server.processExited": "Il processo CLI è terminato con il codice {{code}} prima dell'avvio del server",
"server.processSignaled": "Il processo CLI è stato terminato dal segnale {{signal}} prima dell'avvio del server",
"server.spawnFailed": "Impossibile avviare il binario CLI ({{code}})",
"server.startupTimeout": "Timeout di avvio del server dopo {{seconds}} secondi",
"remote.connected": "Kilo Remote: connesso",
"remote.connecting": "Kilo Remote: connessione...",
"remote.connected": "Kilo Remote: Connesso",
"remote.connecting": "Kilo Remote: Connessione in corso\u2026",
} as const
@@ -1,5 +1,7 @@
export const dict = {
"server.processExited": "サーバーが起動する前に、CLI プロセスがコード {{code}} で終了しました",
"server.processSignaled": "サーバーが起動する前に、CLI プロセスがシグナル {{signal}} で終了しました",
"server.spawnFailed": "CLI バイナリの起動に失敗しました ({{code}})",
"server.startupTimeout": "サーバーの起動が {{seconds}} 秒後にタイムアウトしました",
"remote.connected": "Kilo Remote: 接続済み",
"remote.connecting": "Kilo Remote: 接続中\u2026",
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "서버가 시작되기 전에 CLI 프로세스가 코드 {{code}}로 종료되었습니다",
"server.startupTimeout": "{{seconds}}초 후 서버 시작 시간이 초과되었습니다",
"server.processExited": "서버가 시작되기 전에 CLI 프로세스가 코드 {{code}}(으)로 종료되었습니다",
"server.processSignaled": "서버 시작되기 전에 CLI 프로세스가 신호 {{signal}}(으)로 종료되었습니다",
"server.spawnFailed": "CLI 바이너리를 생성하지 못했습니다 ({{code}})",
"server.startupTimeout": "{{seconds}}초 후 서버 시작 시간 초과",
"remote.connected": "Kilo Remote: 연결됨",
"remote.connecting": "Kilo Remote: 연결 중\u2026",
} as const
@@ -1,5 +1,7 @@
export const dict = {
"server.processExited": "CLI-proces is afgesloten met code {{code}} voordat de server is gestart",
"server.processSignaled": "CLI-proces is beëindigd door signaal {{signal}} voordat de server is gestart",
"server.spawnFailed": "Kan CLI-binair bestand niet starten ({{code}})",
"server.startupTimeout": "Time-out bij opstarten van server na {{seconds}} seconden",
"remote.connected": "Kilo Remote: Verbonden",
"remote.connecting": "Kilo Remote: Verbinden\u2026",
@@ -1,5 +1,7 @@
export const dict = {
"server.processExited": "CLI-prosessen avsluttet med kode {{code}} før serveren startet",
"server.processSignaled": "CLI-prosessen ble avsluttet av signal {{signal}} før serveren startet",
"server.spawnFailed": "Kunne ikke starte CLI-binærfil ({{code}})",
"server.startupTimeout": "Tidsavbrudd for serveroppstart etter {{seconds}} sekunder",
"remote.connected": "Kilo Remote: Tilkoblet",
"remote.connecting": "Kilo Remote: Kobler til\u2026",
+3 -1
View File
@@ -1,5 +1,7 @@
export const dict = {
"server.processExited": "Proces CLI zakończył się z kodem {{code}} przed uruchomieniem serwera",
"server.processExited": "Proces CLI zakończył działanie z kodem {{code}} przed uruchomieniem serwera",
"server.processSignaled": "Proces CLI został przerwany przez sygnał {{signal}} przed uruchomieniem serwera",
"server.spawnFailed": "Nie udało się uruchomić pliku binarnego CLI ({{code}})",
"server.startupTimeout": "Przekroczono limit czasu uruchamiania serwera po {{seconds}} sekundach",
"remote.connected": "Kilo Remote: Połączono",
"remote.connecting": "Kilo Remote: Łączenie\u2026",
@@ -1,5 +1,7 @@
export const dict = {
"server.processExited": "Процесс CLI завершился с кодом {{code}} до запуска сервера",
"server.processSignaled": "Процесс CLI был завершен сигналом {{signal}} до запуска сервера",
"server.spawnFailed": "Не удалось запустить исполняемый файл CLI ({{code}})",
"server.startupTimeout": "Время ожидания запуска сервера истекло через {{seconds}} секунд",
"remote.connected": "Kilo Remote: Подключено",
"remote.connecting": "Kilo Remote: Подключение\u2026",
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "กระบวนการ CLI ออกด้วยรหัส {{code}} ก่อนที่เซิร์ฟเวอร์จะเริ่มทำงาน",
"server.startupTimeout": "หมดเวลาการเริ่มต้นเซิร์ฟเวอร์หลังจาก {{seconds}} วินาที",
"server.processExited": "กระบวนการ CLI ออกด้วยรหัส {{code}} ก่อนที่เซิร์ฟเวอร์จะเริ่มต้น",
"server.processSignaled": "กระบวนการ CLI ถูกยุติโดยสัญญาณ {{signal}} ก่อนที่เซิร์ฟเวอร์จะเริ่มต้น",
"server.spawnFailed": "ไม่สามารถเริ่มต้นไบนารี CLI ({{code}})",
"server.startupTimeout": "หมดเวลาเริ่มต้นเซิร์ฟเวอร์หลังจาก {{seconds}} วินาที",
"remote.connected": "Kilo Remote: เชื่อมต่อแล้ว",
"remote.connecting": "Kilo Remote: กำลังเชื่อมต่อ\u2026",
} as const
+3 -1
View File
@@ -1,5 +1,7 @@
export const dict = {
"server.processExited": "CLI işlemi sunucu başlamadan önce {{code}} koduyla çıktı",
"server.processExited": "CLI işlemi sunucu başlatılmadan önce {{code}} koduyla çıktı",
"server.processSignaled": "CLI işlemi sunucu başlatılmadan önce {{signal}} sinyali ile sonlandırıldı",
"server.spawnFailed": "CLI ikili dosyası başlatılamadı ({{code}})",
"server.startupTimeout": "{{seconds}} saniye sonra sunucu başlatma zaman aşımı",
"remote.connected": "Kilo Remote: Bağlandı",
"remote.connecting": "Kilo Remote: Bağlanıyor\u2026",
+3 -1
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "Процес CLI завершився з кодом {{code}} до запуску сервера",
"server.startupTimeout": "Час очікування запуску сервера вичерпано після {{seconds}} секунд",
"server.processSignaled": "Процес CLI був завершений сигналом {{signal}} до запуску сервера",
"server.spawnFailed": "Не вдалося запустити двійковий файл CLI ({{code}})",
"server.startupTimeout": "Час очікування запуску сервера минув через {{seconds}} секунд",
"remote.connected": "Kilo Remote: Підключено",
"remote.connecting": "Kilo Remote: Підключення\u2026",
} as const
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "在服务器启动之前,CLI 进程已退出,代码 {{code}}",
"server.startupTimeout": "服务器启动在 {{seconds}} 秒后超时",
"server.processExited": "CLI 进程在服务器启动前以代码 {{code}} 退出",
"server.processSignaled": "CLI 进程在服务器启动前被信号 {{signal}} 终止",
"server.spawnFailed": "生成 CLI 二进制文件失败 ({{code}})",
"server.startupTimeout": "{{seconds}} 秒后服务器启动超时",
"remote.connected": "Kilo Remote: 已连接",
"remote.connecting": "Kilo Remote: 正在连接\u2026",
} as const
+4 -2
View File
@@ -1,6 +1,8 @@
export const dict = {
"server.processExited": "在伺服器啟動之前,CLI 處理程序已退出,代碼 {{code}}",
"server.startupTimeout": "伺服器啟動在 {{seconds}} 秒後逾時",
"server.processExited": "CLI 程序在伺服器啟動前以代碼 {{code}} 退出",
"server.processSignaled": "CLI 程序在伺服器啟動前被信號 {{signal}} 終止",
"server.spawnFailed": "生成 CLI 二進位檔案失敗 ({{code}})",
"server.startupTimeout": "{{seconds}} 秒後伺服器啟動逾時",
"remote.connected": "Kilo Remote: 已連線",
"remote.connecting": "Kilo Remote: 正在連線\u2026",
} as const
@@ -17,7 +17,7 @@ export interface ServerInstance {
const STARTUP_TIMEOUT_SECONDS = 30
type WorkspaceFolderLike = { uri: { fsPath: string } }
type ServerExitListener = (code: number | null) => void
type ServerExitListener = (code: number | null, signal: NodeJS.Signals | null) => void
export function resolveServerCwd(folders: readonly WorkspaceFolderLike[] | undefined, storage: string): string {
return folders?.[0]?.uri.fsPath ?? storage
@@ -182,25 +182,36 @@ export class ServerManager {
stderrLines.push(errorOutput)
})
serverProcess.on("error", (error) => {
console.error("[Kilo New] ServerManager: ❌ Process error:", error)
serverProcess.on("error", (err: NodeJS.ErrnoException) => {
console.error("[Kilo New] ServerManager: ❌ Process error:", err)
if (!resolved) {
reject(error)
const spawnErr = err as NodeJS.ErrnoException & { spawnargs?: string[] }
const code = err.code || err.name || "UNKNOWN"
const header = t("server.spawnFailed", { code })
const lines = [
`Error: ${err.message}`,
...(err.code ? [`Code: ${err.code}`] : []),
...(err.errno !== undefined ? [`Errno: ${err.errno}`] : []),
...(err.syscall ? [`Syscall: ${err.syscall}`] : []),
...(err.path ? [`Path: ${err.path}`] : []),
...(Array.isArray(spawnErr.spawnargs) ? [`Spawn args: ${JSON.stringify(spawnErr.spawnargs)}`] : []),
]
const { userMessage, userDetails } = toErrorMessage(header, [...lines, ...stderrLines], cliPath)
reject(new ServerStartupError(userMessage, userDetails))
}
})
serverProcess.on("exit", (code) => {
console.log("[Kilo New] ServerManager: 🛑 Process exited with code:", code)
serverProcess.on("exit", (code, signal) => {
console.warn("[Kilo New] ServerManager: 🛑 Process exited:", { code, signal })
if (this.instance?.process === serverProcess) {
this.instance = null
this.onExit?.(code)
this.onExit?.(code, signal)
}
if (!resolved) {
const { userMessage, userDetails } = toErrorMessage(
t("server.processExited", { code: code ?? "null" }),
stderrLines,
cliPath,
)
const msg = signal
? t("server.processSignaled", { signal })
: t("server.processExited", { code: code ?? "unknown" })
const { userMessage, userDetails } = toErrorMessage(msg, stderrLines, cliPath)
reject(new ServerStartupError(userMessage, userDetails))
}
})
@@ -295,6 +295,34 @@ describe("toErrorMessage", () => {
const result = toErrorMessage("startup failed", ["some output"])
expect(result.error).toBe("startup failed")
})
it("formats structured spawn error details with syscall, errno, and args", () => {
const spawnLines = [
"Error: spawn UNKNOWN",
"Code: UNKNOWN",
"Errno: -86",
"Syscall: spawn",
"Path: /path/to/bin/kilo",
'Spawn args: ["serve","--port","0"]',
]
const result = toErrorMessage("Failed to spawn CLI binary (UNKNOWN)", spawnLines, "/path/to/bin/kilo")
expect(result.userMessage).toBe("spawn UNKNOWN")
expect(result.userDetails).toContain("CLI path: /path/to/bin/kilo")
expect(result.userDetails).toContain("Failed to spawn CLI binary (UNKNOWN)")
expect(result.userDetails).toContain("Syscall: spawn")
expect(result.userDetails).toContain("Errno: -86")
})
it("handles signal termination without stderr lines cleanly", () => {
const result = toErrorMessage(
"CLI process terminated by signal SIGSEGV before server started",
[],
"/path/to/bin/kilo",
)
expect(result.userMessage).toBe("CLI process terminated by signal SIGSEGV before server started")
expect(result.userDetails).toContain("CLI path: /path/to/bin/kilo")
expect(result.userDetails).toContain("CLI process terminated by signal SIGSEGV before server started")
})
})
describe("server workspace helpers", () => {