Skip to content

Commit 199b6ea

Browse files
authored
Merge pull request #1190 from RooVetGit/file_drag_drop
File drag and drop
2 parents 46576e0 + 8a3b8d1 commit 199b6ea

File tree

8 files changed

+105
-6
lines changed

8 files changed

+105
-6
lines changed

.changeset/shaggy-spies-kneel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Add drag-and-drop for files

src/core/webview/ClineProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2421,6 +2421,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
24212421

24222422
const allowedCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("allowedCommands") || []
24232423

2424+
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) || ""
2425+
24242426
return {
24252427
version: this.context.extension?.packageJSON?.version ?? "",
24262428
apiConfiguration,
@@ -2467,6 +2469,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
24672469
experiments: experiments ?? experimentDefault,
24682470
mcpServers: this.mcpHub?.getAllServers() ?? [],
24692471
maxOpenTabsContext: maxOpenTabsContext ?? 20,
2472+
cwd: cwd,
24702473
}
24712474
}
24722475

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export interface ExtensionState {
129129
customModes: ModeConfig[]
130130
toolRequirements?: Record<string, boolean> // Map of tool names to their requirements (e.g. {"apply_diff": true} if diffEnabled)
131131
maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500)
132+
cwd?: string // Current working directory
132133
}
133134

