From 87418e35822f12ca87eb1b411702cb54beabd5b0 Mon Sep 17 00:00:00 2001 From: code-crusher Date: Sat, 10 Jan 2026 10:33:27 +0530 Subject: [PATCH 1/9] UX improvements for chat --- src/package.json | 2 +- .../src/components/chat/ChatTextArea.tsx | 162 ++++++++++-------- 2 files changed, 90 insertions(+), 74 deletions(-) diff --git a/src/package.json b/src/package.json index 67a263c11..ff680407c 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "matterai", - "version": "5.0.2", + "version": "5.0.3", "icon": "assets/icons/matterai-ic.png", "galleryBanner": { "color": "#FFFFFF", diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 3d323c379..a1fe45dfb 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -207,7 +207,6 @@ export const ChatTextArea = forwardRef( const [selectedMenuIndex, setSelectedMenuIndex] = useState(-1) const [selectedType, setSelectedType] = useState(null) const [justDeletedSpaceAfterMention, setJustDeletedSpaceAfterMention] = useState(false) - const [intendedCursorPosition, setIntendedCursorPosition] = useState(null) const contextMenuContainerRef = useRef(null) const [isFocused, setIsFocused] = useState(false) const [imageWarning, setImageWarning] = useState(null) // kilocode_change @@ -215,6 +214,7 @@ export const ChatTextArea = forwardRef( // const [isUserInput, setIsUserInput] = useState(false) const isUserInputRef = useRef(false) // Use ref to avoid re-renders + const intendedCursorPositionRef = useRef(null) // Track intended cursor position for synchronous restoration // get the icons base uri on mount useEffect(() => { @@ -222,13 +222,10 @@ export const ChatTextArea = forwardRef( setMaterialIconsBaseUri(w.MATERIAL_ICONS_BASE_URI) }, []) - const applyCursorPosition = useCallback( - (position: number) => { - setCursorPosition(position) - setIntendedCursorPosition(position) - }, - [setCursorPosition, setIntendedCursorPosition], - ) + const applyCursorPosition = useCallback((position: number) => { + setCursorPosition(position) + intendedCursorPositionRef.current = position + }, []) // Use custom hook for prompt history navigation const { handleHistoryNavigation, resetHistoryNavigation, resetOnInputChange } = usePromptHistory({ @@ -330,7 +327,7 @@ export const ChatTextArea = forwardRef( if (lastAtIndex !== -1) { const newValue = beforeCursor.slice(0, lastAtIndex) + afterCursor setInputValue(newValue) - setIntendedCursorPosition(lastAtIndex) + intendedCursorPositionRef.current = lastAtIndex } onSelectImages() @@ -398,14 +395,13 @@ export const ChatTextArea = forwardRef( setInputValue(newValue) const newCursorPosition = newValue.indexOf(" ", mentionIndex + insertValue.length) + 1 setCursorPosition(newCursorPosition) - setIntendedCursorPosition(newCursorPosition) + intendedCursorPositionRef.current = newCursorPosition setTimeout(() => { textAreaRef.current?.focus() }, 0) }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [setInputValue, cursorPosition], + [setInputValue, cursorPosition, inputValue, onSelectImages, setMode], ) // kilocode_change start: pull slash commands from Cline @@ -429,7 +425,7 @@ export const ChatTextArea = forwardRef( setInputValue(newValue) setCursorPosition(newCursorPosition) - setIntendedCursorPosition(newCursorPosition) + intendedCursorPositionRef.current = newCursorPosition setTimeout(() => { textAreaRef.current?.focus() @@ -455,11 +451,48 @@ export const ChatTextArea = forwardRef( setIsFocused(false) }, [isMouseDownOnMenu]) + const toPlainText = useCallback((node: Node, isLastSibling: boolean): string => { + if (node.nodeType === Node.TEXT_NODE) { + return node.textContent || "" + } + + if (node.nodeType === Node.ELEMENT_NODE) { + const el = node as HTMLElement + + if (el.dataset?.mentionValue) { + return el.dataset.mentionValue + } + + if (el.tagName === "BR") { + return "\n" + } + + const children = Array.from(el.childNodes) + const text = children.map((child, idx) => toPlainText(child, idx === children.length - 1)).join("") + + if ((el.tagName === "DIV" || el.tagName === "P") && !isLastSibling) { + return text + "\n" + } + + return text + } + + return "" + }, []) + + const getPlainTextFromInput = useCallback(() => { + if (!textAreaRef.current) return "" + const children = Array.from(textAreaRef.current.childNodes) + return children.map((child, idx) => toPlainText(child, idx === children.length - 1)).join("") + }, [toPlainText]) + const handlePaste = useCallback( async (e: React.ClipboardEvent) => { const items = e.clipboardData.items const pastedText = e.clipboardData.getData("text") + const pastedHtml = e.clipboardData.getData("text/html") + // Check if the pasted content is a URL, add space after so user // can easily delete if they don't want it. const urlRegex = /^\S+:\/\/\S+$/ @@ -471,17 +504,40 @@ export const ChatTextArea = forwardRef( setInputValue(newValue) const newCursorPosition = cursorPosition + trimmedUrl.length + 1 setCursorPosition(newCursorPosition) - setIntendedCursorPosition(newCursorPosition) + intendedCursorPositionRef.current = newCursorPosition setShowContextMenu(false) - // Scroll to new cursor position. - setTimeout(() => { - if (textAreaRef.current) { - textAreaRef.current.blur() - textAreaRef.current.focus() - } - }, 0) + return + } + + // If there's HTML data, paste as plain text to clear formatting + if (pastedHtml && pastedText) { + e.preventDefault() + const plainText = pastedText + + // Insert plain text directly into the DOM to preserve existing formatting + const selection = window.getSelection() + if (selection && selection.rangeCount > 0) { + const range = selection.getRangeAt(0) + const textNode = document.createTextNode(plainText) + range.deleteContents() + range.insertNode(textNode) + + // Move cursor to end of inserted text + range.setStartAfter(textNode) + range.setEndAfter(textNode) + selection.removeAllRanges() + selection.addRange(range) + + // Update state to match the new content + const newValue = getPlainTextFromInput() + setInputValue(newValue) + const newCursorPosition = cursorPosition + plainText.length + setCursorPosition(newCursorPosition) + intendedCursorPositionRef.current = newCursorPosition + } + setShowContextMenu(false) return } @@ -550,6 +606,7 @@ export const ChatTextArea = forwardRef( t, selectedImages.length, // kilocode_change - added selectedImages.length showImageWarning, // kilocode_change - added showImageWarning + getPlainTextFromInput, ], ) @@ -631,41 +688,6 @@ export const ChatTextArea = forwardRef( return 0 }, []) - const toPlainText = useCallback((node: Node, isLastSibling: boolean): string => { - if (node.nodeType === Node.TEXT_NODE) { - return node.textContent || "" - } - - if (node.nodeType === Node.ELEMENT_NODE) { - const el = node as HTMLElement - - if (el.dataset?.mentionValue) { - return el.dataset.mentionValue - } - - if (el.tagName === "BR") { - return "\n" - } - - const children = Array.from(el.childNodes) - const text = children.map((child, idx) => toPlainText(child, idx === children.length - 1)).join("") - - if ((el.tagName === "DIV" || el.tagName === "P") && !isLastSibling) { - return text + "\n" - } - - return text - } - - return "" - }, []) - - const getPlainTextFromInput = useCallback(() => { - if (!textAreaRef.current) return "" - const children = Array.from(textAreaRef.current.childNodes) - return children.map((child, idx) => toPlainText(child, idx === children.length - 1)).join("") - }, [toPlainText]) - const getCaretPosition = useCallback(() => { if (!textAreaRef.current) return 0 const selection = window.getSelection() @@ -783,15 +805,21 @@ export const ChatTextArea = forwardRef( // Only update innerHTML if the change is not from user input // This prevents destroying the selection when user is typing or pressing Enter if (isUserInputRef.current) { - isUserInputRef.current = false // Reset flag + // Reset the flag after checking it + isUserInputRef.current = false return // Skip innerHTML update to preserve selection } const html = valueToHtml(inputValue) if (textAreaRef.current.innerHTML !== html) { textAreaRef.current.innerHTML = html + // Restore cursor position synchronously after innerHTML update + if (intendedCursorPositionRef.current !== null) { + setCaretPosition(intendedCursorPositionRef.current) + intendedCursorPositionRef.current = null + } } - }, [inputValue, valueToHtml]) + }, [inputValue, valueToHtml, setCaretPosition]) const updateCursorPosition = useCallback(() => { setCursorPosition(getCaretPosition()) @@ -960,7 +988,7 @@ export const ChatTextArea = forwardRef( if (newText !== inputValue) { event.preventDefault() setInputValue(newText) - setIntendedCursorPosition(newPosition) + intendedCursorPositionRef.current = newPosition } setJustDeletedSpaceAfterMention(false) @@ -998,16 +1026,6 @@ export const ChatTextArea = forwardRef( ], ) - useLayoutEffect(() => { - if (intendedCursorPosition !== null) { - // Use setTimeout to ensure this runs after the DOM is fully updated - setTimeout(() => { - setCaretPosition(intendedCursorPosition) - setIntendedCursorPosition(null) - }, 0) - } - }, [inputValue, intendedCursorPosition, setCaretPosition]) - const searchTimeoutRef = useRef(null) const handleInputChange = useCallback(() => { @@ -1018,7 +1036,7 @@ export const ChatTextArea = forwardRef( const newCursorPosition = getCaretPosition() setCursorPosition(newCursorPosition) - setIntendedCursorPosition(newCursorPosition) + intendedCursorPositionRef.current = newCursorPosition let showMenu = shouldShowContextMenu(newValue, newCursorPosition) const slashMenuVisible = shouldShowSlashCommandsMenu(newValue, newCursorPosition) @@ -1088,7 +1106,6 @@ export const ChatTextArea = forwardRef( resetOnInputChange, setInputValue, setCursorPosition, - setIntendedCursorPosition, setShowSlashCommandsMenu, setShowContextMenu, setSlashCommandsQuery, @@ -1217,7 +1234,7 @@ export const ChatTextArea = forwardRef( setInputValue(newValue) const newCursorPosition = cursorPosition + totalLength + 1 setCursorPosition(newCursorPosition) - setIntendedCursorPosition(newCursorPosition) + intendedCursorPositionRef.current = newCursorPosition } return @@ -1286,7 +1303,6 @@ export const ChatTextArea = forwardRef( inputValue, setInputValue, setCursorPosition, - setIntendedCursorPosition, shouldDisableImages, setSelectedImages, t, From ff8743b37f1f7fc906039adddc48235a098c9734 Mon Sep 17 00:00:00 2001 From: code-crusher Date: Sat, 10 Jan 2026 10:51:55 +0530 Subject: [PATCH 2/9] on exec cms tool reject, do nothing --- .../presentAssistantMessage.ts | 58 +++++++++---------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 4694240a8..334702748 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -1,50 +1,50 @@ import cloneDeep from "clone-deep" import { serializeError } from "serialize-error" -import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" +import type { ClineAsk, ToolName, ToolProgressStatus } from "@roo-code/types" import { defaultModeSlug, getModeBySlug } from "../../shared/modes" import type { ToolParamName, ToolResponse } from "../../shared/tools" +import { shouldUseSingleFileRead } from "@roo-code/types" +import { accessMcpResourceTool } from "../tools/accessMcpResourceTool" +import { askFollowupQuestionTool } from "../tools/askFollowupQuestionTool" +import { attemptCompletionTool } from "../tools/attemptCompletionTool" +import { browserActionTool } from "../tools/browserActionTool" +import { editFileTool } from "../tools/editFileTool" // kilocode_change: Morph fast apply +import { executeCommandTool } from "../tools/executeCommandTool" import { fetchInstructionsTool } from "../tools/fetchInstructionsTool" +import { fileEditTool } from "../tools/fileEditTool" +import { insertContentTool } from "../tools/insertContentTool" +import { listCodeDefinitionNamesTool } from "../tools/listCodeDefinitionNamesTool" import { listFilesTool } from "../tools/listFilesTool" -import { getReadFileToolDescription, readFileTool } from "../tools/readFileTool" -import { getSimpleReadFileToolDescription, simpleReadFileTool } from "../tools/simpleReadFileTool" -import { shouldUseSingleFileRead } from "@roo-code/types" -import { writeToFileTool } from "../tools/writeToFileTool" import { applyDiffTool } from "../tools/multiApplyDiffTool" -import { insertContentTool } from "../tools/insertContentTool" +import { newTaskTool } from "../tools/newTaskTool" +import { getReadFileToolDescription, readFileTool } from "../tools/readFileTool" import { searchAndReplaceTool } from "../tools/searchAndReplaceTool" -import { fileEditTool } from "../tools/fileEditTool" -import { editFileTool } from "../tools/editFileTool" // kilocode_change: Morph fast apply -import { listCodeDefinitionNamesTool } from "../tools/listCodeDefinitionNamesTool" import { searchFilesTool } from "../tools/searchFilesTool" -import { browserActionTool } from "../tools/browserActionTool" -import { executeCommandTool } from "../tools/executeCommandTool" -import { useMcpToolTool } from "../tools/useMcpToolTool" -import { accessMcpResourceTool } from "../tools/accessMcpResourceTool" -import { askFollowupQuestionTool } from "../tools/askFollowupQuestionTool" +import { getSimpleReadFileToolDescription, simpleReadFileTool } from "../tools/simpleReadFileTool" import { switchModeTool } from "../tools/switchModeTool" -import { attemptCompletionTool } from "../tools/attemptCompletionTool" -import { newTaskTool } from "../tools/newTaskTool" +import { useMcpToolTool } from "../tools/useMcpToolTool" +import { writeToFileTool } from "../tools/writeToFileTool" -import { updateTodoListTool } from "../tools/updateTodoListTool" -import { runSlashCommandTool } from "../tools/runSlashCommandTool" import { generateImageTool } from "../tools/generateImageTool" import { planFileEditTool } from "../tools/planFileEditTool" +import { runSlashCommandTool } from "../tools/runSlashCommandTool" +import { updateTodoListTool } from "../tools/updateTodoListTool" +import Anthropic from "@anthropic-ai/sdk" // kilocode_change +import { EXPERIMENT_IDS, experiments } from "../../shared/experiments" +import { yieldPromise } from "../kilocode" import { formatResponse } from "../prompts/responses" -import { validateToolUse } from "../tools/validateToolUse" import { Task } from "../task/Task" +import { applyDiffToolLegacy } from "../tools/applyDiffTool" +import { codebaseSearchTool } from "../tools/codebaseSearchTool" +import { condenseTool } from "../tools/condenseTool" // kilocode_change import { newRuleTool } from "../tools/newRuleTool" // kilocode_change import { reportBugTool } from "../tools/reportBugTool" // kilocode_change -import { condenseTool } from "../tools/condenseTool" // kilocode_change -import { codebaseSearchTool } from "../tools/codebaseSearchTool" -import { experiments, EXPERIMENT_IDS } from "../../shared/experiments" -import { applyDiffToolLegacy } from "../tools/applyDiffTool" -import { yieldPromise } from "../kilocode" -import Anthropic from "@anthropic-ai/sdk" // kilocode_change +import { validateToolUse } from "../tools/validateToolUse" /** * Processes and presents assistant message content to the user interface. @@ -335,13 +335,7 @@ export async function presentAssistantMessage(cline: Task) { ) if (response !== "yesButtonClicked") { - // Handle both messageResponse and noButtonClicked with text. - if (text) { - await cline.say("user_feedback", text, images) - pushToolResult(formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images)) - } else { - pushToolResult(formatResponse.toolDenied()) - } + // On reject, do nothing - just reject cline.didRejectTool = true return false } From c1b1a8c37873ff03b01ba097314d45882c2a507d Mon Sep 17 00:00:00 2001 From: code-crusher Date: Sat, 10 Jan 2026 11:10:16 +0530 Subject: [PATCH 3/9] show credits usage on hover --- src/shared/WebviewMessage.ts | 7 ++ webview-ui/src/components/chat/ChatRow.tsx | 4 +- .../components/kilocode/BottomApiConfig.tsx | 110 ++++++++++++++---- .../kilocode/chat/LowCreditWarning.tsx | 2 +- 4 files changed, 96 insertions(+), 27 deletions(-) diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 67812952e..be7d82d40 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -442,6 +442,13 @@ export type ProfileData = { image: string } organizations?: UserOrganizationWithApiKey[] + // Additional fields from /axoncode/profile endpoint + plan?: string + remainingCredits?: number + remainingReviews?: number + totalCredits?: number + usedCredits?: number + usagePercentage?: number } export interface ProfileDataResponsePayload { diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 6daa079c2..99df5f783 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -1278,7 +1278,7 @@ export const ChatRowContent = ({ // Check if this is the "out of credits" message const isOutOfCreditsMessage = message.text?.includes("Your plan is out of credits") && - message.text?.includes("https://app.matterai.so/usage") + message.text?.includes("https://app.matterai.so/billing") if (isOutOfCreditsMessage) { return ( @@ -1319,7 +1319,7 @@ export const ChatRowContent = ({ e.preventDefault() vscode.postMessage({ type: "openInBrowser", - url: "https://app.matterai.so/usage", + url: "https://app.matterai.so/billing", }) }} style={{ diff --git a/webview-ui/src/components/kilocode/BottomApiConfig.tsx b/webview-ui/src/components/kilocode/BottomApiConfig.tsx index 9938d57b6..5c809e170 100644 --- a/webview-ui/src/components/kilocode/BottomApiConfig.tsx +++ b/webview-ui/src/components/kilocode/BottomApiConfig.tsx @@ -1,15 +1,19 @@ import { useExtensionState } from "@/context/ExtensionStateContext" import { vscode } from "@/utils/vscode" -import { WebviewMessage } from "@roo/WebviewMessage" +import { WebviewMessage, ProfileData } from "@roo/WebviewMessage" import { GaugeCircle } from "lucide-react" import { useEffect, useRef, useState } from "react" +import { createPortal } from "react-dom" import { useSelectedModel } from "../ui/hooks/useSelectedModel" import { ModelSelector } from "./chat/ModelSelector" export const BottomApiConfig = () => { const { currentApiConfigName, apiConfiguration, clineMessages } = useExtensionState() const { id: selectedModelId, provider: selectedProvider } = useSelectedModel(apiConfiguration) - const [usagePercentage, setUsagePercentage] = useState(null) + const [profileData, setProfileData] = useState(null) + const [showHoverCard, setShowHoverCard] = useState(false) + const [cardPosition, setCardPosition] = useState({ top: 0, left: 0 }) + const triggerRef = useRef(null) const [_isLoading, setIsLoading] = useState(false) const previousMessagesRef = useRef("") @@ -27,16 +31,8 @@ export const BottomApiConfig = () => { if (message.type === "profileDataResponse") { const payload = message.payload as any if (payload?.success && payload.data) { - // Extract usage percentage from profile data - // This assumes the API response includes usage metrics as described in the task - const profileData = payload.data as any - if (profileData.usagePercentage !== undefined) { - setUsagePercentage(profileData.usagePercentage) - } else if (profileData.usedCredits !== undefined && profileData.totalCredits !== undefined) { - // Calculate percentage from credits if usagePercentage is not directly provided - const percentage = (profileData.usedCredits / profileData.totalCredits) * 100 - setUsagePercentage(percentage) - } + // Store the full profile data for the hover card + setProfileData(payload.data) } setIsLoading(false) } @@ -81,6 +77,26 @@ export const BottomApiConfig = () => { return null } + // Calculate usage percentage from profile data + const usagePercentage = + profileData?.usagePercentage !== undefined + ? profileData.usagePercentage + : profileData?.usedCredits !== undefined && profileData?.totalCredits !== undefined + ? (profileData.usedCredits / profileData.totalCredits) * 100 + : null + + // Calculate card position when showing + const handleMouseEnter = () => { + if (triggerRef.current) { + const rect = triggerRef.current.getBoundingClientRect() + setCardPosition({ + top: rect.top - 10, + left: rect.left + rect.width / 2, + }) + } + setShowHoverCard(true) + } + return (
{/* kilocode_change - add data-testid="model-selector" below */} @@ -91,18 +107,64 @@ export const BottomApiConfig = () => { fallbackText={`${selectedProvider}:${selectedModelId}`} />
- {apiConfiguration.kilocodeToken && usagePercentage !== null && ( - - - used {usagePercentage}% monthly limit - + {apiConfiguration.kilocodeToken && ( +
setShowHoverCard(false)}> + + + {usagePercentage !== null ? `used ${usagePercentage.toFixed(1)}% monthly limit` : "loading..."} + + {showHoverCard && + createPortal( +
+
+
+
+ Current Plan +
+ +
+ {profileData?.plan} +
+
+
+
+ Monthly Credits +
+
+ ${(profileData?.remainingCredits || 0).toFixed(1)} / $ + {(profileData?.totalCredits || 0).toFixed(1)} credits +
+
+
+
+ Monthly Reviews +
+
+ {(profileData?.remainingReviews || 0).toFixed(1)} reviews remaining +
+
+
+
, + document.body, + )} +
)} ) diff --git a/webview-ui/src/components/kilocode/chat/LowCreditWarning.tsx b/webview-ui/src/components/kilocode/chat/LowCreditWarning.tsx index b6028b64f..f5458be00 100644 --- a/webview-ui/src/components/kilocode/chat/LowCreditWarning.tsx +++ b/webview-ui/src/components/kilocode/chat/LowCreditWarning.tsx @@ -75,7 +75,7 @@ export const LowCreditWarning = ({ vscode.postMessage({ type: "openInBrowser", - url: "https://app.matterai.so/usage", + url: "https://app.matterai.so/billing", }) }}> {t("kilocode:lowCreditWarning.addCredit")} From ae342d5341338ffb5886b16ddaeb9ac5b7e6bdd0 Mon Sep 17 00:00:00 2001 From: code-crusher Date: Sat, 10 Jan 2026 11:22:36 +0530 Subject: [PATCH 4/9] notify llm if the files have been changed by the user post its own edits --- src/core/task/Task.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 9b48963e7..523c1956e 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -2037,16 +2037,26 @@ export class Task extends EventEmitter implements TaskLike { } // kilocode_change end + // Check for files that were modified by the user after the assistant's last edit + // and inject a notification to inform the LLM about these changes + const recentlyModifiedFiles = this.fileContextTracker.getAndClearRecentlyModifiedFiles() + let finalUserContent: Anthropic.Messages.ContentBlockParam[] + if (recentlyModifiedFiles.length > 0) { + // Build a notification message listing the modified files + const fileList = recentlyModifiedFiles.map((f) => ` - ${f}`).join("\n") + const notification = `The following file(s) have been modified by the user since your last edit:\n${fileList}\n\nPlease use read_file to get the latest content of these files before proceeding further to ensure you're working with the most up-to-date information.` + // Inject the notification as a separate text block before the user content + finalUserContent = [{ type: "text" as const, text: notification }, ...parsedUserContent] + } else { + finalUserContent = parsedUserContent + } + // Only add environment details on the first iteration (when includeFileDetails is true) // For subsequent iterations with tool results, don't add environment details to avoid duplication - let finalUserContent: Anthropic.Messages.ContentBlockParam[] if (currentIncludeFileDetails) { const environmentDetails = await getEnvironmentDetails(this, currentIncludeFileDetails) // Add environment details as its own text block, separate from tool results - finalUserContent = [...parsedUserContent, { type: "text" as const, text: environmentDetails }] - } else { - // For tool results, don't add environment details - finalUserContent = parsedUserContent + finalUserContent = [...finalUserContent, { type: "text" as const, text: environmentDetails }] } await this.addToApiConversationHistory({ role: "user", content: finalUserContent }) From 94115983cb08bd638c145c54e4be331d090929f6 Mon Sep 17 00:00:00 2001 From: code-crusher Date: Sat, 10 Jan 2026 12:44:21 +0530 Subject: [PATCH 5/9] context window usage tracking --- src/api/providers/kilocode-models.ts | 12 +-- src/core/task/Task.ts | 16 ++++ src/core/webview/ClineProvider.ts | 1 + src/shared/ExtensionMessage.ts | 5 ++ .../src/components/chat/ChatTextArea.tsx | 11 ++- .../components/chat/ContextUsageIndicator.tsx | 73 +++++++++++++++++++ .../src/components/common/MarkdownBlock.tsx | 4 +- .../ui/hooks/useOpenRouterModelProviders.ts | 12 +-- .../src/context/ExtensionStateContext.tsx | 4 + 9 files changed, 120 insertions(+), 18 deletions(-) create mode 100644 webview-ui/src/components/chat/ContextUsageIndicator.tsx diff --git a/src/api/providers/kilocode-models.ts b/src/api/providers/kilocode-models.ts index e1a14a727..2dbca0774 100644 --- a/src/api/providers/kilocode-models.ts +++ b/src/api/providers/kilocode-models.ts @@ -32,7 +32,7 @@ export const KILO_CODE_MODELS: Record = { name: "Axon Code", description: "Axon Code is super intelligent LLM model for coding tasks", input_modalities: ["text"], - context_length: 256000, + context_length: 200000, max_output_length: 32768, output_modalities: ["text"], supported_sampling_parameters: [ @@ -67,7 +67,7 @@ export const KILO_CODE_MODELS: Record = { description: "Axon Mini is an general purpose super intelligent LLM coding model for low-effort day-to-day tasks", input_modalities: ["text"], - context_length: 256000, + context_length: 200000, max_output_length: 16384, output_modalities: ["text"], supported_sampling_parameters: [ @@ -102,8 +102,8 @@ export const KILO_CODE_MODELS: Record = { description: "Axon Code 2 is the next-generation of Axon Code for coding tasks, currently in experimental stage.", input_modalities: ["text"], - context_length: 256000, - max_output_length: 32768, + context_length: 200000, + max_output_length: 64000, output_modalities: ["text"], supported_sampling_parameters: [ "temperature", @@ -123,8 +123,8 @@ export const KILO_CODE_MODELS: Record = { created: 1750426201, owned_by: "matterai", pricing: { - prompt: "0.000001", - completion: "0.000004", + prompt: "0.0000012", + completion: "0.0000048", image: "0", request: "0", input_cache_reads: "0", diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 523c1956e..6df003c2d 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -271,6 +271,12 @@ export class Task extends EventEmitter implements TaskLike { apiConversationHistory: ApiMessage[] = [] clineMessages: ClineMessage[] = [] + // Context Window Usage Tracking + contextWindowUsage?: { + currentTokens: number + maxTokens: number + } // kilocode_change: Track context window usage + // Ask private askResponse?: ClineAskResponse private askResponseText?: string @@ -2355,6 +2361,16 @@ export class Task extends EventEmitter implements TaskLike { cacheReadTokens = tokens.cacheRead totalCost = tokens.total + // kilocode_change: Update context window usage tracking + const modelInfo = this.api.getModel().info + const maxTokens = modelInfo.contextWindow || 256000 + const currentTokens = + tokens.input + tokens.output + tokens.cacheWrite + tokens.cacheRead + this.contextWindowUsage = { + currentTokens, + maxTokens, + } + // Update the API request message with the latest usage data updateApiReqMsg() await this.saveClineMessages() diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 470df102c..13e796721 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2140,6 +2140,7 @@ ${prompt} openRouterUseMiddleOutTransform, featureRoomoteControlEnabled, codeReviewSettings, + contextWindowUsage: this.getCurrentTask()?.contextWindowUsage, // kilocode_change: Track context window usage } } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 0687b61b7..cd09ae35a 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -392,6 +392,11 @@ export type ExtensionState = Pick< writeDelayMs: number requestDelaySeconds: number + contextWindowUsage?: { + currentTokens: number + maxTokens: number + } // kilocode_change: Track context window usage + enableCheckpoints: boolean maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500) maxWorkspaceFiles: number // Maximum number of files to include in current working directory details (0-500) diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index a1fe45dfb..0a7723d3f 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -32,6 +32,7 @@ import { ImageWarningBanner } from "./ImageWarningBanner" // kilocode_change import { IndexingStatusBadge } from "./IndexingStatusBadge" import { usePromptHistory } from "./hooks/usePromptHistory" import { AcceptRejectButtons } from "./kilocode/AcceptRejectButtons" +import { ContextUsageIndicator } from "./ContextUsageIndicator" // kilocode_change // kilocode_change start: pull slash commands from Cline import SlashCommandMenu from "@/components/chat/SlashCommandMenu" @@ -1557,7 +1558,12 @@ export const ChatTextArea = forwardRef( {/* kilocode_change: position tweaked, rtl support */}
{/* kilocode_change start */} - {!isEditMode && } + {!isEditMode && ( + <> + + + + )}
+ }> +
+ + {/* Background circle */} + + {/* Progress circle */} + + +
+ + ) +} diff --git a/webview-ui/src/components/common/MarkdownBlock.tsx b/webview-ui/src/components/common/MarkdownBlock.tsx index c1e51805c..4c51af19d 100644 --- a/webview-ui/src/components/common/MarkdownBlock.tsx +++ b/webview-ui/src/components/common/MarkdownBlock.tsx @@ -44,8 +44,8 @@ const StyledMarkdown = styled.div` font-family: var(--vscode-editor-font-family, monospace); font-size: 0.85em; filter: saturation(110%) brightness(95%); - color: #ffffff !important; - background-color: black !important; + color: #c4fdff !important; + background-color: #c4fdff20 !important; padding: 1px 2px; white-space: pre-line; word-break: break-word; diff --git a/webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts b/webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts index 8355549f2..c6b45b457 100644 --- a/webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts +++ b/webview-ui/src/components/ui/hooks/useOpenRouterModelProviders.ts @@ -43,7 +43,7 @@ const KILO_CODE_MODELS: Record = { name: "Axon Code", description: "Axon Code is super intelligent LLM model for coding tasks", input_modalities: ["text"], - context_length: 256000, + context_length: 200000, max_output_length: 32768, output_modalities: ["text"], supported_sampling_parameters: [ @@ -78,8 +78,8 @@ const KILO_CODE_MODELS: Record = { description: "Axon Code 2 is the next-generation of Axon Code for coding tasks, currently in experimental stage.", input_modalities: ["text"], - context_length: 256000, - max_output_length: 32768, + context_length: 200000, + max_output_length: 64000, output_modalities: ["text"], supported_sampling_parameters: [ "temperature", @@ -99,8 +99,8 @@ const KILO_CODE_MODELS: Record = { created: 1750426201, owned_by: "matterai", pricing: { - prompt: "0.000001", - completion: "0.000004", + prompt: "0.0000012", + completion: "0.0000048", image: "0", request: "0", input_cache_reads: "0", @@ -113,7 +113,7 @@ const KILO_CODE_MODELS: Record = { description: "Axon Mini is an general purpose super intelligent LLM coding model for low-effort day-to-day tasks", input_modalities: ["text"], - context_length: 256000, + context_length: 200000, max_output_length: 16384, output_modalities: ["text"], supported_sampling_parameters: [ diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 19c6cac1a..7163d8057 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -201,6 +201,10 @@ export interface ExtensionStateContextType extends ExtensionState { enterpriseApiKey?: string reviewOnlyMode?: boolean }) => void + contextWindowUsage?: { + currentTokens: number + maxTokens: number + } // kilocode_change: Track context window usage } export const ExtensionStateContext = createContext(undefined) From 58d9a6a2f09b877db6db47c90f3349e6ce5d752d Mon Sep 17 00:00:00 2001 From: code-crusher Date: Sat, 10 Jan 2026 13:21:06 +0530 Subject: [PATCH 6/9] context window usage tracking updates --- packages/types/src/history.ts | 6 ++++++ src/core/task-persistence/taskMetadata.ts | 14 ++++++++++++++ src/core/task/Task.ts | 12 +++++++++--- src/core/webview/ClineProvider.ts | 9 +++++++++ webview-ui/src/context/ExtensionStateContext.tsx | 10 +++++++++- 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/packages/types/src/history.ts b/packages/types/src/history.ts index afd96e06d..0ac854d30 100644 --- a/packages/types/src/history.ts +++ b/packages/types/src/history.ts @@ -21,6 +21,12 @@ export const historyItemSchema = z.object({ isFavorited: z.boolean().optional(), // kilocode_change fileNotfound: z.boolean().optional(), // kilocode_change mode: z.string().optional(), + contextWindowUsage: z + .object({ + currentTokens: z.number(), + maxTokens: z.number(), + }) + .optional(), }) export type HistoryItem = z.infer diff --git a/src/core/task-persistence/taskMetadata.ts b/src/core/task-persistence/taskMetadata.ts index f6b9575be..91f369c76 100644 --- a/src/core/task-persistence/taskMetadata.ts +++ b/src/core/task-persistence/taskMetadata.ts @@ -21,6 +21,10 @@ export type TaskMetadataOptions = { globalStoragePath: string workspace: string mode?: string + contextWindowUsage?: { + currentTokens: number + maxTokens: number + } } export async function taskMetadata({ @@ -32,6 +36,7 @@ export async function taskMetadata({ globalStoragePath, workspace, mode, + contextWindowUsage, }: TaskMetadataOptions) { const taskDir = await getTaskDirectoryPath(globalStoragePath, id) @@ -101,6 +106,15 @@ export async function taskMetadata({ size: taskDirSize, workspace, mode, + // Use provided contextWindowUsage if available, otherwise calculate from tokenUsage + contextWindowUsage: contextWindowUsage + ? contextWindowUsage + : tokenUsage.contextTokens > 0 + ? { + currentTokens: tokenUsage.contextTokens, + maxTokens: 200000, // Default max tokens for KiloCode models + } + : undefined, } return { historyItem, tokenUsage } diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 6df003c2d..842d6c518 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -275,7 +275,7 @@ export class Task extends EventEmitter implements TaskLike { contextWindowUsage?: { currentTokens: number maxTokens: number - } // kilocode_change: Track context window usage + } // Ask private askResponse?: ClineAskResponse @@ -411,6 +411,8 @@ export class Task extends EventEmitter implements TaskLike { if (historyItem) { this._taskMode = historyItem.mode || defaultModeSlug this.taskModeReady = Promise.resolve() + // Restore context window usage from history + this.contextWindowUsage = historyItem.contextWindowUsage TelemetryService.instance.captureTaskRestarted(this.taskId) } else { // For new tasks, don't set the mode yet - wait for async initialization. @@ -731,6 +733,7 @@ export class Task extends EventEmitter implements TaskLike { globalStoragePath: this.globalStoragePath, workspace: this.cwd, mode: this._taskMode || defaultModeSlug, // Use the task's own mode, not the current provider mode. + contextWindowUsage: this.contextWindowUsage, // Pass current context window usage }) if (hasTokenUsageChanged(tokenUsage, this.tokenUsageSnapshot)) { @@ -1729,6 +1732,9 @@ export class Task extends EventEmitter implements TaskLike { public dispose(): void { console.log(`[Task#dispose] disposing task ${this.taskId}.${this.instanceId}`) + // Reset context window usage + this.contextWindowUsage = undefined + // Dispose message queue and remove event listeners. try { if (this.messageQueueStateChangedHandler) { @@ -2361,9 +2367,9 @@ export class Task extends EventEmitter implements TaskLike { cacheReadTokens = tokens.cacheRead totalCost = tokens.total - // kilocode_change: Update context window usage tracking + // Update context window usage tracking const modelInfo = this.api.getModel().info - const maxTokens = modelInfo.contextWindow || 256000 + const maxTokens = modelInfo.contextWindow || 200000 const currentTokens = tokens.input + tokens.output + tokens.cacheWrite + tokens.cacheRead this.contextWindowUsage = { diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 13e796721..b40cb348d 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -481,6 +481,9 @@ export class ClineProvider // Make sure no reference kept, once promises end it will be // garbage collected. task = undefined + + // Update state to reflect that the task has been removed + await this.postStateToWebview() } } @@ -2706,6 +2709,12 @@ ${prompt} options: CreateTaskOptions = {}, configuration: RooCodeSettings = {}, ): Promise { + // Clear any existing task before creating a new one + if (this.clineStack.length > 0) { + console.log(`[createTask] Clearing existing task before creating new one`) + await this.removeClineFromStack() + } + if (configuration) { await this.setValues(configuration) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 7163d8057..a36506721 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -226,7 +226,15 @@ export const mergeExtensionState = (prevState: ExtensionState, newState: Extensi // Note that we completely replace the previous apiConfiguration and customSupportPrompts objects // with new ones since the state that is broadcast is the entire objects so merging is not necessary. - return { ...rest, apiConfiguration, customModePrompts, customSupportPrompts, experiments } + // Also replace contextWindowUsage when it changes or when currentTaskItem changes + const result = { ...rest, apiConfiguration, customModePrompts, customSupportPrompts, experiments } + + // Explicitly handle contextWindowUsage - replace when new state has it or when task changes + if (newState.contextWindowUsage !== undefined || newState.currentTaskItem !== prevState.currentTaskItem) { + result.contextWindowUsage = newState.contextWindowUsage + } + + return result } export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { From 91a1be16ddcfe94f0a08ac77f414ad6bd90551a0 Mon Sep 17 00:00:00 2001 From: code-crusher Date: Sat, 10 Jan 2026 14:27:19 +0530 Subject: [PATCH 7/9] headers for repo --- src/api/index.ts | 7 +++ .../__tests__/kilocode-openrouter.spec.ts | 63 ++++++++++++++++++- src/api/providers/kilocode-openrouter.ts | 6 ++ src/core/task/Task.ts | 18 ++++++ src/shared/kilocode/headers.ts | 1 + 5 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/api/index.ts b/src/api/index.ts index 029bb50e1..892a19954 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -87,6 +87,13 @@ export interface ApiHandlerCreateMessageMetadata { * @kilocode-only */ projectId?: string + /** + * KiloCode-specific: The git repository URL or root folder name for the current workspace. + * If a git repository, contains the git remote URL. Otherwise, contains the root folder name. + * Used by KiloCodeOpenrouterHandler for backend tracking. Ignored by other providers. + * @kilocode-only + */ + repo?: string // kilocode_change end } diff --git a/src/api/providers/__tests__/kilocode-openrouter.spec.ts b/src/api/providers/__tests__/kilocode-openrouter.spec.ts index 313807d67..329520ec9 100644 --- a/src/api/providers/__tests__/kilocode-openrouter.spec.ts +++ b/src/api/providers/__tests__/kilocode-openrouter.spec.ts @@ -9,7 +9,12 @@ import OpenAI from "openai" import { KilocodeOpenrouterHandler } from "../kilocode-openrouter" import { ApiHandlerOptions } from "../../../shared/api" -import { X_KILOCODE_TASKID, X_KILOCODE_ORGANIZATIONID, X_KILOCODE_PROJECTID } from "../../../shared/kilocode/headers" +import { + X_KILOCODE_TASKID, + X_KILOCODE_ORGANIZATIONID, + X_KILOCODE_PROJECTID, + X_AXON_REPO, +} from "../../../shared/kilocode/headers" // Mock dependencies vitest.mock("openai") @@ -149,6 +154,60 @@ describe("KilocodeOpenrouterHandler", () => { expect(result).toBeUndefined() }) + + it("includes repo header when provided in metadata", () => { + const handler = new KilocodeOpenrouterHandler(mockOptions) + const result = handler.customRequestOptions({ + taskId: "test-task-id", + mode: "code", + repo: "https://github.com/user/repo.git", + }) + + expect(result).toEqual({ + headers: { + [X_KILOCODE_TASKID]: "test-task-id", + [X_AXON_REPO]: "https://github.com/user/repo.git", + }, + }) + }) + + it("includes repo header with folder name when not a git repository", () => { + const handler = new KilocodeOpenrouterHandler(mockOptions) + const result = handler.customRequestOptions({ + taskId: "test-task-id", + mode: "code", + repo: "my-project-folder", + }) + + expect(result).toEqual({ + headers: { + [X_KILOCODE_TASKID]: "test-task-id", + [X_AXON_REPO]: "my-project-folder", + }, + }) + }) + + it("includes all headers including repo when all metadata is provided", () => { + const handler = new KilocodeOpenrouterHandler({ + ...mockOptions, + kilocodeOrganizationId: "test-org-id", + }) + const result = handler.customRequestOptions({ + taskId: "test-task-id", + mode: "code", + projectId: "https://github.com/user/repo.git", + repo: "https://github.com/user/repo.git", + }) + + expect(result).toEqual({ + headers: { + [X_KILOCODE_TASKID]: "test-task-id", + [X_KILOCODE_ORGANIZATIONID]: "test-org-id", + [X_KILOCODE_PROJECTID]: "https://github.com/user/repo.git", + [X_AXON_REPO]: "https://github.com/user/repo.git", + }, + }) + }) }) describe("createMessage", () => { @@ -178,6 +237,7 @@ describe("KilocodeOpenrouterHandler", () => { taskId: "test-task-id", mode: "code", projectId: "https://github.com/user/repo.git", + repo: "https://github.com/user/repo.git", } const generator = handler.createMessage(systemPrompt, messages, metadata) @@ -191,6 +251,7 @@ describe("KilocodeOpenrouterHandler", () => { [X_KILOCODE_TASKID]: "test-task-id", [X_KILOCODE_PROJECTID]: "https://github.com/user/repo.git", [X_KILOCODE_ORGANIZATIONID]: "test-org-id", + [X_AXON_REPO]: "https://github.com/user/repo.git", }, }), ) diff --git a/src/api/providers/kilocode-openrouter.ts b/src/api/providers/kilocode-openrouter.ts index 0566e3e7d..79befefc0 100644 --- a/src/api/providers/kilocode-openrouter.ts +++ b/src/api/providers/kilocode-openrouter.ts @@ -12,6 +12,7 @@ import { X_KILOCODE_TASKID, X_KILOCODE_PROJECTID, X_KILOCODE_TESTER, + X_AXON_REPO, } from "../../shared/kilocode/headers" /** @@ -53,6 +54,11 @@ export class KilocodeOpenrouterHandler extends OpenRouterHandler { } } + // Add X-AXON-REPO header with git repository URL or root folder name + if (metadata?.repo) { + headers[X_AXON_REPO] = metadata.repo + } + // Add X-KILOCODE-TESTER: SUPPRESS header if the setting is enabled if ( kilocodeOptions.kilocodeTesterWarningsDisabledUntil && diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 842d6c518..d573b8f8b 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -76,6 +76,7 @@ import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry" // utils import { calculateApiCostAnthropic } from "../../shared/cost" import { getWorkspacePath } from "../../utils/path" +import { getGitRepositoryInfo } from "../../utils/git" // prompts import { formatResponse } from "../prompts/responses" @@ -3102,6 +3103,21 @@ export class Task extends EventEmitter implements TaskLike { // kilocode_change start // Fetch project properties for KiloCode provider tracking const kiloConfig = this.providerRef.deref()?.getKiloConfig() + + // Get git repository URL or root folder name for X-AXON-REPO header + let repo: string | undefined + try { + const gitInfo = await getGitRepositoryInfo(this.workspacePath) + if (gitInfo.repositoryUrl) { + repo = gitInfo.repositoryUrl + } else { + // Not a git repository, use root folder name + repo = path.basename(this.workspacePath) + } + } catch (error) { + // Fallback to root folder name if git info retrieval fails + repo = path.basename(this.workspacePath) + } // kilocode_change end // Check auto-approval limits @@ -3153,6 +3169,8 @@ export class Task extends EventEmitter implements TaskLike { // kilocode_change start // KiloCode-specific: pass projectId for backend tracking (ignored by other providers) projectId: (await kiloConfig)?.project?.id, + // KiloCode-specific: pass git repository URL or root folder name for backend tracking + repo, // kilocode_change end } diff --git a/src/shared/kilocode/headers.ts b/src/shared/kilocode/headers.ts index 595b4004b..5cddeabeb 100644 --- a/src/shared/kilocode/headers.ts +++ b/src/shared/kilocode/headers.ts @@ -3,3 +3,4 @@ export const X_KILOCODE_ORGANIZATIONID = "X-KiloCode-OrganizationId" export const X_KILOCODE_TASKID = "X-AxonCode-TaskId" export const X_KILOCODE_PROJECTID = "X-KiloCode-ProjectId" export const X_KILOCODE_TESTER = "X-KILOCODE-TESTER" +export const X_AXON_REPO = "X-AXON-REPO" From 66141b95f4b3b74119cf2cbf000d12cb1f3af6d6 Mon Sep 17 00:00:00 2001 From: code-crusher Date: Sun, 11 Jan 2026 15:32:12 +0530 Subject: [PATCH 8/9] fetch and show chat title --- packages/types/src/history.ts | 1 + src/core/task-persistence/index.ts | 2 +- src/core/task-persistence/taskMetadata.ts | 70 +++++++++++++++++++ src/core/task/Task.ts | 27 +++++++ webview-ui/src/components/chat/ChatView.tsx | 1 + .../src/components/history/TaskItem.tsx | 2 +- .../src/components/history/TaskItemFooter.tsx | 2 +- .../components/kilocode/KiloTaskHeader.tsx | 17 ++++- 8 files changed, 118 insertions(+), 4 deletions(-) diff --git a/packages/types/src/history.ts b/packages/types/src/history.ts index 0ac854d30..647d8b9eb 100644 --- a/packages/types/src/history.ts +++ b/packages/types/src/history.ts @@ -21,6 +21,7 @@ export const historyItemSchema = z.object({ isFavorited: z.boolean().optional(), // kilocode_change fileNotfound: z.boolean().optional(), // kilocode_change mode: z.string().optional(), + title: z.string().optional(), // kilocode_change: Task title from backend contextWindowUsage: z .object({ currentTokens: z.number(), diff --git a/src/core/task-persistence/index.ts b/src/core/task-persistence/index.ts index c8656002b..160c40447 100644 --- a/src/core/task-persistence/index.ts +++ b/src/core/task-persistence/index.ts @@ -1,3 +1,3 @@ export { type ApiMessage, readApiMessages, saveApiMessages } from "./apiMessages" export { readTaskMessages, saveTaskMessages } from "./taskMessages" -export { taskMetadata } from "./taskMetadata" +export { taskMetadata, fetchTaskTitle } from "./taskMetadata" diff --git a/src/core/task-persistence/taskMetadata.ts b/src/core/task-persistence/taskMetadata.ts index 91f369c76..360ac6768 100644 --- a/src/core/task-persistence/taskMetadata.ts +++ b/src/core/task-persistence/taskMetadata.ts @@ -1,5 +1,6 @@ import NodeCache from "node-cache" import getFolderSize from "get-folder-size" +import axios from "axios" // kilocode_change import type { ClineMessage, HistoryItem } from "@roo-code/types" @@ -9,9 +10,77 @@ import { getApiMetrics } from "../../shared/getApiMetrics" import { findLastIndex } from "../../shared/array" import { getTaskDirectoryPath } from "../../utils/storage" import { t } from "../../i18n" +import { getKiloUrlFromToken } from "@roo-code/types" // kilocode_change const taskSizeCache = new NodeCache({ stdTTL: 30, checkperiod: 5 * 60 }) +// kilocode_change: Fetch task title from backend +export interface TaskTitleResponse { + taskId: string + title: string +} + +/** + * Fetch task title from backend with retry logic. + * The title is only available after the first response streaming starts. + * + * @param taskId - The task ID to fetch title for + * @param kilocodeToken - The KiloCode authentication token + * @param maxRetries - Maximum number of retry attempts (default: 3) + * @param retryDelayMs - Delay between retries in milliseconds (default: 2000) + * @returns Promise resolving to the task title or null if not found + */ +export async function fetchTaskTitle( + taskId: string, + kilocodeToken: string, + maxRetries: number = 3, + retryDelayMs: number = 2000, +): Promise { + if (!kilocodeToken) { + return null + } + + const url = `https://api.matterai.so/axoncode/meta/${taskId}` + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const response = await axios.get(url, { + headers: { + Authorization: `Bearer ${kilocodeToken}`, + }, + timeout: 5000, // 5 second timeout + }) + + if (response.data?.title) { + return response.data.title + } + + // If we got a response but no title, retry + if (attempt < maxRetries) { + await new Promise((resolve) => setTimeout(resolve, retryDelayMs)) + } + } catch (error) { + // Log error but continue retrying + if (axios.isAxiosError(error)) { + if (error.response?.status === 404) { + // Task not found or title not yet available + if (attempt < maxRetries) { + await new Promise((resolve) => setTimeout(resolve, retryDelayMs)) + continue + } + } + } + + // For other errors, log and continue retrying + if (attempt < maxRetries) { + await new Promise((resolve) => setTimeout(resolve, retryDelayMs)) + } + } + } + + return null +} + export type TaskMetadataOptions = { taskId: string rootTaskId?: string @@ -106,6 +175,7 @@ export async function taskMetadata({ size: taskDirSize, workspace, mode, + title: (taskMessage as any)?.title, // kilocode_change: Include title if available // Use provided contextWindowUsage if available, otherwise calculate from tokenUsage contextWindowUsage: contextWindowUsage ? contextWindowUsage diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index d573b8f8b..551056553 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -102,6 +102,7 @@ import { readTaskMessages, saveTaskMessages, taskMetadata, + fetchTaskTitle, // kilocode_change } from "../task-persistence" import { getEnvironmentDetails } from "../environment/getEnvironmentDetails" import { checkContextWindowExceededError } from "../context/context-management/context-error-handling" @@ -2188,6 +2189,9 @@ export class Task extends EventEmitter implements TaskLike { let pendingGroundingSources: GroundingSource[] = [] this.isStreaming = true + // kilocode_change: Track if we've started fetching the title + let hasStartedTitleFetch = false + try { const iterator = stream[Symbol.asyncIterator]() let item = await iterator.next() @@ -2200,6 +2204,29 @@ export class Task extends EventEmitter implements TaskLike { continue } + // kilocode_change: Fetch task title after first chunk is received + if (!hasStartedTitleFetch) { + hasStartedTitleFetch = true + // Start title fetching in background without blocking the stream + const state = await this.providerRef.deref()?.getState() + const kilocodeToken = state?.apiConfiguration?.kilocodeToken + if (kilocodeToken) { + fetchTaskTitle(this.taskId, kilocodeToken, 3, 2000) + .then(async (title: string | null) => { + if (title && this.clineMessages.length > 0) { + // Update the first message with the title + const firstMessage = this.clineMessages[0] + ;(firstMessage as any).title = title + await this.saveClineMessages() + } + }) + .catch((error: unknown) => { + // Silently fail - title fetching is optional + console.warn("Failed to fetch task title:", error) + }) + } + } + switch (chunk.type) { case "reasoning": { reasoningMessage += chunk.text diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 5719f2273..f487bd63b 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -2153,6 +2153,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction {/* kilocode_change start */} diff --git a/webview-ui/src/components/history/TaskItem.tsx b/webview-ui/src/components/history/TaskItem.tsx index 6630a170d..d01132c21 100644 --- a/webview-ui/src/components/history/TaskItem.tsx +++ b/webview-ui/src/components/history/TaskItem.tsx @@ -74,7 +74,7 @@ const TaskItem = ({ {/* Task text */} = ({ {/* Action Buttons for non-compact view */} {!isSelectionMode && (
- + {/* kilocode_change: Use title if available */} {/* */} {/* {variant === "full" && } */} {onDelete && } diff --git a/webview-ui/src/components/kilocode/KiloTaskHeader.tsx b/webview-ui/src/components/kilocode/KiloTaskHeader.tsx index ea180d000..b68523e5d 100644 --- a/webview-ui/src/components/kilocode/KiloTaskHeader.tsx +++ b/webview-ui/src/components/kilocode/KiloTaskHeader.tsx @@ -27,6 +27,7 @@ export interface TaskHeaderProps { onMessageClick?: (index: number) => void isTaskActive?: boolean todos?: any[] + title?: string // kilocode_change: Task title from backend } const KiloTaskHeader = ({ @@ -39,11 +40,12 @@ const KiloTaskHeader = ({ // contextTokens, // buttonsDisabled, // handleCondenseContext, - // onClose, + onClose, // groupedMessages, // onMessageClick, // isTaskActive = false, todos, + title, }: TaskHeaderProps) => { // const { t } = useTranslation() // const { showTaskTimeline } = useExtensionState() @@ -85,6 +87,19 @@ const KiloTaskHeader = ({ position: "relative", zIndex: 1, }}> + {/* kilocode_change: Show title with X button at the top */} + {title && ( +
+
+ {title} +
+ +
+ )}
Date: Sun, 11 Jan 2026 22:07:36 +0530 Subject: [PATCH 9/9] update changelog --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4e8e7ab7..1460d9304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [v5.0.3] - 2026-01-11 + +### Added + +- Context window usage tracking +- Show credits usage on hover +- Fetch and show chat title + +### Changed + +- UX improvements for chat +- Headers for repo + +### Fixed + +- Notify LLM if files have been changed by the user post its own edits +- On exec tool reject, do nothing + +--- + ## [v5.0.2] - 2026-01-09 ### Changed