-
Notifications
You must be signed in to change notification settings - Fork 0
Release v5.0.3 #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Release v5.0.3 #31
Conversation
ContextSummary By MatterAI
🔄 What ChangedVersion 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 ChangeImproves 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 ChangedClick to Expand
🧪 Test Added/RecommendedAdded
Recommended
🔒Security Vulnerabilities
Implementation
Screenshots
How to Test
Get in Touch
Total Score: 4/5 |
There was a problem hiding this 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 | |||
There was a problem hiding this comment.
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
| import axios from "axios" // kilocode_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 | ||
| } |
There was a problem hiding this comment.
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
| 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 | |
| } |
| .catch((error: unknown) => { | ||
| // Silently fail - title fetching is optional | ||
| console.warn("Failed to fetch task title:", error) | ||
| }) |
There was a problem hiding this comment.
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
| .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)) | |
| }) |
| // 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 | ||
| } |
There was a problem hiding this comment.
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
| // 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 | |
| } |
Release v5.0.3 with the following changes: