Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## [v5.3.1] - 2026-01-30

### Added

- Retry button for 5xx streaming failures

### Fixed

- Fixed use_skill tool being included in system prompt when no skills are available

---

## [v5.3.0] - 2026-01-29

### Added
Expand Down
16 changes: 12 additions & 4 deletions src/api/providers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,9 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
if (this.providerName == "KiloCode" && isAnyRecognizedKiloCodeError(error)) {
throw error
}
throw new Error(makeOpenRouterErrorReadable(error))
const err = new Error(makeOpenRouterErrorReadable(error)) as any
err.status = error?.status || error?.code
throw err
// kilocode_change end
}

Expand All @@ -247,7 +249,9 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
if ("error" in chunk) {
const error = chunk.error as { message?: string; code?: number }
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
throw new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`)
const err = new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`) as any
err.status = error?.code
throw err
Comment on lines 250 to +254
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Type Safety

Issue: chunk.error is cast to { code?: number }, but OpenAI/OpenRouter API error codes are typically strings (e.g., "invalid_api_key"). Assigning a string to err.status (which implies an HTTP status code) is misleading and violates the type assertion.

Fix: Update the type assertion to allow string codes and only assign to status if it is a number.

Impact: Prevents runtime type mismatches and potential logic errors downstream.

Suggested change
const error = chunk.error as { message?: string; code?: number }
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
throw new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`)
const err = new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`) as any
err.status = error?.code
throw err
const error = chunk.error as { message?: string; code?: string | number }
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
const err = new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`) as any
err.status = typeof error?.code === "number" ? error.code : undefined
throw err

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/matter nothing will break existing right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this change won't break existing functionality! The fix is fully backward compatible:

Safe Changes:

  • Only affects error handling paths (when API calls fail)
  • Success cases remain completely unchanged
  • Existing error codes (both string and number) will work as before
  • No changes to function signatures or return types

What Actually Changes:

  • err.status will now only be set for numeric HTTP status codes (proper behavior)
  • String error codes (like "invalid_api_key") won't be incorrectly assigned to status
  • Better type safety prevents future bugs

Example Impact:

  • Before: "invalid_api_key"err.status = "invalid_api_key" (wrong!)
  • After: "invalid_api_key"err.status = undefined (correct!)
  • Before: 401err.status = 401 (correct!)
  • After: 401err.status = 401 (still correct!)

The change actually makes error handling more predictable and prevents downstream code from making incorrect assumptions about error types. Your existing error handling logic will work the same or better!

}

// kilocode_change start
Expand Down Expand Up @@ -332,7 +336,9 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
} catch (error) {
console.error("OpenRouter API Error:", error)
let errorMessage = makeOpenRouterErrorReadable(error)
throw new Error(errorMessage)
const err = new Error(errorMessage) as any
err.status = error?.status || error?.code
throw err
}

if (lastUsage) {
Expand Down Expand Up @@ -411,7 +417,9 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH

if ("error" in response) {
const error = response.error as { message?: string; code?: number }
throw new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`)
const err = new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`) as any
err.status = error?.code
throw err
}

const completion = response as OpenAI.Chat.ChatCompletion
Expand Down
12 changes: 9 additions & 3 deletions src/api/providers/utils/openai-error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,19 @@ export function handleOpenAIError(error: unknown, providerName: string): Error {

// Invalid character/ByteString conversion error in API key
if (msg.includes("Cannot convert argument to a ByteString")) {
return new Error(i18n.t("common:errors.api.invalidKeyInvalidChars"))
const err = new Error(i18n.t("common:errors.api.invalidKeyInvalidChars")) as any
err.status = (error as any).status
return err
}

// For other Error instances, wrap with provider-specific prefix
return new Error(`${providerName} completion error: ${msg}`)
const err = new Error(`${providerName} completion error: ${msg}`) as any
err.status = (error as any).status
return err
}

// Non-Error: wrap with provider-specific prefix
return new Error(`${providerName} completion error: ${String(error)}`)
const err = new Error(`${providerName} completion error: ${String(error)}`) as any
err.status = (error as any).status
return err
}
5 changes: 4 additions & 1 deletion src/core/prompts/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ async function getSkillsSection(workspacePath: string): Promise<string> {
.join("\n")

return `You are provided Skills below, these skills are to be used by you as per your descretion. The purpose of these skills is to provide you additional niche context for you tasks. You might get skills for React, Security or even third-party tools. Use the tool use_skill to get the skill context:
${skillList}`
${skillList}

IMPORTANT: Skills are not tool calls such as read_file_with_content.
`
}

