Merge remote-tracking branch 'upstream/main' into sbc_add_subtasks

This commit is contained in:
ShayBC 2025-02-27 01:03:44 +02:00
commit f835b8c3e2
7 changed files with 287 additions and 11 deletions

View file

@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Handle really long text in the ChatRow similar to TaskHeader

View file

@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Support multiple files in drag-and-drop

View file

@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Better OpenRouter error handling

View file

@ -1066,7 +1066,7 @@ export class Cline {
} catch (error) {
// note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely.
if (alwaysApproveResubmit) {
const errorMsg = error.message ?? "Unknown error"
const errorMsg = error.error?.metadata?.raw ?? error.message ?? "Unknown error"
const baseDelay = requestDelaySeconds || 5
const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt))
// Wait for the greater of the exponential delay or the rate limit delay

View file

@ -617,8 +617,10 @@ export const ChatRowContent = ({
color: "var(--vscode-badge-foreground)",
borderRadius: "3px",
padding: "9px",
whiteSpace: "pre-line",
wordWrap: "break-word",
overflow: "hidden",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
<div
style={{

View file

@ -590,15 +590,36 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const files = Array.from(e.dataTransfer.files)
const text = e.dataTransfer.getData("text")
if (text) {
// Convert the path to a mention-friendly format
const mentionText = convertToMentionPath(text, cwd)
// Split text on newlines to handle multiple files
const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "")
const newValue =
inputValue.slice(0, cursorPosition) + mentionText + " " + inputValue.slice(cursorPosition)
setInputValue(newValue)
const newCursorPosition = cursorPosition + mentionText.length + 1
setCursorPosition(newCursorPosition)
setIntendedCursorPosition(newCursorPosition)
if (lines.length > 0) {
// Process each line as a separate file path
let newValue = inputValue.slice(0, cursorPosition)
let totalLength = 0
lines.forEach((line, index) => {
// Convert each path to a mention-friendly format
const mentionText = convertToMentionPath(line, cwd)
newValue += mentionText
totalLength += mentionText.length
// Add space after each mention except the last one
if (index < lines.length - 1) {
newValue += " "
totalLength += 1
}
})
// Add space after the last mention and append the rest of the input
newValue += " " + inputValue.slice(cursorPosition)
totalLength += 1
setInputValue(newValue)
const newCursorPosition = cursorPosition + totalLength
setCursorPosition(newCursorPosition)
setIntendedCursorPosition(newCursorPosition)
}
return
}

View file

@ -3,6 +3,7 @@ import ChatTextArea from "../ChatTextArea"
import { useExtensionState } from "../../../context/ExtensionStateContext"
import { vscode } from "../../../utils/vscode"
import { defaultModeSlug } from "../../../../../src/shared/modes"
import * as pathMentions from "../../../utils/path-mentions"
// Mock modules
jest.mock("../../../utils/vscode", () => ({
@ -12,9 +13,20 @@ jest.mock("../../../utils/vscode", () => ({
}))
jest.mock("../../../components/common/CodeBlock")
jest.mock("../../../components/common/MarkdownBlock")
jest.mock("../../../utils/path-mentions", () => ({
convertToMentionPath: jest.fn((path, cwd) => {
// Simple mock implementation that mimics the real function's behavior
if (cwd && path.toLowerCase().startsWith(cwd.toLowerCase())) {
const relativePath = path.substring(cwd.length)
return "@" + (relativePath.startsWith("/") ? relativePath : "/" + relativePath)
}
return path
}),
}))
// Get the mocked postMessage function
const mockPostMessage = vscode.postMessage as jest.Mock
const mockConvertToMentionPath = pathMentions.convertToMentionPath as jest.Mock
// Mock ExtensionStateContext
jest.mock("../../../context/ExtensionStateContext")
@ -160,4 +172,230 @@ describe("ChatTextArea", () => {
expect(setInputValue).toHaveBeenCalledWith("Enhanced test prompt")
})
})
describe("multi-file drag and drop", () => {
const mockCwd = "/Users/test/project"
beforeEach(() => {
jest.clearAllMocks()
;(useExtensionState as jest.Mock).mockReturnValue({
filePaths: [],
openedTabs: [],
cwd: mockCwd,
})
mockConvertToMentionPath.mockClear()
})
it("should process multiple file paths separated by newlines", () => {
const setInputValue = jest.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="Initial text" />,
)
// Create a mock dataTransfer object with text data containing multiple file paths
const dataTransfer = {
getData: jest.fn().mockReturnValue("/Users/test/project/file1.js\n/Users/test/project/file2.js"),
files: [],
}
// Simulate drop event
fireEvent.drop(container.querySelector(".chat-text-area")!, {
dataTransfer,
preventDefault: jest.fn(),
})
// Verify convertToMentionPath was called for each file path
expect(mockConvertToMentionPath).toHaveBeenCalledTimes(2)
expect(mockConvertToMentionPath).toHaveBeenCalledWith("/Users/test/project/file1.js", mockCwd)
expect(mockConvertToMentionPath).toHaveBeenCalledWith("/Users/test/project/file2.js", mockCwd)
// Verify setInputValue was called with the correct value
// The mock implementation of convertToMentionPath will convert the paths to @/file1.js and @/file2.js
expect(setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Initial text")
})
it("should filter out empty lines in the dragged text", () => {
const setInputValue = jest.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="Initial text" />,
)
// Create a mock dataTransfer object with text data containing empty lines
const dataTransfer = {
getData: jest.fn().mockReturnValue("/Users/test/project/file1.js\n\n/Users/test/project/file2.js\n\n"),
files: [],
}
// Simulate drop event
fireEvent.drop(container.querySelector(".chat-text-area")!, {
dataTransfer,
preventDefault: jest.fn(),
})
// Verify convertToMentionPath was called only for non-empty lines
expect(mockConvertToMentionPath).toHaveBeenCalledTimes(2)
// Verify setInputValue was called with the correct value
expect(setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Initial text")
})
it("should correctly update cursor position after adding multiple mentions", () => {
const setInputValue = jest.fn()
const initialCursorPosition = 5
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="Hello world" />,
)
// Set the cursor position manually
const textArea = container.querySelector("textarea")
if (textArea) {
textArea.selectionStart = initialCursorPosition
textArea.selectionEnd = initialCursorPosition
}
// Create a mock dataTransfer object with text data
const dataTransfer = {
getData: jest.fn().mockReturnValue("/Users/test/project/file1.js\n/Users/test/project/file2.js"),
files: [],
}
// Simulate drop event
fireEvent.drop(container.querySelector(".chat-text-area")!, {
dataTransfer,
preventDefault: jest.fn(),
})
// The cursor position should be updated based on the implementation in the component
expect(setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Hello world")
})
it("should handle very long file paths correctly", () => {
const setInputValue = jest.fn()
const { container } = render(<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />)
// Create a very long file path
const longPath =
"/Users/test/project/very/long/path/with/many/nested/directories/and/a/very/long/filename/with/extension.typescript"
// Create a mock dataTransfer object with the long path
const dataTransfer = {
getData: jest.fn().mockReturnValue(longPath),
files: [],
}
// Simulate drop event
fireEvent.drop(container.querySelector(".chat-text-area")!, {
dataTransfer,
preventDefault: jest.fn(),
})
// Verify convertToMentionPath was called with the long path
expect(mockConvertToMentionPath).toHaveBeenCalledWith(longPath, mockCwd)
// The mock implementation will convert it to @/very/long/path/...
expect(setInputValue).toHaveBeenCalledWith(
"@/very/long/path/with/many/nested/directories/and/a/very/long/filename/with/extension.typescript ",
)
})
it("should handle paths with special characters correctly", () => {
const setInputValue = jest.fn()
const { container } = render(<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />)
// Create paths with special characters
const specialPath1 = "/Users/test/project/file with spaces.js"
const specialPath2 = "/Users/test/project/file-with-dashes.js"
const specialPath3 = "/Users/test/project/file_with_underscores.js"
const specialPath4 = "/Users/test/project/file.with.dots.js"
// Create a mock dataTransfer object with the special paths
const dataTransfer = {
getData: jest
.fn()
.mockReturnValue(`${specialPath1}\n${specialPath2}\n${specialPath3}\n${specialPath4}`),
files: [],
}
// Simulate drop event
fireEvent.drop(container.querySelector(".chat-text-area")!, {
dataTransfer,
preventDefault: jest.fn(),
})
// Verify convertToMentionPath was called for each path
expect(mockConvertToMentionPath).toHaveBeenCalledTimes(4)
expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath1, mockCwd)
expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath2, mockCwd)
expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath3, mockCwd)
expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath4, mockCwd)
// Verify setInputValue was called with the correct value
expect(setInputValue).toHaveBeenCalledWith(
"@/file with spaces.js @/file-with-dashes.js @/file_with_underscores.js @/file.with.dots.js ",
)
})
it("should handle paths outside the current working directory", () => {
const setInputValue = jest.fn()
const { container } = render(<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />)
// Create paths outside the current working directory
const outsidePath = "/Users/other/project/file.js"
// Mock the convertToMentionPath function to return the original path for paths outside cwd
mockConvertToMentionPath.mockImplementationOnce((path, cwd) => {
return path // Return original path for this test
})
// Create a mock dataTransfer object with the outside path
const dataTransfer = {
getData: jest.fn().mockReturnValue(outsidePath),
files: [],
}
// Simulate drop event
fireEvent.drop(container.querySelector(".chat-text-area")!, {
dataTransfer,
preventDefault: jest.fn(),
})
// Verify convertToMentionPath was called with the outside path
expect(mockConvertToMentionPath).toHaveBeenCalledWith(outsidePath, mockCwd)
// Verify setInputValue was called with the original path
expect(setInputValue).toHaveBeenCalledWith("/Users/other/project/file.js ")
})
it("should do nothing when dropped text is empty", () => {
const setInputValue = jest.fn()
const { container } = render(
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="Initial text" />,
)
// Create a mock dataTransfer object with empty text
const dataTransfer = {
getData: jest.fn().mockReturnValue(""),
files: [],
}
// Simulate drop event
fireEvent.drop(container.querySelector(".chat-text-area")!, {
dataTransfer,
preventDefault: jest.fn(),
})
// Verify convertToMentionPath was not called
expect(mockConvertToMentionPath).not.toHaveBeenCalled()
// Verify setInputValue was not called
expect(setInputValue).not.toHaveBeenCalled()
})
})
})