134135
export interface ClineMessage {

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { vscode } from "../../utils/vscode"
1616
import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
1717
import { Mode, getAllModes } from "../../../../src/shared/modes"
1818
import { CaretIcon } from "../common/CaretIcon"
19+
import { convertToMentionPath } from "../../utils/path-mentions"
1920

2021
interface ChatTextAreaProps {
2122
inputValue: string
@@ -50,7 +51,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
5051
},
5152
ref,
5253
) => {
53-
const { filePaths, openedTabs, currentApiConfigName, listApiConfigMeta, customModes } = useExtensionState()
54+
const { filePaths, openedTabs, currentApiConfigName, listApiConfigMeta, customModes, cwd } = useExtensionState()
5455
const [gitCommits, setGitCommits] = useState<any[]>([])
5556
const [showDropdown, setShowDropdown] = useState(false)
5657

@@ -589,18 +590,24 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
589590
const files = Array.from(e.dataTransfer.files)
590591
const text = e.dataTransfer.getData("text")
591592
if (text) {
592-
const newValue = inputValue.slice(0, cursorPosition) + text + inputValue.slice(cursorPosition)
593+
// Convert the path to a mention-friendly format
594+
const mentionText = convertToMentionPath(text, cwd)
595+
596+
const newValue =
597+
inputValue.slice(0, cursorPosition) + mentionText + " " + inputValue.slice(cursorPosition)
593598
setInputValue(newValue)
594-
const newCursorPosition = cursorPosition + text.length
599+
const newCursorPosition = cursorPosition + mentionText.length + 1
595600
setCursorPosition(newCursorPosition)
596601
setIntendedCursorPosition(newCursorPosition)
597602
return
598603
}
604+
599605
const acceptedTypes = ["png", "jpeg", "webp"]
600606
const imageFiles = files.filter((file) => {
601607
const [type, subtype] = file.type.split("/")
602608
return type === "image" && acceptedTypes.includes(subtype)
603609
})
610+
604611
if (!shouldDisableImages && imageFiles.length > 0) {
605612
const imagePromises = imageFiles.map((file) => {
606613
return new Promise<string | null>((resolve) => {

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -880,9 +880,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
880880
const placeholderText = useMemo(() => {
881881
const baseText = task ? "Type a message..." : "Type your task here..."
882882
const contextText = "(@ to add context, / to switch modes"
883-
const imageText = shouldDisableImages ? "" : ", hold shift to drag in images"
884-
const helpText = imageText ? `\n${contextText}${imageText})` : `\n${contextText})`
885-
return baseText + helpText
883+
const imageText = shouldDisableImages ? "hold shift to drag in files" : ", hold shift to drag in files/images"
884+
return baseText + `\n${contextText}${imageText})`
886885
}, [task, shouldDisableImages])
887886

888887
const itemContent = useCallback(

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
118118
autoApprovalEnabled: false,
119119
customModes: [],
120120
maxOpenTabsContext: 20,
121+
cwd: "",
121122
})
122123

123124
const [didHydrateState, setDidHydrateState] = useState(false)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { convertToMentionPath } from "../path-mentions"
2+
3+
describe("path-mentions", () => {
4+
describe("convertToMentionPath", () => {
5+
it("should convert an absolute path to a mention path when it starts with cwd", () => {
6+
// Windows-style paths
7+
expect(convertToMentionPath("C:\\Users\\user\\project\\file.txt", "C:\\Users\\user\\project")).toBe(
8+
"@/file.txt",
9+
)
10+
11+
// Unix-style paths
12+
expect(convertToMentionPath("/Users/user/project/file.txt", "/Users/user/project")).toBe("@/file.txt")
13+
})
14+
15+
it("should handle paths with trailing slashes in cwd", () => {
16+
expect(convertToMentionPath("/Users/user/project/file.txt", "/Users/user/project/")).toBe("@/file.txt")
17+
})
18+
19+
it("should be case-insensitive when matching paths", () => {
20+
expect(convertToMentionPath("/Users/User/Project/file.txt", "/users/user/project")).toBe("@/file.txt")
21+
})
22+
23+
it("should return the original path when cwd is not provided", () => {
24+
expect(convertToMentionPath("/Users/user/project/file.txt")).toBe("/Users/user/project/file.txt")
25+
})
26+
27+
it("should return the original path when it does not start with cwd", () => {
28+
expect(convertToMentionPath("/Users/other/project/file.txt", "/Users/user/project")).toBe(
29+
"/Users/other/project/file.txt",
30+
)
31+
})
32+
33+
it("should normalize backslashes to forward slashes", () => {
34+
expect(convertToMentionPath("C:\\Users\\user\\project\\subdir\\file.txt", "C:\\Users\\user\\project")).toBe(
35+
"@/subdir/file.txt",
36+
)
37+
})
38+
39+
it("should handle nested paths correctly", () => {
40+
expect(convertToMentionPath("/Users/user/project/nested/deeply/file.txt", "/Users/user/project")).toBe(
41+
"@/nested/deeply/file.txt",
42+
)
43+
})
44+
})
45+
})
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Utilities for handling path-related operations in mentions
3+
*/
4+
5+
/**
6+
* Converts an absolute path to a mention-friendly path
7+
* If the provided path starts with the current working directory,
8+
* it's converted to a relative path prefixed with @
9+
*
10+
* @param path The path to convert
11+
* @param cwd The current working directory
12+
* @returns A mention-friendly path
13+
*/
14+
export function convertToMentionPath(path: string, cwd?: string): string {
15+
const normalizedPath = path.replace(/\\/g, "/")
16+
let normalizedCwd = cwd ? cwd.replace(/\\/g, "/") : ""
17+
18+
if (!normalizedCwd) {
19+
return path
20+
}
21+
22+
// Remove trailing slash from cwd if it exists
23+
if (normalizedCwd.endsWith("/")) {
24+
normalizedCwd = normalizedCwd.slice(0, -1)
25+
}
26+
27+
// Always use case-insensitive comparison for path matching
28+
const lowerPath = normalizedPath.toLowerCase()
29+
const lowerCwd = normalizedCwd.toLowerCase()
30+
31+
if (lowerPath.startsWith(lowerCwd)) {
32+
const relativePath = normalizedPath.substring(normalizedCwd.length)
33+
// Ensure there's a slash after the @ symbol when we create the mention path
34+
return "@" + (relativePath.startsWith("/") ? relativePath : "/" + relativePath)
35+
}
36+
37+
return path
38+
}

0 commit comments

Comments
 (0)