const applyDiffToolDescription = `
Expand Down
7 changes: 7 additions & 0 deletions src/core/prompts/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { getGenerateImageDescription } from "./generate-image"
import { getCheckPastChatMemoriesDescription } from "./check-past-chat-memories"
import { getUseSkillDescription } from "./use-skill"
import { CodeIndexManager } from "../../../services/code-index/manager"
import { discoverSkills } from "../../tools/skills"

// kilocode_change start: Morph fast apply
import { isFastApplyAvailable } from "../../tools/editFileTool"
Expand Down Expand Up @@ -177,6 +178,12 @@ export async function getToolDescriptionsForMode(
tools.delete("run_slash_command")
}

// Conditionally exclude use_skill if no skills are available
const skills = await discoverSkills({ workspacePath: cwd })
if (skills.length === 0) {
tools.delete("use_skill")
}

// Map tool descriptions for allowed tools
const descriptions = await Promise.all(
Array.from(tools).map(async (toolName) => {
Expand Down
10 changes: 9 additions & 1 deletion src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3289,7 +3289,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
// kilocode_change end
// note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely.
if (autoApprovalEnabled && alwaysApproveResubmit) {

// Check if this is a 5xx error - always show retry dialog for server errors
const isServerError = error.status && error.status >= 500 && error.status < 600

if (autoApprovalEnabled && alwaysApproveResubmit && !isServerError) {
let errorMsg

if (error.error?.metadata?.raw) {
Expand Down Expand Up @@ -3403,7 +3407,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
// kilocode_change end

// Check if this is a 5xx error - always show retry dialog for server errors
const isServerError = error.status && Number(error.status) >= 500 && Number(error.status) < 600

// For mid-stream failures, show the retry dialog to allow user to retry
// Always show retry dialog for 5xx server errors
const { response } = await this.ask(
"api_req_failed",
error.message ?? JSON.stringify(serializeError(error), null, 2),
Expand Down
2 changes: 1 addition & 1 deletion src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "%extension.displayName%",
"description": "%extension.description%",
"publisher": "matterai",
"version": "5.3.0",
"version": "5.3.1",
"icon": "assets/icons/matterai-ic.png",
"galleryBanner": {
"color": "#FFFFFF",
Expand Down
14 changes: 14 additions & 0 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1740,6 +1740,20 @@ export const ChatRowContent = ({
switch (message.ask) {
case "mistake_limit_reached":
return <ErrorRow type="mistake_limit" message={message.text || ""} />
case "api_req_failed":
return (
<CommandExecution
executionId={message.ts.toString()}
text={message.text}
icon={icon}
title={title}
onPrimaryButtonClick={onPrimaryButtonClick}
onSecondaryButtonClick={onSecondaryButtonClick}
enableButtons={enableButtons}
primaryButtonText={primaryButtonText}
secondaryButtonText={secondaryButtonText}
/>
)
case "command":
return (
<CommandExecution
Expand Down
34 changes: 25 additions & 9 deletions webview-ui/src/components/chat/ErrorRow.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { useState, useCallback, memo, useRef, useEffect } from "react"
import { useTranslation } from "react-i18next"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { XCircle, Info } from "lucide-react"
import { useCopyToClipboard } from "@src/utils/clipboard"
import { vscode } from "@src/utils/vscode"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { Info } from "lucide-react"
import React, { memo, useCallback, useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import CodeBlock from "../kilocode/common/CodeBlock" // kilocode_change

export interface ErrorRowProps {
Expand Down Expand Up @@ -125,7 +126,6 @@ export const ErrorRow = memo(
}`}
onClick={handleToggleExpand}>
<div className="flex items-center gap-2 flex-grow">
<XCircle className="w-3 h-3 text-vscode-foreground opacity-50" />
<span className="font-bold">{errorTitle}</span>
</div>
<div className="flex items-center">
Expand Down Expand Up @@ -154,11 +154,8 @@ export const ErrorRow = memo(
<div className="relative my-1">
<div
className={
headerClassName || "flex items-center gap-2 py-1.5 px-2 rounded-md bg-vscode-editor-background"
headerClassName || "flex items-center gap-2 py-2 px-2 rounded-md bg-vscode-editor-background"
}>
{/* Error Icon */}
<XCircle className="w-4 h-4 flex-shrink-0 text-vscode-foreground opacity-50" />

{/* Error Title */}
{errorTitle && (
<span className="text-vscode-editor-foreground font-medium text-sm whitespace-nowrap">
Expand Down Expand Up @@ -219,6 +216,25 @@ export const ErrorRow = memo(
</div>
)}

{/* Retry Button - outside tooltip, below error row */}
{type === "api_failure" && message?.includes("Provider error:") && (
<div className="mt-1 flex justify-start">
<VSCodeButton
appearance="secondary"
className="p-0"
onClick={() => {
// This will be handled by the parent component
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
})
}}>
<span className="codicon codicon-refresh mr-1" />
{t("chat:retry.title")}
</VSCodeButton>
</div>
)}
Comment on lines +220 to +236
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Functional Issue

Issue: The Retry button sends an askResponse (yesButtonClicked), which requires an active ask promise on the extension backend to function. However, ChatRow.tsx renders CommandExecution (not ErrorRow) for api_req_failed tasks. If ErrorRow is displayed, there is likely no active ask waiting for this response, making this button non-functional.

Fix: Remove the retry button from ErrorRow as the retry logic for API failures is handled by CommandExecution in ChatRow.tsx.

Impact: Prevents confusing UI elements that do not work.

Suggested change
{type === "api_failure" && message?.includes("Provider error:") && (
<div className="mt-1 flex justify-start">
<VSCodeButton
appearance="secondary"
className="p-0"
onClick={() => {
// This will be handled by the parent component
vscode.postMessage({
type: "askResponse",
askResponse: "yesButtonClicked",
})
}}>
<span className="codicon codicon-refresh mr-1" />
{t("chat:retry.title")}
</VSCodeButton>
</div>
)}
{/* Retry Button - removed as it requires an active ask context handled by CommandExecution */}


<style>{`
@keyframes fadeIn {
from {
Expand Down
Loading