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
2 changes: 2 additions & 0 deletions src/api/providers/__tests__/fetch-with-timeout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ describe("fetchWithTimeout - header precedence and timeout wiring", () => {
expect(hoisted.mockAgentConstructor).toHaveBeenCalledWith({
headersTimeout: timeoutMs,
bodyTimeout: timeoutMs,
// kilocode_change: Happy Eyeballs — race IPv4/IPv6 on connect
connect: { autoSelectFamily: true, autoSelectFamilyAttemptTimeout: 250 },
})

// Fetch called with merged headers where persistent wins
Expand Down
1 change: 1 addition & 0 deletions src/api/providers/__tests__/openrouter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ describe("OpenRouterHandler", () => {
"X-AxonCode-Version": Package.version,
"User-Agent": `Kilo-Code/${Package.version}`,
},
fetch: expect.any(Function), // kilocode_change: Happy Eyeballs agent
})
})

Expand Down
4 changes: 4 additions & 0 deletions src/api/providers/kilocode/fetchWithTimeout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export function fetchWithTimeout(timeoutMs: number, headers?: Record<string, str
const agent = new undici.EnvHttpProxyAgent({
headersTimeout: timeoutMs,
bodyTimeout: timeoutMs,
// kilocode_change: Happy Eyeballs (RFC 8305) — race IPv4/IPv6 on connect so
// a broken address-family path falls back to the other family instead of
// hanging. undici has no Happy Eyeballs by default, unlike curl.
connect: { autoSelectFamily: true, autoSelectFamilyAttemptTimeout: 250 },
})
return (input, init) => {
const requestInit: undici.RequestInit = {
Expand Down
23 changes: 22 additions & 1 deletion src/api/providers/openrouter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import * as undici from "undici"

import { getActiveToolUseStyle, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@roo-code/types"

Expand Down Expand Up @@ -38,6 +39,19 @@ import { safeJsonParse } from "../../shared/safeJsonParse"

import { handleOpenAIError } from "./utils/openai-error-handler"

// kilocode_change: Happy Eyeballs (RFC 8305) — race IPv4/IPv6 on connect so a
// broken address-family path (e.g. a carrier DNS64-synthesized IPv6 that
// doesn't actually route on a mobile hotspot) falls back to the other family
// instead of hanging. undici has no Happy Eyeballs by default, unlike curl.
const happyEyeballsAgent = new undici.Agent({
connect: { autoSelectFamily: true, autoSelectFamilyAttemptTimeout: 250 },
})
const happyEyeballsFetch: typeof fetch = (input, init) =>
undici.fetch(input as undici.RequestInfo, {
...(init as undici.RequestInit),
dispatcher: happyEyeballsAgent,
}) as unknown as Promise<Response>

function stripThinkingTokens(text: string): string {
// Remove <think>...</think> blocks entirely, including nested ones
return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim()
Expand Down Expand Up @@ -143,7 +157,14 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
const baseURL = this.options.openRouterBaseUrl || "https://api2.matterai.so/v1/web"
const apiKey = this.options.openRouterApiKey ?? "not-provided"

this.client = new OpenAI({ baseURL, apiKey, defaultHeaders: DEFAULT_HEADERS })
this.client = new OpenAI({
baseURL,
apiKey,
defaultHeaders: DEFAULT_HEADERS,
// kilocode_change: route through the Happy Eyeballs agent so connect
// falls back across address families instead of sticking on the first.
fetch: happyEyeballsFetch,
})
}

// forked_change start
Expand Down
44 changes: 44 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1590,6 +1590,17 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {

const { contextTokens: prevContextTokens } = this.getTokenUsage()

// Show an in-progress indicator while the summarization call runs
await this.say(
"condense_context",
undefined /* text */,
undefined /* images */,
true /* partial */,
undefined /* checkpoint */,
undefined /* progressStatus */,
{ isNonInteractive: true } /* options */,
)

const {
messages,
summary,
Expand All @@ -1607,6 +1618,16 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
condensingApiHandler, // Specific handler for condensing
)
if (error) {
// Finalize the in-progress row (renders nothing since there is no result)
await this.say(
"condense_context",
undefined /* text */,
undefined /* images */,
false /* partial */,
undefined /* checkpoint */,
undefined /* progressStatus */,
{ isNonInteractive: true } /* options */,
)
Comment on lines +1621 to +1630

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Error Handling

Issue: In the error path (if (error)), the cleanup await this.say("condense_context", ...) call is not protected against rejection. If this.say throws, the subsequent condense_context_error message at line 1631 will never execute, leaving the user without error feedback. The matching error handler in condenseTool.ts (lines 100-110) correctly uses .catch(() => {}) for this exact cleanup pattern.

Fix: Add .catch(() => {}) to the cleanup say call, matching the defensive pattern used in condenseTool.ts.

Impact: Ensures the condense error is always reported to the user even if the indicator cleanup fails.

Suggested change
// Finalize the in-progress row (renders nothing since there is no result)
await this.say(
"condense_context",
undefined /* text */,
undefined /* images */,
false /* partial */,
undefined /* checkpoint */,
undefined /* progressStatus */,
{ isNonInteractive: true } /* options */,
)
// Finalize the in-progress row (renders nothing since there is no result)
await this.say(
"condense_context",
undefined /* text */,
undefined /* images */,
false /* partial */,
undefined /* checkpoint */,
undefined /* progressStatus */,
{ isNonInteractive: true } /* options */,
).catch(() => {})

this.say(
"condense_context_error",
error,
Expand Down Expand Up @@ -3834,6 +3855,17 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
settings: this.apiConfiguration,
})

// Show an in-progress indicator while the condense/summarize call runs
await this.say(
"condense_context",
undefined /* text */,
undefined /* images */,
true /* partial */,
undefined /* checkpoint */,
undefined /* progressStatus */,
{ isNonInteractive: true } /* options */,
)

const truncateResult = await truncateConversationIfNeeded({
messages: this.apiConversationHistory,
totalTokens: contextTokens || 0,
Expand Down Expand Up @@ -3872,6 +3904,18 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
return true
}

// Condensation did not produce a summary (e.g., too few messages or an
// error). Finalize the in-progress row so the indicator clears.
await this.say(
"condense_context",
undefined /* text */,
undefined /* images */,
false /* partial */,
undefined /* checkpoint */,
undefined /* progressStatus */,
{ isNonInteractive: true } /* options */,
)

return false
}

Expand Down
56 changes: 54 additions & 2 deletions src/core/tools/condenseTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,24 @@ export const condenseTool = async (

const { contextTokens: prevContextTokens } = cline.getTokenUsage()

// Show an in-progress indicator while the summarization call runs
await cline.say(
"condense_context",
undefined /* text */,
undefined /* images */,
true /* partial */,
undefined /* checkpoint */,
undefined /* progressStatus */,
{ isNonInteractive: true } /* options */,
)

// Use summarizeConversation to create a condensed version of the conversation
const summarizedMessages = await summarizeConversation(
const {
messages,
summary,
cost,
newContextTokens = 0,
} = await summarizeConversation(
cline.apiConversationHistory,
cline.api,
await cline.getSystemPrompt(),
Expand All @@ -51,11 +67,47 @@ export const condenseTool = async (
)

// Overwrite the apiConversationHistory with the summarized messages
await cline.overwriteApiConversationHistory(summarizedMessages.messages)
await cline.overwriteApiConversationHistory(messages)

if (summary) {
await cline.say(
"condense_context",
undefined /* text */,
undefined /* images */,
false /* partial */,
undefined /* checkpoint */,
undefined /* progressStatus */,
{ isNonInteractive: true } /* options */,
{ summary, cost, newContextTokens, prevContextTokens },
)
} else {
// Summarization failed; finalize the in-progress row so the indicator clears
await cline.say(
"condense_context",
undefined /* text */,
undefined /* images */,
false /* partial */,
undefined /* checkpoint */,
undefined /* progressStatus */,
{ isNonInteractive: true } /* options */,
)
}
}
return
}
} catch (error) {
// Clear the in-progress indicator if the summarization failed
await cline
.say(
"condense_context",
undefined /* text */,
undefined /* images */,
false /* partial */,
undefined /* checkpoint */,
undefined /* progressStatus */,
{ isNonInteractive: true } /* options */,
)
.catch(() => {})
await handleError("condensing context window", error)
return
}
Expand Down
18 changes: 18 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3343,6 +3343,24 @@ ${comment.suggestion}
}
// forked_change end: check for kilocodeToken change to remove organizationId and fetch organization modes
break
case "kilocodeLogout": {
// kilocode_change: clear KiloCode credentials from the Account settings page and return to the login page
const { apiConfiguration, currentApiConfigName = "default" } = await provider.getState()
await provider.upsertProviderProfile(currentApiConfigName, {
...apiConfiguration,
kilocodeToken: undefined,
kilocodeOrganizationId: undefined,
kilocodeModel: undefined,
})
// Navigate away from settings so the WelcomeView (login) renders once credentials are cleared.
await provider.postMessageToWebview({
type: "action",
action: "switchTab",
tab: "chat",
values: { fromLogout: true },
})
break
}
case "updateTaskModel":
// Task-local model update for isolation
// When there's an active task, only update the task's model to prevent affecting other tasks
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": "6.7.5",
"version": "6.7.6",
"icon": "assets/icons/matterai-ic.png",
"galleryBanner": {
"color": "#FFFFFF",
Expand Down
19 changes: 19 additions & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ export interface WebviewMessage {
| "fetchMarketplaceData"
| "installSuggestedPlugin"
| "switchTab"
| "kilocodeLogout" // kilocode_change: clear KiloCode credentials from the Account settings page
| "profileThresholds"
| "editMessage" // kilocode_change
| "systemNotificationsEnabled" // kilocode_change
Expand Down Expand Up @@ -512,6 +513,24 @@ export type ProfileData = {
// as a fraction of the user's monthly plan limit.
tieredUsage?: AxonCodeTieredUsage
weeklyReset?: AxonCodeWeeklyResetAvailability
// Overage lets the plan keep running on shared org API credits once the
// plan windows hit 98%. `enabled` is true only when the org has turned
// overage on AND this user is covered by the access selection.
overage?: AxonCodeOverage
}

export interface AxonCodeOverageUsage {
spent: number
budget: number | null
remaining: number | null
percentage: number | null
periodStart: string
resetsAt: string
}

export interface AxonCodeOverage {
enabled: boolean
usage?: AxonCodeOverageUsage | null
}

export interface AxonCodeWeeklyResetAvailability {
Expand Down
8 changes: 7 additions & 1 deletion webview-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,13 @@ const App = () => {
// Handle switchTab action with tab parameter
if (message.action === "switchTab" && message.tab) {
const targetTab = message.tab as Tab
switchTab(targetTab)
if (message.values?.fromLogout) {
// Logout flow: credentials were already cleared by the extension, so skip the
// unsaved-changes check (the settings cached state is stale after the account is gone)
setTab(targetTab)
} else {
switchTab(targetTab)
}
// Extract targetSection from values if provided
const targetSection = message.values?.section as string | undefined
setCurrentSection(targetSection)
Expand Down
11 changes: 9 additions & 2 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import { X } from "lucide-react"
import { useOptionalAgentFileViewer } from "../agent/AgentFileViewerContext" // kilocode_change: for agent manager file viewer
import { KilocodeNotifications } from "../kilocode/KilocodeNotifications" // kilocode_change
import { OutOfCreditsBanner } from "../kilocode/chat/OutOfCreditsBanner" // kilocode_change
import { OverageActiveBanner } from "../kilocode/chat/OverageActiveBanner" // kilocode_change
import { CheckpointWarning } from "./CheckpointWarning"
import { QueuedMessages } from "./QueuedMessages"
import { SourceControlPanel } from "./SourceControlPanel" // kilocode_change
Expand Down Expand Up @@ -3027,14 +3028,20 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
</div>
)}

{/* kilocode_change: Show notification when monthly limit is exhausted */}
{isUsageExhausted && !task && (
{/* kilocode_change: Show notification when monthly limit is exhausted.
When overage is enabled for the user, the plan keeps running on API
credits, so we show an "overage active" banner instead of the
out-of-credits banner. */}
{isUsageExhausted && !task && !profileData?.overage?.enabled && (
<OutOfCreditsBanner
className="w-full min-w-0 px-4 mb-4"
creditsResetDate={profileData?.creditsResetDate}
tieredUsage={profileData?.tieredUsage}
/>
)}
{isUsageExhausted && !task && profileData?.overage?.enabled && (
<OverageActiveBanner className="w-full min-w-0 px-4 mb-4" usage={profileData?.overage?.usage} />
)}

{/* {!task && (
<div className={`w-full min-w-0 px-4 ${isReviewOnlyMode ? "mb-4" : "mb-1.5"}`}>
Expand Down
Loading
Loading