mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-07 02:58:15 +00:00
Merge remote-tracking branch 'upstream/main' into sbc_add_subtasks
This commit is contained in:
commit
f835b8c3e2
7 changed files with 287 additions and 11 deletions
5
.changeset/fluffy-apples-attack.md
Normal file
5
.changeset/fluffy-apples-attack.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Handle really long text in the ChatRow similar to TaskHeader
|
||||
5
.changeset/orange-zoos-train.md
Normal file
5
.changeset/orange-zoos-train.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Support multiple files in drag-and-drop
|
||||
5
.changeset/tender-cycles-help.md
Normal file
5
.changeset/tender-cycles-help.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Better OpenRouter error handling
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue