Skip to content

Conversation

@code-crusher
Copy link
Member

Release v5.0.3 with the following changes:

  • Update changelog
  • Fetch and show chat title
  • Headers for repo
  • Context window usage tracking updates
  • Context window usage tracking
  • Notify LLM if files have been changed by user post its own edits
  • Show credits usage on hover
  • On exec cms tool reject, do nothing
  • UX improvements for chat

@matter-code-review
Copy link
Contributor

Context

Summary By MatterAI MatterAI logo

🔄 What Changed

Version 5.0.3 release. Key updates include backend-driven task title fetching, real-time context window usage tracking, and repository URL reporting via custom headers. UI enhancements include a billing/credits hover card and improved chat input stability.

🔍 Impact of the Change

Improves user observability of token consumption and billing status. Enhances backend analytics by providing repository context. Fixes critical chat UX issues related to cursor jumping and formatted text pasting.

📁 Total Files Changed

Click to Expand
File ChangeLog
packages/types/src/history.ts Schema Update - Added title and contextWindowUsage to history item schema.
src/api/index.ts API Interface - Added repo field for workspace tracking.
src/api/providers/kilocode-models.ts Model Metadata - Updated context lengths and pricing for Axon models.
src/api/providers/kilocode-openrouter.ts Header Logic - Implemented X-AXON-REPO header for backend tracking.
src/core/task/Task.ts Task Logic - Integrated title fetching, token tracking, and user-edit notifications.
src/core/task-persistence/taskMetadata.ts Metadata Fetching - Added fetchTaskTitle with retry logic using Axios.
webview-ui/src/components/chat/ChatTextArea.tsx Input Fixes - Resolved cursor jumping and implemented plain-text paste.
webview-ui/src/components/chat/ContextUsageIndicator.tsx New Component - Visual circular progress for token usage.
webview-ui/src/components/kilocode/BottomApiConfig.tsx Billing UI - Added plan and credit details hover card.
src/package.json Version Bump - Updated extension version to 5.0.3.

🧪 Test Added/Recommended

Added

  • Unit tests in kilocode-openrouter.spec.ts for verifying X-AXON-REPO header inclusion in various metadata scenarios.

Recommended

  • Add integration tests for the fetchTaskTitle retry logic to ensure it handles network failures gracefully.
  • Add visual regression tests for the new ContextUsageIndicator and BottomApiConfig hover card.

🔒Security Vulnerabilities

  • N/A. Axios calls use existing Bearer tokens; no new sensitive data exposure detected.

Implementation

  • Implemented fetchTaskTitle in taskMetadata.ts with a retry mechanism to fetch titles once streaming starts.
  • Added contextWindowUsage tracking in Task.ts to monitor token limits dynamically.
  • Created ContextUsageIndicator component using SVG for real-time token usage visualization.
  • Enhanced ChatTextArea with intendedCursorPositionRef to prevent cursor resets during React state updates.
  • Updated KilocodeOpenrouterHandler to report the git repository URL or root folder name via headers.

Screenshots

before after
N/A New token usage circle and billing hover card added

How to Test

  • Start a new task and verify the title updates from "New Task" to a backend-generated title after the first response.
  • Monitor the circular token indicator in the chat input as the conversation grows.
  • Hover over the usage percentage in the footer to view plan and credit details.
  • Paste formatted HTML into the chat input and verify it converts to plain text without breaking the UI.

Get in Touch

Total Score: 4/5

@code-crusher code-crusher merged commit 258ec95 into main Jan 11, 2026
5 of 14 checks passed
Copy link
Contributor

@matter-code-review matter-code-review bot left a comment

Choose a reason for hiding this comment

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

🧪 PR Review is completed: Review of KiloCode integration features. Major focus on replacing heavy dependencies (Axios) with native APIs, fixing a critical cursor positioning bug in the chat input paste logic, and ensuring security in error logging.

Skipped files
  • CHANGELOG.md: Skipped file pattern

@@ -1,5 +1,6 @@
import NodeCache from "node-cache"
import getFolderSize from "get-folder-size"
import axios from "axios" // kilocode_change
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Code Quality

Issue: Importing axios adds a heavy dependency (~30kb) for a single API call, increasing bundle size unnecessarily when native fetch is available in the extension host environment.

Fix: Remove the axios import and use fetch in the implementation.

Impact: Reduces bundle size and dependency complexity

Suggested change
import axios from "axios" // kilocode_change

Comment on lines +33 to +82
export async function fetchTaskTitle(
taskId: string,
kilocodeToken: string,
maxRetries: number = 3,
retryDelayMs: number = 2000,
): Promise<string | null> {
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<TaskTitleResponse>(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
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Performance

Issue: Using axios for a simple GET request adds unnecessary overhead. The retry logic also blocks the thread with await new Promise inside the loop (though acceptable for async tasks, fetch is lighter and standard).

Fix: Refactor to use the native fetch API with AbortController for timeouts.

Impact: Improves performance and removes external dependency

Suggested change
export async function fetchTaskTitle(
taskId: string,
kilocodeToken: string,
maxRetries: number = 3,
retryDelayMs: number = 2000,
): Promise<string | null> {
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<TaskTitleResponse>(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 async function fetchTaskTitle(
taskId: string,
kilocodeToken: string,
maxRetries: number = 3,
retryDelayMs: number = 2000,
): Promise<string | null> {
if (!kilocodeToken) {
return null
}
const url = `https://api.matterai.so/axoncode/meta/${encodeURIComponent(taskId)}`
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 5000)
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${kilocodeToken}`,
},
signal: controller.signal,
})
clearTimeout(timeoutId)
if (response.ok) {
const data = (await response.json()) as TaskTitleResponse
if (data?.title) {
return data.title
}
}
// If 404 or other error, retry if attempts remain
if (attempt < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, retryDelayMs))
}
} catch (error) {
// Log error but continue retrying
if (attempt < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, retryDelayMs))
}
}
}
return null
}

Comment on lines +2223 to +2226
.catch((error: unknown) => {
// Silently fail - title fetching is optional
console.warn("Failed to fetch task title:", error)
})
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Security

Issue: Logging the full error object can expose sensitive information (like the kilocodeToken in headers) if the error originates from a library like Axios that includes request details in the error object.

Fix: Log only the error message or a sanitized version of the error.

Impact: Prevents potential leakage of authentication tokens in logs

Suggested change
.catch((error: unknown) => {
// Silently fail - title fetching is optional
console.warn("Failed to fetch task title:", error)
})
.catch((error: unknown) => {
// Silently fail - title fetching is optional
console.warn("Failed to fetch task title:", error instanceof Error ? error.message : String(error))
})

Comment on lines +514 to 543
// 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
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🟠 Logic Error

Issue: The cursor position calculation cursorPosition + plainText.length is incorrect when pasting replaces selected text. It adds the length to the old cursor position (which might be the start of the selection), ignoring that the selection was deleted. This causes the cursor to jump to an incorrect position.

Fix: Calculate the new cursor position directly from the DOM range after insertion, ensuring accuracy regardless of previous selection state.

Impact: Fixes cursor jumping bugs during paste operations

Suggested change
// 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
}
// 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)
// Calculate new cursor position from DOM
let newCursorPosition = 0
if (textAreaRef.current) {
const preCaretRange = range.cloneRange()
preCaretRange.selectNodeContents(textAreaRef.current)
preCaretRange.setEnd(range.endContainer, range.endOffset)
newCursorPosition = preCaretRange.toString().length
}
setCursorPosition(newCursorPosition)
intendedCursorPositionRef.current = newCursorPosition
}
setShowContextMenu(false)
return
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants