Răsfoiți Sursa

Merge branch 'main' into cte/openrouter-claude-thinking

Chris Estreich 10 luni în urmă
părinte
comite
69bcd2e340

+ 5 - 0
.changeset/shaggy-spies-kneel.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Add drag-and-drop for files

+ 3 - 0
src/core/webview/ClineProvider.ts

@@ -2028,6 +2028,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 
 		const allowedCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("allowedCommands") || []
 
+		const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || ""
+
 		return {
 			version: this.context.extension?.packageJSON?.version ?? "",
 			apiConfiguration,
@@ -2074,6 +2076,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			experiments: experiments ?? experimentDefault,
 			mcpServers: this.mcpHub?.getAllServers() ?? [],
 			maxOpenTabsContext: maxOpenTabsContext ?? 20,
+			cwd: cwd,
 		}
 	}
 

+ 1 - 0
src/shared/ExtensionMessage.ts

@@ -129,6 +129,7 @@ export interface ExtensionState {
 	customModes: ModeConfig[]
 	toolRequirements?: Record<string, boolean> // Map of tool names to their requirements (e.g. {"apply_diff": true} if diffEnabled)
 	maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500)
+	cwd?: string // Current working directory
 }
 
 export interface ClineMessage {

+ 10 - 3
webview-ui/src/components/chat/ChatTextArea.tsx

@@ -16,6 +16,7 @@ import { vscode } from "../../utils/vscode"
 import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
 import { Mode, getAllModes } from "../../../../src/shared/modes"
 import { CaretIcon } from "../common/CaretIcon"
+import { convertToMentionPath } from "../../utils/path-mentions"
 
 interface ChatTextAreaProps {
 	inputValue: string
@@ -50,7 +51,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 		},
 		ref,
 	) => {
-		const { filePaths, openedTabs, currentApiConfigName, listApiConfigMeta, customModes } = useExtensionState()
+		const { filePaths, openedTabs, currentApiConfigName, listApiConfigMeta, customModes, cwd } = useExtensionState()
 		const [gitCommits, setGitCommits] = useState<any[]>([])
 		const [showDropdown, setShowDropdown] = useState(false)
 
@@ -589,18 +590,24 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 					const files = Array.from(e.dataTransfer.files)
 					const text = e.dataTransfer.getData("text")
 					if (text) {
-						const newValue = inputValue.slice(0, cursorPosition) + text + inputValue.slice(cursorPosition)
+						// Convert the path to a mention-friendly format
+						const mentionText = convertToMentionPath(text, cwd)
+
+						const newValue =
+							inputValue.slice(0, cursorPosition) + mentionText + " " + inputValue.slice(cursorPosition)
 						setInputValue(newValue)
-						const newCursorPosition = cursorPosition + text.length
+						const newCursorPosition = cursorPosition + mentionText.length + 1
 						setCursorPosition(newCursorPosition)
 						setIntendedCursorPosition(newCursorPosition)
 						return
 					}
+
 					const acceptedTypes = ["png", "jpeg", "webp"]
 					const imageFiles = files.filter((file) => {
 						const [type, subtype] = file.type.split("/")
 						return type === "image" && acceptedTypes.includes(subtype)
 					})
+
 					if (!shouldDisableImages && imageFiles.length > 0) {
 						const imagePromises = imageFiles.map((file) => {
 							return new Promise<string | null>((resolve) => {

+ 2 - 3
webview-ui/src/components/chat/ChatView.tsx

@@ -880,9 +880,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 	const placeholderText = useMemo(() => {
 		const baseText = task ? "Type a message..." : "Type your task here..."
 		const contextText = "(@ to add context, / to switch modes"
-		const imageText = shouldDisableImages ? "" : ", hold shift to drag in images"
-		const helpText = imageText ? `\n${contextText}${imageText})` : `\n${contextText})`
-		return baseText + helpText
+		const imageText = shouldDisableImages ? "hold shift to drag in files" : ", hold shift to drag in files/images"
+		return baseText + `\n${contextText}${imageText})`
 	}, [task, shouldDisableImages])
 
 	const itemContent = useCallback(

+ 1 - 0
webview-ui/src/context/ExtensionStateContext.tsx

@@ -118,6 +118,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		autoApprovalEnabled: false,
 		customModes: [],
 		maxOpenTabsContext: 20,
+		cwd: "",
 	})
 
 	const [didHydrateState, setDidHydrateState] = useState(false)

+ 45 - 0
webview-ui/src/utils/__tests__/path-mentions.test.ts

@@ -0,0 +1,45 @@
+import { convertToMentionPath } from "../path-mentions"
+
+describe("path-mentions", () => {
+	describe("convertToMentionPath", () => {
+		it("should convert an absolute path to a mention path when it starts with cwd", () => {
+			// Windows-style paths
+			expect(convertToMentionPath("C:\\Users\\user\\project\\file.txt", "C:\\Users\\user\\project")).toBe(
+				"@/file.txt",
+			)
+
+			// Unix-style paths
+			expect(convertToMentionPath("/Users/user/project/file.txt", "/Users/user/project")).toBe("@/file.txt")
+		})
+
+		it("should handle paths with trailing slashes in cwd", () => {
+			expect(convertToMentionPath("/Users/user/project/file.txt", "/Users/user/project/")).toBe("@/file.txt")
+		})
+
+		it("should be case-insensitive when matching paths", () => {
+			expect(convertToMentionPath("/Users/User/Project/file.txt", "/users/user/project")).toBe("@/file.txt")
+		})
+
+		it("should return the original path when cwd is not provided", () => {
+			expect(convertToMentionPath("/Users/user/project/file.txt")).toBe("/Users/user/project/file.txt")
+		})
+
+		it("should return the original path when it does not start with cwd", () => {
+			expect(convertToMentionPath("/Users/other/project/file.txt", "/Users/user/project")).toBe(
+				"/Users/other/project/file.txt",
+			)
+		})
+
+		it("should normalize backslashes to forward slashes", () => {
+			expect(convertToMentionPath("C:\\Users\\user\\project\\subdir\\file.txt", "C:\\Users\\user\\project")).toBe(
+				"@/subdir/file.txt",
+			)
+		})
+
+		it("should handle nested paths correctly", () => {
+			expect(convertToMentionPath("/Users/user/project/nested/deeply/file.txt", "/Users/user/project")).toBe(
+				"@/nested/deeply/file.txt",
+			)
+		})
+	})
+})

+ 38 - 0
webview-ui/src/utils/path-mentions.ts

@@ -0,0 +1,38 @@
+/**
+ * Utilities for handling path-related operations in mentions
+ */
+
+/**
+ * Converts an absolute path to a mention-friendly path
+ * If the provided path starts with the current working directory,
+ * it's converted to a relative path prefixed with @
+ *
+ * @param path The path to convert
+ * @param cwd The current working directory
+ * @returns A mention-friendly path
+ */
+export function convertToMentionPath(path: string, cwd?: string): string {
+	const normalizedPath = path.replace(/\\/g, "/")
+	let normalizedCwd = cwd ? cwd.replace(/\\/g, "/") : ""
+
+	if (!normalizedCwd) {
+		return path
+	}
+
+	// Remove trailing slash from cwd if it exists
+	if (normalizedCwd.endsWith("/")) {
+		normalizedCwd = normalizedCwd.slice(0, -1)
+	}
+
+	// Always use case-insensitive comparison for path matching
+	const lowerPath = normalizedPath.toLowerCase()
+	const lowerCwd = normalizedCwd.toLowerCase()
+
+	if (lowerPath.startsWith(lowerCwd)) {
+		const relativePath = normalizedPath.substring(normalizedCwd.length)
+		// Ensure there's a slash after the @ symbol when we create the mention path
+		return "@" + (relativePath.startsWith("/") ? relativePath : "/" + relativePath)
+	}
+
+	return path
+}