fix: Moved kilocode specific code to kilocode folder
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -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"))
|
||||
|
||||
+12
-11
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user