From 3c6d640af013958c3c92fcc44097c46b9e498f9c Mon Sep 17 00:00:00 2001 From: "matterai-app[bot]" Date: Thu, 6 Aug 2026 15:13:22 +0530 Subject: [PATCH] Release v6.7.6: Happy Eyeballs connection fallback, condense indicator, KiloCode logout, overage banner - Add Happy Eyeballs (RFC 8305) IPv4/IPv6 connection fallback to OpenRouter and KiloCode fetch agents to prevent hangs on broken address-family paths - Add in-progress condense_context indicator for both manual and auto-condense paths in Task.ts and condenseTool.ts - Add KiloCode logout flow: new kilocodeLogout message handler, logout button in KiloCode settings, tab switch to chat/welcome on logout - Add OverageActiveBanner component shown when overage is enabled, replacing the out-of-credits banner in ChatView - Add AxonCodeOverage/AxonCodeOverageUsage types to WebviewMessage - Add 'View My Analytics' link in KiloCode settings - Update validation message to 'You must be logged in to continue' - Bump version to 6.7.6 --- .../__tests__/fetch-with-timeout.spec.ts | 2 + .../providers/__tests__/openrouter.spec.ts | 1 + .../providers/kilocode/fetchWithTimeout.ts | 4 + src/api/providers/openrouter.ts | 23 ++++- src/core/task/Task.ts | 44 +++++++++ src/core/tools/condenseTool.ts | 56 ++++++++++- src/core/webview/webviewMessageHandler.ts | 18 ++++ src/package.json | 2 +- src/shared/WebviewMessage.ts | 19 ++++ webview-ui/src/App.tsx | 8 +- webview-ui/src/components/chat/ChatView.tsx | 11 ++- .../kilocode/chat/OverageActiveBanner.tsx | 93 +++++++++++++++++++ .../kilocode/common/WeeklyResetButton.tsx | 2 +- .../kilocode/settings/providers/KiloCode.tsx | 33 +++++-- .../src/components/settings/ApiOptions.tsx | 2 - webview-ui/src/i18n/locales/en/kilocode.json | 1 + webview-ui/src/i18n/locales/en/settings.json | 2 +- 17 files changed, 301 insertions(+), 20 deletions(-) create mode 100644 webview-ui/src/components/kilocode/chat/OverageActiveBanner.tsx diff --git a/src/api/providers/__tests__/fetch-with-timeout.spec.ts b/src/api/providers/__tests__/fetch-with-timeout.spec.ts index 62cca5f38e..f0781a5b49 100644 --- a/src/api/providers/__tests__/fetch-with-timeout.spec.ts +++ b/src/api/providers/__tests__/fetch-with-timeout.spec.ts @@ -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 diff --git a/src/api/providers/__tests__/openrouter.spec.ts b/src/api/providers/__tests__/openrouter.spec.ts index 28f90c3df3..d2a0c3ca6b 100644 --- a/src/api/providers/__tests__/openrouter.spec.ts +++ b/src/api/providers/__tests__/openrouter.spec.ts @@ -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 }) }) diff --git a/src/api/providers/kilocode/fetchWithTimeout.ts b/src/api/providers/kilocode/fetchWithTimeout.ts index 1ed43f4116..e98ab61585 100644 --- a/src/api/providers/kilocode/fetchWithTimeout.ts +++ b/src/api/providers/kilocode/fetchWithTimeout.ts @@ -6,6 +6,10 @@ export function fetchWithTimeout(timeoutMs: number, headers?: Record { const requestInit: undici.RequestInit = { diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 468f2d35d5..a0904aba9e 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -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" @@ -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 + function stripThinkingTokens(text: string): string { // Remove ... blocks entirely, including nested ones return text.replace(/[\s\S]*?<\/think>/g, "").trim() @@ -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 diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 878e7a0cae..7eaee5b8cd 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1590,6 +1590,17 @@ export class Task extends EventEmitter 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, @@ -1607,6 +1618,16 @@ export class Task extends EventEmitter 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 */, + ) this.say( "condense_context_error", error, @@ -3834,6 +3855,17 @@ export class Task extends EventEmitter 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, @@ -3872,6 +3904,18 @@ export class Task extends EventEmitter 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 } diff --git a/src/core/tools/condenseTool.ts b/src/core/tools/condenseTool.ts index 7c0b80f1a8..706315242d 100644 --- a/src/core/tools/condenseTool.ts +++ b/src/core/tools/condenseTool.ts @@ -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(), @@ -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 } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 30cd88ecce..04bee4ceff 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -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 diff --git a/src/package.json b/src/package.json index b475c35669..fa8b652fb9 100644 --- a/src/package.json +++ b/src/package.json @@ -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", diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 5d531c8e80..8445df408a 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -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 @@ -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 { diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 546dd62923..ca7c7484de 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -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) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 2c37faae27..2d369a8ae9 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -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 @@ -3027,14 +3028,20 @@ const ChatViewComponent: React.ForwardRefRenderFunction )} - {/* 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 && ( )} + {isUsageExhausted && !task && profileData?.overage?.enabled && ( + + )} {/* {!task && (
diff --git a/webview-ui/src/components/kilocode/chat/OverageActiveBanner.tsx b/webview-ui/src/components/kilocode/chat/OverageActiveBanner.tsx new file mode 100644 index 0000000000..552597b831 --- /dev/null +++ b/webview-ui/src/components/kilocode/chat/OverageActiveBanner.tsx @@ -0,0 +1,93 @@ +import { useMemo } from "react" +import { vscode } from "@src/utils/vscode" +import { AxonCodeOverageUsage } from "@roo/WebviewMessage" + +function formatCurrency(value: number): string { + if (!Number.isFinite(value)) return "$0" + return new Intl.NumberFormat(undefined, { + style: "currency", + currency: "USD", + maximumFractionDigits: 2, + }).format(value) +} + +function formatResetDate(iso?: string): string | null { + if (!iso) return null + const date = new Date(iso) + if (Number.isNaN(date.getTime())) return null + try { + return date.toLocaleString(undefined, { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "2-digit", + }) + } catch { + return date.toString() + } +} + +type OverageActiveBannerProps = { + usage?: AxonCodeOverageUsage | null + className?: string +} + +/** + * Shown in place of the out-of-credits banner when overage is enabled for the + * user. The plan keeps running on shared org API credits past the 98% plan + * threshold, so instead of blocking the user we surface that overage is active + * and how much of the monthly overage budget has been spent. + */ +export const OverageActiveBanner = ({ usage, className }: OverageActiveBannerProps) => { + const formattedResetDate = useMemo(() => formatResetDate(usage?.resetsAt), [usage?.resetsAt]) + + const hasBudget = usage && usage.budget !== null && Number.isFinite(usage.budget) + const spent = usage?.spent ?? 0 + const budget = usage?.budget ?? 0 + const percentage = usage?.percentage ?? null + + return ( +
+
+
+
+ Overage is enabled + + Your plan limits have been reached, but overage is keeping Orbital running on API credits. + + {hasBudget && ( + + Overage spend: {formatCurrency(spent)} / {formatCurrency(budget)} + {percentage !== null ? ` (${percentage}%)` : ""} + + )} + {!hasBudget && Number.isFinite(spent) && spent > 0 && ( + + Overage spend this month: {formatCurrency(spent)} + + )} + {formattedResetDate && ( + + Overage budget resets at {formattedResetDate} + + )} +
+
+ +
+
+
+
+ ) +} diff --git a/webview-ui/src/components/kilocode/common/WeeklyResetButton.tsx b/webview-ui/src/components/kilocode/common/WeeklyResetButton.tsx index 119729ef5b..33f166a98a 100644 --- a/webview-ui/src/components/kilocode/common/WeeklyResetButton.tsx +++ b/webview-ui/src/components/kilocode/common/WeeklyResetButton.tsx @@ -41,7 +41,7 @@ export const WeeklyResetButton = ({ plan, availability, isResetting, error, onRe appearance="primary" disabled={!available || isResetting} onClick={onReset} - className="inline-flex w-auto cursor-pointer items-center rounded text-xs h-5 py-0 px-2 leading-4 text-[var(--vscode-button-foreground)] bg-[var(--vscode-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] disabled:cursor-not-allowed disabled:opacity-50"> + className="inline-flex w-auto cursor-pointer items-center rounded text-xs py-0 px-2 leading-4 text-[var(--vscode-button-foreground)] bg-[var(--vscode-button-background)] hover:bg-[var(--vscode-button-hoverBackground)] disabled:cursor-not-allowed disabled:opacity-50"> {isResetting ? "Resetting…" : `Reset Weekly Limit, ${available ? "1/1" : "0/1"} Remaining`} {!available && availability?.nextAvailableAt && ( diff --git a/webview-ui/src/components/kilocode/settings/providers/KiloCode.tsx b/webview-ui/src/components/kilocode/settings/providers/KiloCode.tsx index 6ae09ded43..743fd20540 100644 --- a/webview-ui/src/components/kilocode/settings/providers/KiloCode.tsx +++ b/webview-ui/src/components/kilocode/settings/providers/KiloCode.tsx @@ -10,6 +10,7 @@ import { import type { RouterModels } from "@roo/api" import { ProfileData, WebviewMessage } from "@roo/WebviewMessage" import { MatterProgressIndicator } from "@src/components/chat/ProgressIndicator" +import { Button } from "@src/components/ui" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" import { useAppTranslation } from "@src/i18n/TranslationContext" import { vscode } from "@src/utils/vscode" @@ -40,7 +41,6 @@ function formatRelativeTime(isoStr?: string): string { type KiloCodeProps = { apiConfiguration: ProviderSettings setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void - currentApiConfigName?: string hideKiloCodeButton?: boolean routerModels?: RouterModels organizationAllowList: OrganizationAllowList @@ -53,7 +53,6 @@ type KiloCodeProps = { export const KiloCode = ({ apiConfiguration, setApiConfigurationField, - // currentApiConfigName, hideKiloCodeButton, routerModels, organizationAllowList, @@ -155,6 +154,11 @@ export const KiloCode = ({ vscode.postMessage({ type: "resetWeeklyUsageRequest" }) } + const handleLogout = () => { + // The extension clears the KiloCode credentials and navigates back to the login page + vscode.postMessage({ type: "kilocodeLogout" }) + } + // Always show all models including axon-code-2-pro // The model will be marked as disabled if betaModelsEnabled is false const models = useMemo(() => routerModels?.["kilocode-openrouter"] ?? {}, [routerModels]) @@ -276,13 +280,18 @@ export const KiloCode = ({
) })} - +
+ + {t("kilocode:settings.provider.viewAnalytics")} + + +
)} @@ -319,6 +328,12 @@ export const KiloCode = ({ proModelIds={proModelIds} proModelsEnabled={betaModelsEnabled} /> + + {!hideKiloCodeButton && apiConfiguration.kilocodeToken && ( + + )} ) } diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 9a1c7b53ef..8e5fe34e19 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -46,7 +46,6 @@ const ApiOptions = ({ errorMessage, setErrorMessage, hideKiloCodeButton = false, - currentApiConfigName, // kilocode_change }: ApiOptionsProps) => { // const { t } = useAppTranslation() const { @@ -392,7 +391,6 @@ const ApiOptions = ({ apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} hideKiloCodeButton={hideKiloCodeButton} - currentApiConfigName={currentApiConfigName} routerModels={routerModels} organizationAllowList={organizationAllowList} uriScheme={uriScheme} diff --git a/webview-ui/src/i18n/locales/en/kilocode.json b/webview-ui/src/i18n/locales/en/kilocode.json index dbefbb34d4..a5f556a347 100644 --- a/webview-ui/src/i18n/locales/en/kilocode.json +++ b/webview-ui/src/i18n/locales/en/kilocode.json @@ -65,6 +65,7 @@ "apiKey": "Orbital API Key", "login": "Log in at MatterAI", "logout": "Log Out", + "viewAnalytics": "View My Analytics", "providerRouting": { "title": "Provider Routing", "managedByOrganization": "Manage Organization-level Provider Routing", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 82d73f8d2c..784fa22ddd 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -901,7 +901,7 @@ "maxThinkingTokens": "Max Thinking Tokens" }, "validation": { - "apiKey": "You must provide a valid API key.", + "apiKey": "You must be logged in to continue", "awsRegion": "You must choose a region to use with Amazon Bedrock.", "googleCloud": "You must provide a valid Google Cloud Project ID and Region.", "modelId": "You must provide a valid model ID.",