diff --git a/packages/kilo-vscode/src/services/cli-backend/server-manager.ts b/packages/kilo-vscode/src/services/cli-backend/server-manager.ts index 15b562e7fb..e3b5485021 100644 --- a/packages/kilo-vscode/src/services/cli-backend/server-manager.ts +++ b/packages/kilo-vscode/src/services/cli-backend/server-manager.ts @@ -208,7 +208,11 @@ export class ServerStartupError extends Error { } } -function toErrorMessage( +function stripAnsi(str: string): string { + return str.replace(/\x1b\[[0-9;]*m/g, "") +} + +export function toErrorMessage( error: string, stderrLines: string[], cliPath?: string, @@ -218,7 +222,11 @@ function toErrorMessage( error: string } { let lines = stderrLines.flatMap((line) => line.split("\n")) - const userMessage = [...lines].reverse().find((line) => line.trim() !== "") ?? error + + const errorLine = lines.map(stripAnsi).find((line) => /Error:\s+/.test(line)) + const userMessage = errorLine + ? errorLine.match(/Error:\s+(.+)/)![1].trim() + : stripAnsi([...lines].reverse().find((line) => line.trim() !== "") ?? error).trim() lines = [error, ...lines] if (cliPath && cliPath.trim() !== "") { @@ -228,8 +236,8 @@ function toErrorMessage( const detailsText = lines.join("\n").trim() return { - userMessage: userMessage, + userMessage, userDetails: detailsText, - error: error, + error, } } diff --git a/packages/kilo-vscode/tests/unit/server-manager-utils.test.ts b/packages/kilo-vscode/tests/unit/server-manager-utils.test.ts index 84a1c5219e..c8ce28f025 100644 --- a/packages/kilo-vscode/tests/unit/server-manager-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/server-manager-utils.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from "bun:test" import { parseServerPort } from "../../src/services/cli-backend/server-utils" +import { toErrorMessage } from "../../src/services/cli-backend/server-manager" describe("parseServerPort", () => { it("parses port from standard CLI startup message", () => { @@ -44,3 +45,61 @@ describe("parseServerPort", () => { expect(parseServerPort(output)).toBe(3000) }) }) + +describe("toErrorMessage", () => { + it("uses last non-empty stderr line as userMessage when no Error: line", () => { + const result = toErrorMessage("startup failed", ["line one", "line two", ""]) + expect(result.userMessage).toBe("line two") + }) + + it("extracts message after Error: when present", () => { + const result = toErrorMessage("startup failed", ["some noise", "Error: something went wrong"]) + expect(result.userMessage).toBe("something went wrong") + }) + + it("strips ANSI codes before matching Error:", () => { + const ansiError = "\x1b[91m\x1b[1mError: \x1b[0mConfig file at /path/kilo.json is not valid JSON(C):" + const result = toErrorMessage("startup failed", [ansiError]) + expect(result.userMessage).toBe("Config file at /path/kilo.json is not valid JSON(C):") + }) + + it("finds Error: line anywhere, not just the last line", () => { + const result = toErrorMessage("startup failed", ["Error: the real problem", "subsequent noise", "more noise"]) + expect(result.userMessage).toBe("the real problem") + }) + + it("falls back to last non-empty line when no Error: match", () => { + const result = toErrorMessage("startup failed", ["", "just some output", ""]) + expect(result.userMessage).toBe("just some output") + }) + + it("falls back to error arg when stderr is empty", () => { + const result = toErrorMessage("startup failed", []) + expect(result.userMessage).toBe("startup failed") + }) + + it("strips ANSI from fallback last non-empty line", () => { + const result = toErrorMessage("startup failed", ["\x1b[31msome colored output\x1b[0m"]) + expect(result.userMessage).toBe("some colored output") + }) + + it("includes error arg in userDetails", () => { + const result = toErrorMessage("startup failed", ["some output"]) + expect(result.userDetails).toContain("startup failed") + }) + + it("includes CLI path in userDetails when provided", () => { + const result = toErrorMessage("startup failed", [], "/usr/local/bin/kilo") + expect(result.userDetails).toContain("CLI path: /usr/local/bin/kilo") + }) + + it("does not include CLI path in userDetails when not provided", () => { + const result = toErrorMessage("startup failed", []) + expect(result.userDetails).not.toContain("CLI path:") + }) + + it("returns original error string as error field", () => { + const result = toErrorMessage("startup failed", ["some output"]) + expect(result.error).toBe("startup failed") + }) +})