mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
Merge pull request #6955 from Kilo-Org/kirillk/cli-init-failure-6209
feat(vscode): surface CLI startup errors in the chat view
This commit is contained in:
@@ -11,7 +11,7 @@ import type {
|
||||
FilePartInput,
|
||||
Config,
|
||||
} from "@kilocode/sdk/v2/client"
|
||||
import { type KiloConnectionService, type KilocodeNotification } from "./services/cli-backend"
|
||||
import { type KiloConnectionService, type KilocodeNotification, ServerStartupError } from "./services/cli-backend"
|
||||
import type { EditorContext, CloudSessionData } from "./services/cli-backend/types"
|
||||
import { FileIgnoreController } from "./services/autocomplete/shims/FileIgnoreController"
|
||||
import { handleChatCompletionRequest } from "./services/autocomplete/chat-autocomplete/handleChatCompletionRequest"
|
||||
@@ -75,6 +75,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
/** Guard to prevent checkAndShowMigrationWizard running concurrently. */ // legacy-migration
|
||||
private migrationCheckInFlight = false // legacy-migration
|
||||
private unsubscribeNotificationDismiss: (() => void) | null = null
|
||||
private initConnectionPromise: Promise<void> | null = null
|
||||
private webviewMessageDisposable: vscode.Disposable | null = null
|
||||
|
||||
/** Lazily initialized ignore controller for .kilocodeignore filtering */
|
||||
@@ -397,6 +398,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
case "openChanges":
|
||||
vscode.commands.executeCommand("kilo-code.new.showChanges")
|
||||
break
|
||||
case "retryConnection":
|
||||
console.log("[Kilo New] KiloProvider: 🔄 Retrying connection...")
|
||||
this.initializeConnection().catch((e) =>
|
||||
console.error("[Kilo New] KiloProvider: ❌ Retry connection failed:", e),
|
||||
)
|
||||
break
|
||||
case "openSubAgentViewer":
|
||||
vscode.commands.executeCommand("kilo-code.new.openSubAgentViewer", message.sessionID, message.title)
|
||||
break
|
||||
@@ -610,9 +617,22 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
* Initialize connection to the CLI backend server.
|
||||
* Subscribes to the shared KiloConnectionService.
|
||||
*/
|
||||
private async initializeConnection(): Promise<void> {
|
||||
private initializeConnection(): Promise<void> {
|
||||
if (this.initConnectionPromise) {
|
||||
return this.initConnectionPromise
|
||||
}
|
||||
this.initConnectionPromise = this.doInitializeConnection().finally(() => {
|
||||
this.initConnectionPromise = null
|
||||
})
|
||||
return this.initConnectionPromise
|
||||
}
|
||||
|
||||
private async doInitializeConnection(): Promise<void> {
|
||||
console.log("[Kilo New] KiloProvider: 🔧 Starting initializeConnection...")
|
||||
|
||||
this.connectionState = "connecting"
|
||||
this.postMessage({ type: "connectionState", state: "connecting" })
|
||||
|
||||
// Clean up any existing subscriptions (e.g., sidebar re-shown)
|
||||
this.unsubscribeEvent?.()
|
||||
this.unsubscribeState?.()
|
||||
@@ -710,6 +730,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
type: "connectionState",
|
||||
state: "error",
|
||||
error: getErrorMessage(error) || "Failed to connect to CLI backend",
|
||||
...(error instanceof ServerStartupError && {
|
||||
userMessage: error.userMessage,
|
||||
userDetails: error.userDetails,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export const dict = {
|
||||
"server.processExited": "انتهت عملية CLI بالرمز {{code}} قبل بدء الخادم",
|
||||
"server.startupTimeout": "انتهت مهلة بدء تشغيل الخادم بعد {{seconds}} ثانية",
|
||||
} as const
|
||||
@@ -0,0 +1,4 @@
|
||||
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",
|
||||
} as const
|
||||
@@ -0,0 +1,4 @@
|
||||
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",
|
||||
} as const
|
||||
@@ -0,0 +1,4 @@
|
||||
export const dict = {
|
||||
"server.processExited": "CLI-processen afsluttede med kode {{code}} før serveren startede",
|
||||
"server.startupTimeout": "Serverens opstartstid udløb efter {{seconds}} sekunder",
|
||||
} as const
|
||||
@@ -0,0 +1,4 @@
|
||||
export const dict = {
|
||||
"server.processExited": "Der CLI-Prozess wurde mit dem Code {{code}} beendet, bevor der Server gestartet wurde",
|
||||
"server.startupTimeout": "Zeitüberschreitung beim Serverstart nach {{seconds}} Sekunden",
|
||||
} as const
|
||||
@@ -0,0 +1,4 @@
|
||||
export const dict = {
|
||||
"server.processExited": "CLI process exited with code {{code}} before server started",
|
||||
"server.startupTimeout": "Server startup timeout after {{seconds}} seconds",
|
||||
} as const
|
||||
@@ -0,0 +1,4 @@
|
||||
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",
|
||||
} as const
|
||||
@@ -0,0 +1,4 @@
|
||||
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",
|
||||
} as const
|
||||
@@ -0,0 +1,66 @@
|
||||
import { dict as ar } from "./ar"
|
||||
import { dict as br } from "./br"
|
||||
import { dict as bs } from "./bs"
|
||||
import { dict as da } from "./da"
|
||||
import { dict as de } from "./de"
|
||||
import { dict as en } from "./en"
|
||||
import { dict as es } from "./es"
|
||||
import { dict as fr } from "./fr"
|
||||
import { dict as ja } from "./ja"
|
||||
import { dict as ko } from "./ko"
|
||||
import { dict as no } from "./no"
|
||||
import { dict as pl } from "./pl"
|
||||
import { dict as ru } from "./ru"
|
||||
import { dict as th } from "./th"
|
||||
import { dict as zh } from "./zh"
|
||||
import { dict as zht } from "./zht"
|
||||
import { type dict as enDict } from "./en"
|
||||
|
||||
const bundles: Record<string, Record<string, string>> = {
|
||||
ar,
|
||||
br,
|
||||
bs,
|
||||
da,
|
||||
de,
|
||||
en,
|
||||
es,
|
||||
fr,
|
||||
ja,
|
||||
ko,
|
||||
no,
|
||||
pl,
|
||||
ru,
|
||||
th,
|
||||
zh,
|
||||
zht,
|
||||
}
|
||||
|
||||
function resolveLocale(lang: string): string {
|
||||
const lower = lang.toLowerCase()
|
||||
if (lower.startsWith("zh")) return lower.includes("hant") ? "zht" : "zh"
|
||||
if (lower.startsWith("nb") || lower.startsWith("nn")) return "no"
|
||||
if (lower.startsWith("pt")) return "br"
|
||||
for (const key of Object.keys(bundles)) {
|
||||
if (lower.startsWith(key)) return key
|
||||
}
|
||||
return "en"
|
||||
}
|
||||
|
||||
function loadTranslations(): Record<string, string> {
|
||||
// vscode.env.language is available at module load time in the extension host
|
||||
const vscode = require("vscode") as typeof import("vscode")
|
||||
const locale = resolveLocale(vscode.env.language)
|
||||
return { ...en, ...(bundles[locale] ?? {}) }
|
||||
}
|
||||
|
||||
const translations: Record<string, string> = loadTranslations()
|
||||
|
||||
export function t(key: keyof typeof enDict, vars?: Record<string, string | number>): string {
|
||||
let text = translations[key] ?? key
|
||||
if (vars) {
|
||||
for (const [k, v] of Object.entries(vars)) {
|
||||
text = text.replaceAll(`{{${k}}}`, String(v))
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const dict = {
|
||||
"server.processExited": "サーバーが起動する前に、CLI プロセスがコード {{code}} で終了しました",
|
||||
"server.startupTimeout": "サーバーの起動が {{seconds}} 秒後にタイムアウトしました",
|
||||
} as const
|
||||
@@ -0,0 +1,4 @@
|
||||
export const dict = {
|
||||
"server.processExited": "서버가 시작되기 전에 CLI 프로세스가 코드 {{code}}로 종료되었습니다",
|
||||
"server.startupTimeout": "{{seconds}}초 후 서버 시작 시간이 초과되었습니다",
|
||||
} as const
|
||||
@@ -0,0 +1,4 @@
|
||||
export const dict = {
|
||||
"server.processExited": "CLI-prosessen avsluttet med kode {{code}} før serveren startet",
|
||||
"server.startupTimeout": "Tidsavbrudd for serveroppstart etter {{seconds}} sekunder",
|
||||
} as const
|
||||
@@ -0,0 +1,4 @@
|
||||
export const dict = {
|
||||
"server.processExited": "Proces CLI zakończył się z kodem {{code}} przed uruchomieniem serwera",
|
||||
"server.startupTimeout": "Przekroczono limit czasu uruchamiania serwera po {{seconds}} sekundach",
|
||||
} as const
|
||||
@@ -0,0 +1,4 @@
|
||||
export const dict = {
|
||||
"server.processExited": "Процесс CLI завершился с кодом {{code}} до запуска сервера",
|
||||
"server.startupTimeout": "Время ожидания запуска сервера истекло через {{seconds}} секунд",
|
||||
} as const
|
||||
@@ -0,0 +1,4 @@
|
||||
export const dict = {
|
||||
"server.processExited": "กระบวนการ CLI ออกด้วยรหัส {{code}} ก่อนที่เซิร์ฟเวอร์จะเริ่มทำงาน",
|
||||
"server.startupTimeout": "หมดเวลาการเริ่มต้นเซิร์ฟเวอร์หลังจาก {{seconds}} วินาที",
|
||||
} as const
|
||||
@@ -0,0 +1,4 @@
|
||||
export const dict = {
|
||||
"server.processExited": "在服务器启动之前,CLI 进程已退出,代码为 {{code}}",
|
||||
"server.startupTimeout": "服务器启动在 {{seconds}} 秒后超时",
|
||||
} as const
|
||||
@@ -0,0 +1,4 @@
|
||||
export const dict = {
|
||||
"server.processExited": "在伺服器啟動之前,CLI 處理程序已退出,代碼為 {{code}}",
|
||||
"server.startupTimeout": "伺服器啟動在 {{seconds}} 秒後逾時",
|
||||
} as const
|
||||
@@ -3,3 +3,4 @@
|
||||
export type { KilocodeNotification } from "./types"
|
||||
|
||||
export { KiloConnectionService } from "./connection-service"
|
||||
export { ServerStartupError } from "./server-manager"
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as crypto from "crypto"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { t } from "./i18n"
|
||||
import { parseServerPort } from "./server-utils"
|
||||
|
||||
export interface ServerInstance {
|
||||
@@ -11,6 +12,8 @@ export interface ServerInstance {
|
||||
process: ChildProcess
|
||||
}
|
||||
|
||||
const STARTUP_TIMEOUT_SECONDS = 30
|
||||
|
||||
export class ServerManager {
|
||||
private instance: ServerInstance | null = null
|
||||
private startupPromise: Promise<ServerInstance> | null = null
|
||||
@@ -84,6 +87,7 @@ export class ServerManager {
|
||||
console.log("[Kilo New] ServerManager: 📦 Process spawned with PID:", serverProcess.pid)
|
||||
|
||||
let resolved = false
|
||||
const stderrLines: string[] = []
|
||||
|
||||
serverProcess.stdout?.on("data", (data: Buffer) => {
|
||||
const output = data.toString()
|
||||
@@ -100,6 +104,7 @@ export class ServerManager {
|
||||
serverProcess.stderr?.on("data", (data: Buffer) => {
|
||||
const errorOutput = data.toString()
|
||||
console.error("[Kilo New] ServerManager: ⚠️ CLI Server stderr:", errorOutput)
|
||||
stderrLines.push(errorOutput)
|
||||
})
|
||||
|
||||
serverProcess.on("error", (error) => {
|
||||
@@ -115,18 +120,27 @@ export class ServerManager {
|
||||
this.instance = null
|
||||
}
|
||||
if (!resolved) {
|
||||
reject(new Error(`CLI process exited with code ${code} before server started`))
|
||||
const { userMessage, userDetails } = toErrorMessage(
|
||||
t("server.processExited", { code: code ?? "null" }),
|
||||
stderrLines,
|
||||
cliPath,
|
||||
)
|
||||
reject(new ServerStartupError(userMessage, userDetails))
|
||||
}
|
||||
})
|
||||
|
||||
// Timeout after 30 seconds
|
||||
setTimeout(() => {
|
||||
if (!resolved) {
|
||||
console.error("[Kilo New] ServerManager: ⏰ Server startup timeout (30s)")
|
||||
console.error(`[Kilo New] ServerManager: ⏰ Server startup timeout (${STARTUP_TIMEOUT_SECONDS}s)`)
|
||||
ServerManager.killProcess(serverProcess)
|
||||
reject(new Error("Server startup timeout"))
|
||||
const { userMessage, userDetails } = toErrorMessage(
|
||||
t("server.startupTimeout", { seconds: STARTUP_TIMEOUT_SECONDS }),
|
||||
stderrLines,
|
||||
cliPath,
|
||||
)
|
||||
reject(new ServerStartupError(userMessage, userDetails))
|
||||
}
|
||||
}, 30000)
|
||||
}, STARTUP_TIMEOUT_SECONDS * 1000)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -184,3 +198,48 @@ export class ServerManager {
|
||||
proc.on("exit", () => clearTimeout(timer))
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerStartupError extends Error {
|
||||
readonly userMessage: string
|
||||
readonly userDetails: string
|
||||
constructor(userMessage: string, userDetails: string) {
|
||||
super(userDetails)
|
||||
this.name = "ServerStartupError"
|
||||
this.userMessage = userMessage
|
||||
this.userDetails = userDetails
|
||||
}
|
||||
}
|
||||
|
||||
function stripAnsi(str: string): string {
|
||||
return str.replace(/\x1b\[[0-9;]*m/g, "")
|
||||
}
|
||||
|
||||
export function toErrorMessage(
|
||||
error: string,
|
||||
stderrLines: string[],
|
||||
cliPath?: string,
|
||||
): {
|
||||
userMessage: string
|
||||
userDetails: string
|
||||
error: string
|
||||
} {
|
||||
let lines = stderrLines.flatMap((line) => line.split("\n"))
|
||||
|
||||
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() !== "") {
|
||||
lines = [`CLI path: ${cliPath}`, ...lines]
|
||||
}
|
||||
|
||||
const detailsText = lines.map(stripAnsi).join("\n").trim()
|
||||
|
||||
return {
|
||||
userMessage,
|
||||
userDetails: detailsText,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,10 +11,12 @@ import { MessageList } from "./MessageList"
|
||||
import { PromptInput } from "./PromptInput"
|
||||
import { QuestionDock } from "./QuestionDock"
|
||||
import { PermissionDock } from "./PermissionDock"
|
||||
import { StartupErrorBanner } from "./StartupErrorBanner"
|
||||
import { useSession } from "../../context/session"
|
||||
import { useVSCode } from "../../context/vscode"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import { useWorktreeMode } from "../../context/worktree-mode"
|
||||
import { useServer } from "../../context/server"
|
||||
|
||||
interface ChatViewProps {
|
||||
onSelectSession?: (id: string) => void
|
||||
@@ -26,6 +28,7 @@ export const ChatView: Component<ChatViewProps> = (props) => {
|
||||
const vscode = useVSCode()
|
||||
const language = useLanguage()
|
||||
const worktreeMode = useWorktreeMode()
|
||||
const server = useServer()
|
||||
// Show "Show Changes" only in the standalone sidebar, not inside Agent Manager
|
||||
const isSidebar = () => worktreeMode === undefined
|
||||
|
||||
@@ -90,6 +93,9 @@ export const ChatView: Component<ChatViewProps> = (props) => {
|
||||
|
||||
<Show when={dock()}>
|
||||
<div class="chat-input">
|
||||
<Show when={server.connectionState() === "error" && server.errorMessage()}>
|
||||
<StartupErrorBanner errorMessage={server.errorMessage()!} errorDetails={server.errorDetails()!} />
|
||||
</Show>
|
||||
<Show when={questionRequest()} keyed>
|
||||
{(req) => <QuestionDock request={req} />}
|
||||
</Show>
|
||||
|
||||
@@ -175,6 +175,16 @@ export const PromptInput: Component = () => {
|
||||
(text().trim().length > 0 || imageAttach.images().length > 0 || reviewComments().length > 0) &&
|
||||
!isBusy() &&
|
||||
!isDisabled()
|
||||
const placeholder = () => {
|
||||
switch (server.connectionState()) {
|
||||
case "connecting":
|
||||
return language.t("prompt.placeholder.connecting")
|
||||
case "error":
|
||||
return language.t("prompt.placeholder.error")
|
||||
default:
|
||||
return language.t("prompt.placeholder.default")
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribe = vscode.onMessage((message) => {
|
||||
if (message.type === "chatCompletionResult") {
|
||||
@@ -557,9 +567,7 @@ export const PromptInput: Component = () => {
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
class="prompt-input"
|
||||
placeholder={
|
||||
isDisabled() ? language.t("prompt.placeholder.connecting") : language.t("prompt.placeholder.default")
|
||||
}
|
||||
placeholder={placeholder()}
|
||||
value={text()}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* StartupErrorBanner
|
||||
* Shown in the chat view when the CLI server fails to start.
|
||||
*/
|
||||
|
||||
import { Component, createSignal, Show } from "solid-js"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import { useVSCode } from "../../context/vscode"
|
||||
|
||||
interface StartupErrorBannerProps {
|
||||
errorMessage: string
|
||||
errorDetails: string
|
||||
}
|
||||
|
||||
export const StartupErrorBanner: Component<StartupErrorBannerProps> = (props) => {
|
||||
const language = useLanguage()
|
||||
const vscode = useVSCode()
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
|
||||
const retry = () => {
|
||||
vscode.postMessage({ type: "retryConnection" })
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="startup-error-banner">
|
||||
<div class="startup-error-header" onClick={() => setExpanded((v) => !v)} role="button" aria-expanded={expanded()}>
|
||||
<span class={`startup-error-chevron${expanded() ? " startup-error-chevron-expanded" : ""}`}>
|
||||
<Icon name="chevron-right" size="small" />
|
||||
</span>
|
||||
<span class="startup-error-title">
|
||||
{language.t("error.startup.title")}: <span class="startup-error-firstline">{props.errorMessage}</span>
|
||||
</span>
|
||||
<button
|
||||
class="startup-error-retry"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
retry()
|
||||
}}
|
||||
aria-label={language.t("common.retry")}
|
||||
>
|
||||
{language.t("common.retry")}
|
||||
</button>
|
||||
</div>
|
||||
<Show when={expanded()}>
|
||||
<pre class="startup-error-details">{props.errorDetails}</pre>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -11,7 +11,8 @@ interface ServerContextValue {
|
||||
connectionState: Accessor<ConnectionState>
|
||||
serverInfo: Accessor<ServerInfo | undefined>
|
||||
extensionVersion: Accessor<string | undefined>
|
||||
error: Accessor<string | undefined>
|
||||
errorMessage: Accessor<string | undefined>
|
||||
errorDetails: Accessor<string | undefined>
|
||||
isConnected: Accessor<boolean>
|
||||
profileData: Accessor<ProfileData | null>
|
||||
deviceAuth: Accessor<DeviceAuthState>
|
||||
@@ -31,7 +32,8 @@ export const ServerProvider: ParentComponent = (props) => {
|
||||
const [connectionState, setConnectionState] = createSignal<ConnectionState>("connecting")
|
||||
const [serverInfo, setServerInfo] = createSignal<ServerInfo | undefined>()
|
||||
const [extensionVersion, setExtensionVersion] = createSignal<string | undefined>()
|
||||
const [error, setError] = createSignal<string | undefined>()
|
||||
const [errorMessage, setErrorMessage] = createSignal<string | undefined>()
|
||||
const [errorDetails, setErrorDetails] = createSignal<string | undefined>()
|
||||
const [profileData, setProfileData] = createSignal<ProfileData | null>(null)
|
||||
const [deviceAuth, setDeviceAuth] = createSignal<DeviceAuthState>(initialDeviceAuth)
|
||||
const [vscodeLanguage, setVscodeLanguage] = createSignal<string | undefined>()
|
||||
@@ -46,7 +48,8 @@ export const ServerProvider: ParentComponent = (props) => {
|
||||
setServerInfo(message.serverInfo)
|
||||
if (message.extensionVersion) setExtensionVersion(message.extensionVersion)
|
||||
setConnectionState("connected")
|
||||
setError(undefined)
|
||||
setErrorMessage(undefined)
|
||||
setErrorDetails(undefined)
|
||||
if (message.vscodeLanguage) {
|
||||
setVscodeLanguage(message.vscodeLanguage)
|
||||
}
|
||||
@@ -66,15 +69,18 @@ export const ServerProvider: ParentComponent = (props) => {
|
||||
console.log("[Kilo New] Connection state changed:", message.state)
|
||||
setConnectionState(message.state)
|
||||
if (message.error) {
|
||||
setError(message.error)
|
||||
setErrorMessage(message.userMessage ?? message.error)
|
||||
setErrorDetails(message.userDetails ?? message.error)
|
||||
} else if (message.state === "connected") {
|
||||
setError(undefined)
|
||||
setErrorMessage(undefined)
|
||||
setErrorDetails(undefined)
|
||||
}
|
||||
break
|
||||
|
||||
case "error":
|
||||
console.error("[Kilo New] Server error:", message.message)
|
||||
setError(message.message)
|
||||
setErrorMessage(message.message)
|
||||
setErrorDetails(message.message)
|
||||
break
|
||||
|
||||
case "profileData":
|
||||
@@ -128,7 +134,8 @@ export const ServerProvider: ParentComponent = (props) => {
|
||||
connectionState,
|
||||
serverInfo,
|
||||
extensionVersion,
|
||||
error,
|
||||
errorMessage,
|
||||
errorDetails,
|
||||
isConnected: () => connectionState() === "connected",
|
||||
profileData,
|
||||
deviceAuth,
|
||||
|
||||
@@ -407,6 +407,8 @@ export const dict = {
|
||||
|
||||
"error.globalSync.connectFailed": "تعذر الاتصال بالخادم. هل هناك خادم يعمل في `{{url}}`؟",
|
||||
|
||||
"error.startup.title": "فشل الاتصال بالخادم",
|
||||
|
||||
"error.paidModel.title": "يجب عليك تسجيل الدخول لاستخدام هذا النموذج",
|
||||
"error.paidModel.description":
|
||||
"سجّل الدخول أو أنشئ حسابًا للوصول إلى أكثر من 500 نموذج، واستخدم الرصيد بسعر التكلفة، أو استخدم مفتاحك الخاص.",
|
||||
|
||||
@@ -407,6 +407,8 @@ export const dict = {
|
||||
|
||||
"error.globalSync.connectFailed": "Não foi possível conectar ao servidor. Há um servidor executando em `{{url}}`?",
|
||||
|
||||
"error.startup.title": "Falha na conexão com o servidor",
|
||||
|
||||
"error.paidModel.title": "Você precisa fazer login para usar este modelo",
|
||||
"error.paidModel.description":
|
||||
"Faça login ou crie uma conta para acessar mais de 500 modelos, usar créditos a preço de custo ou trazer sua própria chave.",
|
||||
|
||||
@@ -412,6 +412,8 @@ export const dict = {
|
||||
|
||||
"error.globalSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?",
|
||||
|
||||
"error.startup.title": "Povezivanje sa serverom nije uspjelo",
|
||||
|
||||
"error.paidModel.title": "Morate se prijaviti da biste koristili ovaj model",
|
||||
"error.paidModel.description":
|
||||
"Prijavite se ili kreirajte račun za pristup preko 500 modela, koristite kredite po cijeni koštanja ili donesite vlastiti ključ.",
|
||||
|
||||
@@ -409,6 +409,8 @@ export const dict = {
|
||||
|
||||
"error.globalSync.connectFailed": "Kunne ikke forbinde til server. Kører der en server på `{{url}}`?",
|
||||
|
||||
"error.startup.title": "Serverforbindelse mislykkedes",
|
||||
|
||||
"error.paidModel.title": "Du skal logge ind for at bruge denne model",
|
||||
"error.paidModel.description":
|
||||
"Log ind eller opret en konto for at få adgang til over 500 modeller, brug kreditter til kostpris, eller medbring din egen nøgle.",
|
||||
|
||||
@@ -414,6 +414,8 @@ export const dict = {
|
||||
|
||||
"error.globalSync.connectFailed": "Verbindung zum Server fehlgeschlagen. Läuft ein Server unter `{{url}}`?",
|
||||
|
||||
"error.startup.title": "Serververbindung fehlgeschlagen",
|
||||
|
||||
"error.paidModel.title": "Du musst dich anmelden, um dieses Modell zu nutzen",
|
||||
"error.paidModel.description":
|
||||
"Melde dich an oder erstelle ein Konto, um auf über 500 Modelle zuzugreifen, Credits zum Selbstkostenpreis zu nutzen oder deinen eigenen Schlüssel mitzubringen.",
|
||||
|
||||
@@ -411,6 +411,8 @@ export const dict = {
|
||||
|
||||
"error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?",
|
||||
|
||||
"error.startup.title": "Server connection failed",
|
||||
|
||||
"error.paidModel.title": "You need to sign in to use this model",
|
||||
"error.paidModel.description":
|
||||
"Sign in or create an account to access over 500 models, use credits at cost, or bring your own key.",
|
||||
@@ -838,6 +840,7 @@ export const dict = {
|
||||
|
||||
"prompt.placeholder.connecting": "Connecting to server...",
|
||||
"prompt.placeholder.default": "Type a message... (Enter to send, Shift+Enter for new line)",
|
||||
"prompt.placeholder.error": "",
|
||||
|
||||
"context.usage.sessionCost": "Session cost",
|
||||
|
||||
|
||||
@@ -410,6 +410,8 @@ export const dict = {
|
||||
|
||||
"error.globalSync.connectFailed": "No se pudo conectar al servidor. ¿Hay un servidor ejecutándose en `{{url}}`?",
|
||||
|
||||
"error.startup.title": "Error de conexión al servidor",
|
||||
|
||||
"error.paidModel.title": "Necesitas iniciar sesión para usar este modelo",
|
||||
"error.paidModel.description":
|
||||
"Inicia sesión o crea una cuenta para acceder a más de 500 modelos, usar créditos a precio de coste o traer tu propia clave.",
|
||||
|
||||
@@ -414,6 +414,8 @@ export const dict = {
|
||||
"error.globalSync.connectFailed":
|
||||
"Impossible de se connecter au serveur. Y a-t-il un serveur en cours d'exécution à `{{url}}` ?",
|
||||
|
||||
"error.startup.title": "Échec de la connexion au serveur",
|
||||
|
||||
"error.paidModel.title": "Vous devez vous connecter pour utiliser ce modèle",
|
||||
"error.paidModel.description":
|
||||
"Connectez-vous ou créez un compte pour accéder à plus de 500 modèles, utiliser des crédits au prix coûtant ou apporter votre propre clé.",
|
||||
|
||||
@@ -407,6 +407,8 @@ export const dict = {
|
||||
|
||||
"error.globalSync.connectFailed": "サーバーに接続できませんでした。`{{url}}`でサーバーが実行されていますか?",
|
||||
|
||||
"error.startup.title": "サーバー接続に失敗しました",
|
||||
|
||||
"error.paidModel.title": "このモデルを使用するにはサインインが必要です",
|
||||
"error.paidModel.description":
|
||||
"サインインまたはアカウントを作成して、500以上のモデルにアクセスし、原価でクレジットを使用するか、独自のキーを持ち込みましょう。",
|
||||
|
||||
@@ -411,6 +411,8 @@ export const dict = {
|
||||
|
||||
"error.globalSync.connectFailed": "서버에 연결할 수 없습니다. `{{url}}`에서 서버가 실행 중인가요?",
|
||||
|
||||
"error.startup.title": "서버 연결 실패",
|
||||
|
||||
"error.paidModel.title": "이 모델을 사용하려면 로그인이 필요합니다",
|
||||
"error.paidModel.description":
|
||||
"로그인하거나 계정을 만들어 500개 이상의 모델에 접근하고, 원가로 크레딧을 사용하거나, 자체 키를 가져오세요.",
|
||||
|
||||
@@ -411,6 +411,8 @@ export const dict = {
|
||||
|
||||
"error.globalSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?",
|
||||
|
||||
"error.startup.title": "Servertilkobling mislyktes",
|
||||
|
||||
"error.paidModel.title": "Du må logge inn for å bruke denne modellen",
|
||||
"error.paidModel.description":
|
||||
"Logg inn eller opprett en konto for å få tilgang til over 500 modeller, bruk kreditter til kostpris, eller ta med din egen nøkkel.",
|
||||
|
||||
@@ -409,6 +409,8 @@ export const dict = {
|
||||
|
||||
"error.globalSync.connectFailed": "Nie można połączyć się z serwerem. Czy serwer działa pod adresem `{{url}}`?",
|
||||
|
||||
"error.startup.title": "Błąd połączenia z serwerem",
|
||||
|
||||
"error.paidModel.title": "Musisz się zalogować, aby użyć tego modelu",
|
||||
"error.paidModel.description":
|
||||
"Zaloguj się lub utwórz konto, aby uzyskać dostęp do ponad 500 modeli, korzystać z kredytów po kosztach lub użyć własnego klucza.",
|
||||
|
||||
@@ -410,6 +410,8 @@ export const dict = {
|
||||
|
||||
"error.globalSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?",
|
||||
|
||||
"error.startup.title": "Ошибка подключения к серверу",
|
||||
|
||||
"error.paidModel.title": "Для использования этой модели необходимо войти",
|
||||
"error.paidModel.description":
|
||||
"Войдите или создайте аккаунт для доступа к более чем 500 моделям, используйте кредиты по себестоимости или подключите свой ключ.",
|
||||
|
||||
@@ -408,6 +408,8 @@ export const dict = {
|
||||
|
||||
"error.globalSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?",
|
||||
|
||||
"error.startup.title": "การเชื่อมต่อเซิร์ฟเวอร์ล้มเหลว",
|
||||
|
||||
"error.paidModel.title": "คุณต้องเข้าสู่ระบบเพื่อใช้โมเดลนี้",
|
||||
"error.paidModel.description":
|
||||
"เข้าสู่ระบบหรือสร้างบัญชีเพื่อเข้าถึงโมเดลกว่า 500 รายการ ใช้เครดิตในราคาทุน หรือใช้คีย์ของคุณเอง",
|
||||
|
||||
@@ -406,6 +406,8 @@ export const dict = {
|
||||
|
||||
"error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?",
|
||||
|
||||
"error.startup.title": "服务器连接失败",
|
||||
|
||||
"error.paidModel.title": "您需要登录才能使用此模型",
|
||||
"error.paidModel.description": "登录或创建账户以访问500多个模型,按成本使用积分,或使用您自己的密钥。",
|
||||
"error.paidModel.action": "登录",
|
||||
|
||||
@@ -407,6 +407,8 @@ export const dict = {
|
||||
|
||||
"error.globalSync.connectFailed": "無法連線到伺服器。是否有伺服器正在 `{{url}}` 執行?",
|
||||
|
||||
"error.startup.title": "伺服器連線失敗",
|
||||
|
||||
"error.paidModel.title": "您需要登入才能使用此模型",
|
||||
"error.paidModel.description": "登入或建立帳戶以存取500多個模型,按成本使用點數,或使用您自己的金鑰。",
|
||||
"error.paidModel.action": "登入",
|
||||
|
||||
@@ -101,6 +101,76 @@
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
/* Startup Error Banner */
|
||||
.startup-error-banner {
|
||||
margin: 8px 12px;
|
||||
border: 1px solid var(--vscode-inputValidation-errorBorder, #f14c4c);
|
||||
border-radius: 4px;
|
||||
background: var(--vscode-inputValidation-errorBackground, rgba(241, 76, 76, 0.1));
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.startup-error-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.startup-error-title {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.startup-error-firstline {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.startup-error-chevron {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
transition: transform 150ms;
|
||||
}
|
||||
|
||||
.startup-error-chevron-expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.startup-error-retry {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--vscode-textLink-foreground);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.startup-error-retry:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.startup-error-details {
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
border-top: 1px solid var(--vscode-inputValidation-errorBorder, #f14c4c);
|
||||
background: var(--vscode-editor-background);
|
||||
font-family: var(--vscode-editor-font-family, monospace);
|
||||
font-size: 11px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* New Task Button */
|
||||
.new-task-button-wrapper {
|
||||
padding: 8px 12px 0;
|
||||
|
||||
@@ -363,6 +363,8 @@ export interface ConnectionStateMessage {
|
||||
type: "connectionState"
|
||||
state: ConnectionState
|
||||
error?: string
|
||||
userMessage?: string
|
||||
userDetails?: string
|
||||
}
|
||||
|
||||
export interface ErrorMessage {
|
||||
@@ -1539,6 +1541,10 @@ export interface OpenChangesRequest {
|
||||
type: "openChanges"
|
||||
}
|
||||
|
||||
export interface RetryConnectionRequest {
|
||||
type: "retryConnection"
|
||||
}
|
||||
|
||||
// Open a sub-agent session in a read-only editor panel
|
||||
export interface OpenSubAgentViewerRequest {
|
||||
type: "openSubAgentViewer"
|
||||
@@ -1637,6 +1643,7 @@ export type WebviewMessage =
|
||||
| ApplyWorktreeDiffMessage
|
||||
| EnhancePromptRequest
|
||||
| OpenChangesRequest
|
||||
| RetryConnectionRequest
|
||||
| OpenSubAgentViewerRequest
|
||||
| SetDefaultBaseBranchRequest
|
||||
|
||||
|
||||
Reference in New Issue
Block a user