diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 05194727b75..55c2408bb0b 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -1048,6 +1048,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.postMessage({ type: "connectionState", state }) if (state === "connected") { + // kilocode_change start - fire config warnings independently so a failure in the + // sequential await chain doesn't prevent warnings from being shown + void this.checkConfigWarnings() + // kilocode_change end try { // Profile fetch is best-effort — returns 401 when user isn't logged into gateway. const sdkClient = this.client @@ -1059,7 +1063,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper await this.flushPendingSessionRefresh("sse-connected") await fetchAndSendPendingPermissions(this.permissionCtx) await fetchAndSendPendingQuestions(this.questionCtx) - await this.checkConfigWarnings() // kilocode_change } catch (error) { console.error("[Kilo New] KiloProvider: ❌ Failed during connected state handling:", error) this.postMessage({ @@ -1951,7 +1954,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper // kilocode_change start /** - * Fetch config warnings from the server and display them as VS Code warning messages. + * Fetch config warnings from the server and display a single consolidated + * VS Code warning with a "Show Details" action button. * Only shown once per provider lifecycle (flag resets on dispose/re-create, not on SSE reconnect). */ private async checkConfigWarnings(): Promise { @@ -1962,11 +1966,23 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper const list = result?.data ?? [] if (list.length === 0) return this.configWarningsShown = true - for (const warning of list) { - vscode.window.showWarningMessage(`Config: ${warning.message}`) + + const first = list[0]! + const summary = list.length === 1 ? first.message : `${first.message} (and ${list.length - 1} more)` + + const action = await vscode.window.showWarningMessage(`Config: ${summary}`, "Show Details") + if (action === "Show Details") { + const lines = list.map((w) => { + const base = `${w.path}\n ${w.message}` + return w.detail ? `${base}\n ${w.detail}` : base + }) + const channel = vscode.window.createOutputChannel("Kilo Config Warnings") + channel.clear() + channel.appendLine(lines.join("\n\n")) + channel.show() } - } catch { - // Endpoint may not exist on older CLI versions — silently ignore + } catch (err) { + console.warn("[Kilo New] KiloProvider: checkConfigWarnings failed:", err) } } // kilocode_change end diff --git a/packages/opencode/src/cli/cmd/config.ts b/packages/opencode/src/cli/cmd/config.ts new file mode 100644 index 00000000000..746946bff81 --- /dev/null +++ b/packages/opencode/src/cli/cmd/config.ts @@ -0,0 +1,40 @@ +// kilocode_change - new file +import { EOL } from "os" +import { Config } from "../../config/config" +import { bootstrap } from "../bootstrap" +import { cmd } from "./cmd" +import { UI } from "../ui" + +export const ConfigCommand = cmd({ + command: "config", + describe: "configuration tools", + builder: (yargs) => + yargs + .command({ + command: "check", + describe: "check configuration for warnings and errors", + async handler() { + await bootstrap(process.cwd(), async () => { + const list = await Config.warnings() + if (list.length === 0) { + process.stdout.write("No config warnings." + EOL) + return + } + const S = UI.Style + for (const warning of list) { + process.stderr.write(S.TEXT_WARNING_BOLD + warning.path + S.TEXT_NORMAL + EOL) + process.stderr.write(" " + warning.message + EOL) + if (warning.detail) { + for (const line of warning.detail.split("\n")) { + process.stderr.write(" " + S.TEXT_DIM + line + S.TEXT_NORMAL + EOL) + } + } + process.stderr.write(EOL) + } + process.exitCode = 1 + }) + }, + }) + .demandCommand(), + async handler() {}, +}) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 68660bb9ed1..f8935c321bb 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -636,6 +636,7 @@ export function Prompt(props: PromptProps) { }) .catch(() => {}) } + toast.dismiss() // kilocode_change - dismiss persistent config warning on first submit history.append({ ...store.prompt, mode: currentMode, diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 36119ccd7a2..aa4766bf959 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -466,7 +466,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ sdk.client.vcs.get().then((x) => setStore("vcs", reconcile(x.data))), sdk.client.path.get().then((x) => setStore("path", reconcile(x.data!))), syncWorkspaces(), - // kilocode_change start - show config warnings as toasts + // kilocode_change start - show config warnings as persistent toast sdk.client.config .warnings() .then((x) => { @@ -478,7 +478,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ title: "Config Warning", message: first.message + suffix, variant: "warning", - duration: 8000, + duration: 0, }) }) .catch(() => {}), diff --git a/packages/opencode/src/cli/cmd/tui/ui/toast.tsx b/packages/opencode/src/cli/cmd/tui/ui/toast.tsx index 36095580fb0..32b8deac14d 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/toast.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/toast.tsx @@ -60,9 +60,18 @@ function init() { const { duration, ...currentToast } = parsedOptions setStore("currentToast", currentToast) if (timeoutHandle) clearTimeout(timeoutHandle) - timeoutHandle = setTimeout(() => { - setStore("currentToast", null) - }, duration).unref() + timeoutHandle = null + if (duration && duration > 0) { + timeoutHandle = setTimeout(() => { + setStore("currentToast", null) + timeoutHandle = null + }, duration).unref() + } + }, + dismiss() { + if (timeoutHandle) clearTimeout(timeoutHandle) + timeoutHandle = null + setStore("currentToast", null) }, error: (err: any) => { if (err instanceof Error) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 152fb3fe21b..36d1b550559 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -56,6 +56,7 @@ export namespace Config { export const Warning = z.object({ path: z.string(), message: z.string(), + detail: z.string().optional(), }) export type Warning = z.infer // kilocode_change end @@ -527,8 +528,8 @@ export namespace Config { if (ConfigPaths.JsonError.isInstance(err)) return { path: err.data.path, - message: - `Config file at ${err.data.path} is not valid JSON(C)` + (err.data.message ? `: ${err.data.message}` : ""), + message: `Config file at ${err.data.path} is not valid JSON(C)`, + detail: err.data.message || undefined, } if (ConfigPaths.InvalidError.isInstance(err)) { const text = err.data.issues ? detail(err.data.issues) : err.data.message @@ -560,9 +561,9 @@ export namespace Config { warnings?: Warning[], ) { const text = detail(issues) - const message = text ? `Config file at ${item} is invalid: ${text}` : `Config file at ${item} is invalid` + const message = `Config file at ${item} is invalid` const err = new InvalidError({ path: item, issues }, { cause }) - if (warnings) warnings.push({ path: item, message }) + if (warnings) warnings.push({ path: item, message, detail: text || undefined }) const { Session } = await import("@/session") Bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }) if (kind === "command") { diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 688bc25b742..0b5bc16bc36 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -15,6 +15,7 @@ import { FormatError } from "./cli/error" import { ServeCommand } from "./cli/cmd/serve" import { WorkspaceServeCommand } from "./cli/cmd/workspace-serve" import { Filesystem } from "./util/filesystem" +import { ConfigCommand as ConfigCLICommand } from "./cli/cmd/config" // kilocode_change import { DebugCommand } from "./cli/cmd/debug" import { StatsCommand } from "./cli/cmd/stats" import { McpCommand } from "./cli/cmd/mcp" @@ -195,6 +196,7 @@ let cli = yargs(hideBin(process.argv)) .command(SessionCommand) .command(RemoteCommand) // kilocode_change .command(DbCommand) + .command(ConfigCLICommand) // kilocode_change if (Installation.isLocal()) { cli = cli.command(WorkspaceServeCommand) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index e8f9bd24bfc..44dfca17bb5 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2479,6 +2479,7 @@ export type ConfigWarningsResponses = { 200: Array<{ path: string message: string + detail?: string }> } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index b8192c91aa8..65994bb0b9e 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -1008,6 +1008,9 @@ }, "message": { "type": "string" + }, + "detail": { + "type": "string" } }, "required": ["path", "message"]