fix: Moved kilocode specific code to kilocode folder

This commit is contained in:
Imanol Maiztegui
2026-03-06 21:15:18 +01:00
parent ec0e0bf5ed
commit 2d5f8a256c
7 changed files with 123 additions and 100 deletions
+2 -2
View File
@@ -28,7 +28,7 @@ import { PromptHistoryProvider } from "./component/prompt/history"
import { FrecencyProvider } from "./component/prompt/frecency"
import { PromptStashProvider } from "./component/prompt/stash"
import { DialogAlert } from "./ui/dialog-alert"
import { MessageV2 } from "@/session/message-v2" // kilocode_change
import { isKiloError } from "@/kilocode/kilo-errors" // kilocode_change
import { ToastProvider, useToast } from "./ui/toast"
import { ExitProvider, useExit } from "./context/exit"
import { Session as SessionApi } from "@/session"
@@ -719,7 +719,7 @@ function App() {
const error = evt.properties.error
if (error && typeof error === "object" && error.name === "MessageAbortedError") return
// kilocode_change start - Skip toast for current Kilo errors - Just show inline
if (error && typeof error === "object" && MessageV2.isKiloError(error)) return
if (error && typeof error === "object" && isKiloError(error)) return
// kilocode_change end
const message = (() => {
if (!error) return "An error occurred"
@@ -56,7 +56,7 @@ import { TodoItem } from "../../component/todo-item"
import { DialogMessage } from "./dialog-message"
import type { PromptInfo } from "../../component/prompt/history"
import { DialogConfirm } from "@tui/ui/dialog-confirm"
import { MessageV2 } from "@/session/message-v2" // kilocode_change
import { KiloErrorBlock } from "@/kilocode/components/kilo-error-display" // kilocode_change
import { DialogTimeline } from "./dialog-timeline"
import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
import { DialogSessionRename } from "../../component/dialog-session-rename"
@@ -1296,36 +1296,6 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
return props.message.time.completed - user.time.created
})
// kilocode_change start - Kilo-specific error detection
const kiloErrorCode = createMemo(() => {
const error = props.message.error
if (!error) return undefined
return MessageV2.parseKiloErrorCode(error)
})
const kiloErrorTitle = createMemo(() => {
switch (kiloErrorCode()) {
case MessageV2.KILO_ERROR_CODES.PAID_MODEL_AUTH_REQUIRED:
return "You need to sign in to use this model"
case MessageV2.KILO_ERROR_CODES.PROMOTION_MODEL_LIMIT_REACHED:
return "You need to sign up to keep going"
default:
return undefined
}
})
const kiloErrorDescription = createMemo(() => {
switch (kiloErrorCode()) {
case MessageV2.KILO_ERROR_CODES.PAID_MODEL_AUTH_REQUIRED:
return "Sign in or create an account to access over 500 models, use credits at cost, or bring your own key."
case MessageV2.KILO_ERROR_CODES.PROMOTION_MODEL_LIMIT_REACHED:
return "Sign up for free to continue and explore 500 other models. Takes 2 minutes, no credit card required. Or come back later."
default:
return undefined
}
})
// kilocode_change end
return (
<>
<For each={props.parts}>
@@ -1345,7 +1315,8 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
</For>
<Show when={props.message.error && props.message.error.name !== "MessageAbortedError"}>
{/* kilocode_change start - Kilo-specific error display */}
<Switch
<KiloErrorBlock
error={props.message.error!}
fallback={
<box
border={["left"]}
@@ -1360,24 +1331,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
<text fg={theme.textMuted}>{props.message.error?.data.message}</text>
</box>
}
>
<Match when={kiloErrorCode()}>
<box
border={["left"]}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
marginTop={1}
backgroundColor={theme.backgroundPanel}
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.primary}
>
<text fg={theme.text}>{kiloErrorTitle()}</text>
<text fg={theme.textMuted}>{kiloErrorDescription()}</text>
<text fg={theme.primary}>{"Run /connect or `kilo auth login` to sign in to Kilo Gateway"}</text>
</box>
</Match>
</Switch>
/>
{/* kilocode_change end */}
</Show>
<Switch>
@@ -0,0 +1,61 @@
import { createMemo, Match, Switch, type JSX } from "solid-js"
import { SplitBorder } from "@tui/component/border"
import { useTheme } from "@tui/context/theme"
import { KILO_ERROR_CODES, parseKiloErrorCode } from "@/kilocode/kilo-errors"
import type { AssistantMessage } from "@kilocode/sdk/v2"
interface KiloErrorBlockProps {
error: NonNullable<AssistantMessage["error"]>
fallback: JSX.Element
}
export function KiloErrorBlock(props: KiloErrorBlockProps) {
const { theme } = useTheme()
const kiloErrorCode = createMemo(() => {
return parseKiloErrorCode(props.error)
})
const kiloErrorTitle = createMemo(() => {
switch (kiloErrorCode()) {
case KILO_ERROR_CODES.PAID_MODEL_AUTH_REQUIRED:
return "You need to sign in to use this model"
case KILO_ERROR_CODES.PROMOTION_MODEL_LIMIT_REACHED:
return "You need to sign up to keep going"
default:
return undefined
}
})
const kiloErrorDescription = createMemo(() => {
switch (kiloErrorCode()) {
case KILO_ERROR_CODES.PAID_MODEL_AUTH_REQUIRED:
return "Sign in or create an account to access over 500 models, use credits at cost, or bring your own key."
case KILO_ERROR_CODES.PROMOTION_MODEL_LIMIT_REACHED:
return "Sign up for free to continue and explore 500 other models. Takes 2 minutes, no credit card required. Or come back later."
default:
return undefined
}
})
return (
<Switch fallback={props.fallback}>
<Match when={kiloErrorCode()}>
<box
border={["left"]}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
marginTop={1}
backgroundColor={theme.backgroundPanel}
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.primary}
>
<text fg={theme.text}>{kiloErrorTitle()}</text>
<text fg={theme.textMuted}>{kiloErrorDescription()}</text>
<text fg={theme.primary}>{"Run /connect or `kilo auth login` to sign in to Kilo Gateway"}</text>
</box>
</Match>
</Switch>
)
}
@@ -0,0 +1,42 @@
import type { NamedError } from "@opencode-ai/util/error"
export const KILO_ERROR_CODES = {
PAID_MODEL_AUTH_REQUIRED: "PAID_MODEL_AUTH_REQUIRED",
PROMOTION_MODEL_LIMIT_REACHED: "PROMOTION_MODEL_LIMIT_REACHED",
} as const
export type KiloErrorCode = (typeof KILO_ERROR_CODES)[keyof typeof KILO_ERROR_CODES]
const KILO_ERROR_CODE_VALUES = Object.values(KILO_ERROR_CODES) as string[]
/**
* Check if an error is a Kilo-specific error (has a known Kilo error code in responseBody).
* Currently all Kilo errors are non-retryable, but this may change in the future.
*/
export function isKiloError(error: ReturnType<NamedError["toObject"]>): boolean {
return parseKiloErrorCode(error) !== undefined
}
/**
* Extract the specific Kilo error code from an APIError's responseBody.
* Returns the code string if found, undefined otherwise.
*
* Note: We check error.name === "APIError" directly instead of using
* MessageV2.APIError.isInstance() to avoid a circular dependency
* (message-v2.ts re-exports from this file).
*/
export function parseKiloErrorCode(error: ReturnType<NamedError["toObject"]>): KiloErrorCode | undefined {
if (error.name !== "APIError") return undefined
const responseBody = error.data?.responseBody
if (typeof responseBody !== "string") return undefined
try {
const body = JSON.parse(responseBody)
// Backend sends: { error: { code: "PAID_MODEL_AUTH_REQUIRED" } }
// or: { code: "PROMOTION_MODEL_LIMIT_REACHED" }
const code = body?.error?.code ?? body?.code
if (typeof code === "string" && KILO_ERROR_CODE_VALUES.includes(code)) {
return code as KiloErrorCode
}
} catch {}
return undefined
}
@@ -50,42 +50,6 @@ export namespace MessageV2 {
z.object({ message: z.string(), responseBody: z.string().optional() }),
)
// kilocode_change start - Kilo-specific error detection
export const KILO_ERROR_CODES = {
PAID_MODEL_AUTH_REQUIRED: "PAID_MODEL_AUTH_REQUIRED",
PROMOTION_MODEL_LIMIT_REACHED: "PROMOTION_MODEL_LIMIT_REACHED",
} as const
export type KiloErrorCode = (typeof KILO_ERROR_CODES)[keyof typeof KILO_ERROR_CODES]
/**
* Check if an error is a Kilo-specific error (has a known Kilo error code in responseBody).
* Currently all Kilo errors are non-retryable, but this may change in the future.
*/
export function isKiloError(error: ReturnType<NamedError["toObject"]>): boolean {
return parseKiloErrorCode(error) !== undefined
}
/**
* Extract the specific Kilo error code from an APIError's responseBody.
* Returns the code string if found, undefined otherwise.
*/
export function parseKiloErrorCode(error: ReturnType<NamedError["toObject"]>): KiloErrorCode | undefined {
if (!APIError.isInstance(error)) return undefined
const responseBody = error.data.responseBody
if (!responseBody) return undefined
try {
const body = JSON.parse(responseBody)
// Backend sends: { error: { code: "PAID_MODEL_AUTH_REQUIRED" } }
// or: { code: "PROMOTION_MODEL_LIMIT_REACHED" }
const code = body?.error?.code ?? body?.code
if (code === KILO_ERROR_CODES.PAID_MODEL_AUTH_REQUIRED) return code
if (code === KILO_ERROR_CODES.PROMOTION_MODEL_LIMIT_REACHED) return code
} catch {}
return undefined
}
// kilocode_change end
export const OutputFormatText = z
.object({
type: z.literal("text"),
+2 -1
View File
@@ -1,5 +1,6 @@
import type { NamedError } from "@opencode-ai/util/error"
import { MessageV2 } from "./message-v2"
import { isKiloError } from "@/kilocode/kilo-errors" // kilocode_change
import { iife } from "@/util/iife"
export namespace SessionRetry {
@@ -63,7 +64,7 @@ export namespace SessionRetry {
if (MessageV2.ContextOverflowError.isInstance(error)) return undefined
if (MessageV2.APIError.isInstance(error)) {
// kilocode_change start - Current Kilo errors require user action (login/signup), don't retry
if (MessageV2.isKiloError(error)) return undefined
if (isKiloError(error)) return undefined
// kilocode_change end
if (!error.data.isRetryable) return undefined
if (error.data.responseBody?.includes("FreeUsageLimitError"))
@@ -1,5 +1,6 @@
import { describe, it, expect } from "bun:test"
import { MessageV2 } from "../../src/session/message-v2"
import { KILO_ERROR_CODES, isKiloError, parseKiloErrorCode } from "../../src/kilocode/kilo-errors"
import { SessionRetry } from "../../src/session/retry"
import { NamedError } from "@opencode-ai/util/error"
@@ -26,7 +27,7 @@ describe("parseKiloErrorCode", () => {
statusCode: 401,
responseBody: JSON.stringify({ error: { code: "PAID_MODEL_AUTH_REQUIRED" } }),
})
expect(MessageV2.parseKiloErrorCode(error)).toBe("PAID_MODEL_AUTH_REQUIRED")
expect(parseKiloErrorCode(error)).toBe("PAID_MODEL_AUTH_REQUIRED")
})
it("extracts PROMOTION_MODEL_LIMIT_REACHED from { code } (top-level)", () => {
@@ -34,7 +35,7 @@ describe("parseKiloErrorCode", () => {
statusCode: 429,
responseBody: JSON.stringify({ code: "PROMOTION_MODEL_LIMIT_REACHED" }),
})
expect(MessageV2.parseKiloErrorCode(error)).toBe("PROMOTION_MODEL_LIMIT_REACHED")
expect(parseKiloErrorCode(error)).toBe("PROMOTION_MODEL_LIMIT_REACHED")
})
it("extracts PROMOTION_MODEL_LIMIT_REACHED from { error: { code } }", () => {
@@ -47,7 +48,7 @@ describe("parseKiloErrorCode", () => {
},
}),
})
expect(MessageV2.parseKiloErrorCode(error)).toBe("PROMOTION_MODEL_LIMIT_REACHED")
expect(parseKiloErrorCode(error)).toBe("PROMOTION_MODEL_LIMIT_REACHED")
})
it("returns undefined for non-Kilo error codes", () => {
@@ -55,12 +56,12 @@ describe("parseKiloErrorCode", () => {
statusCode: 429,
responseBody: JSON.stringify({ error: { code: "SOME_OTHER_ERROR" } }),
})
expect(MessageV2.parseKiloErrorCode(error)).toBeUndefined()
expect(parseKiloErrorCode(error)).toBeUndefined()
})
it("returns undefined for non-APIError types", () => {
const error = new MessageV2.AbortedError({ message: "aborted" }).toObject()
expect(MessageV2.parseKiloErrorCode(error)).toBeUndefined()
expect(parseKiloErrorCode(error)).toBeUndefined()
})
it("returns undefined for malformed responseBody", () => {
@@ -68,14 +69,14 @@ describe("parseKiloErrorCode", () => {
statusCode: 401,
responseBody: "not valid json",
})
expect(MessageV2.parseKiloErrorCode(error)).toBeUndefined()
expect(parseKiloErrorCode(error)).toBeUndefined()
})
it("returns undefined when responseBody is missing", () => {
const error = makeAPIError({
statusCode: 401,
})
expect(MessageV2.parseKiloErrorCode(error)).toBeUndefined()
expect(parseKiloErrorCode(error)).toBeUndefined()
})
})
@@ -85,7 +86,7 @@ describe("isKiloError", () => {
statusCode: 401,
responseBody: JSON.stringify({ error: { code: "PAID_MODEL_AUTH_REQUIRED" } }),
})
expect(MessageV2.isKiloError(error)).toBe(true)
expect(isKiloError(error)).toBe(true)
})
it("returns true for PROMOTION_MODEL_LIMIT_REACHED", () => {
@@ -93,7 +94,7 @@ describe("isKiloError", () => {
statusCode: 429,
responseBody: JSON.stringify({ code: "PROMOTION_MODEL_LIMIT_REACHED" }),
})
expect(MessageV2.isKiloError(error)).toBe(true)
expect(isKiloError(error)).toBe(true)
})
it("returns false for regular 429 errors without Kilo code", () => {
@@ -102,12 +103,12 @@ describe("isKiloError", () => {
isRetryable: true,
message: "Too Many Requests",
})
expect(MessageV2.isKiloError(error)).toBe(false)
expect(isKiloError(error)).toBe(false)
})
it("returns false for non-APIError types", () => {
const error = new MessageV2.AbortedError({ message: "aborted" }).toObject()
expect(MessageV2.isKiloError(error)).toBe(false)
expect(isKiloError(error)).toBe(false)
})
})