fix(cli): improve config warning UX across CLI, TUI, and VS Code

Separate verbose parse details into a new Warning.detail field so toast
messages stay short while 'kilo config check' and VS Code's Show Details
action expose the full diagnostic.  Make the TUI config-warning toast
persistent (dismissed on first prompt submit) and fire VS Code's
checkConfigWarnings() independently so prior failures in the SSE handler
cannot suppress it.  Add 'kilo config check' subcommand that prints all
warnings with color and exits non-zero.
This commit is contained in:
Alex Alecu
2026-04-07 17:05:08 +03:00
parent f0118047e5
commit 770887e96c
9 changed files with 88 additions and 15 deletions
+22 -6
View File
@@ -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<void> {
@@ -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
+40
View File
@@ -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() {},
})
@@ -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,
@@ -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(() => {}),
+12 -3
View File
@@ -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)
+5 -4
View File
@@ -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<typeof Warning>
// 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") {
+2
View File
@@ -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)
+1
View File
@@ -2479,6 +2479,7 @@ export type ConfigWarningsResponses = {
200: Array<{
path: string
message: string
detail?: string
}>
}
+3
View File
@@ -1008,6 +1008,9 @@
},
"message": {
"type": "string"
},
"detail": {
"type": "string"
}
},
"required": ["path", "message"]