Просмотр исходного кода

Revert "feat @-mention window path style & file with space (#1924)" (#2162)

* Revert "feat @-mention window path style & file with space (#1924)"

This reverts commit c62e8f2ee23d3f07393e2ef32d4b882a0e077761.

* Add changeset
Matt Rubens 9 месяцев назад
Родитель
Сommit
b0b4240c8b

+ 5 - 0
.changeset/brown-cows-jump.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Revert mention changes in case they're causing performance issues/crashes

+ 2 - 10
src/core/Cline.ts

@@ -2049,8 +2049,6 @@ export class Cline extends EventEmitter<ClineEvents> {
 			// 2. ToolResultBlockParam's content/context text arrays if it contains "<feedback>" (see formatToolDeniedFeedback, attemptCompletion, executeCommand, and consecutiveMistakeCount >= 3) or "<answer>" (see askFollowupQuestion), we place all user generated content in these tags so they can effectively be used as markers for when we should parse mentions)
 			Promise.all(
 				userContent.map(async (block) => {
-					const { osInfo } = (await this.providerRef.deref()?.getState()) || { osInfo: "unix" }
-
 					const shouldProcessMentions = (text: string) =>
 						text.includes("<task>") || text.includes("<feedback>")
 
@@ -2058,7 +2056,7 @@ export class Cline extends EventEmitter<ClineEvents> {
 						if (shouldProcessMentions(block.text)) {
 							return {
 								...block,
-								text: await parseMentions(block.text, this.cwd, this.urlContentFetcher, osInfo),
+								text: await parseMentions(block.text, this.cwd, this.urlContentFetcher),
 							}
 						}
 						return block
@@ -2067,12 +2065,7 @@ export class Cline extends EventEmitter<ClineEvents> {
 							if (shouldProcessMentions(block.content)) {
 								return {
 									...block,
-									content: await parseMentions(
-										block.content,
-										this.cwd,
-										this.urlContentFetcher,
-										osInfo,
-									),
+									content: await parseMentions(block.content, this.cwd, this.urlContentFetcher),
 								}
 							}
 							return block
@@ -2086,7 +2079,6 @@ export class Cline extends EventEmitter<ClineEvents> {
 												contentBlock.text,
 												this.cwd,
 												this.urlContentFetcher,
-												osInfo,
 											),
 										}
 									}

+ 0 - 2
src/core/__tests__/Cline.test.ts

@@ -941,7 +941,6 @@ describe("Cline", () => {
 						"<task>Text with @/some/path in task tags</task>",
 						expect.any(String),
 						expect.any(Object),
-						expect.any(String),
 					)
 
 					// Feedback tag content should be processed
@@ -952,7 +951,6 @@ describe("Cline", () => {
 						"<feedback>Check @/some/path</feedback>",
 						expect.any(String),
 						expect.any(Object),
-						expect.any(String),
 					)
 
 					// Regular tool result should not be processed

+ 113 - 226
src/core/mentions/index.ts

@@ -1,18 +1,17 @@
 import * as vscode from "vscode"
 import * as path from "path"
-import fs from "fs/promises"
 import { openFile } from "../../integrations/misc/open-file"
 import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
-import { mentionRegexGlobal } from "../../shared/context-mentions"
-import { getWorkspacePath } from "../../utils/path"
-import { HandlerConfig, MentionContext, XmlTag } from "./types"
+import { mentionRegexGlobal, formatGitSuggestion, type MentionSuggestion } from "../../shared/context-mentions"
+import fs from "fs/promises"
 import { extractTextFromFile } from "../../integrations/misc/extract-text"
 import { isBinaryFile } from "isbinaryfile"
-import { getWorkingState, getCommitInfo } from "../../utils/git"
 import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
+import { getCommitInfo, getWorkingState } from "../../utils/git"
 import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
+import { getWorkspacePath } from "../../utils/path"
 
-export async function openMention(mention?: string, osInfo?: string): Promise<void> {
+export async function openMention(mention?: string): Promise<void> {
 	if (!mention) {
 		return
 	}
@@ -22,17 +21,10 @@ export async function openMention(mention?: string, osInfo?: string): Promise<vo
 		return
 	}
 
-	if ((osInfo !== "win32" && mention.startsWith("/")) || (osInfo === "win32" && mention.startsWith("\\"))) {
+	if (mention.startsWith("/")) {
 		const relPath = mention.slice(1)
-		let absPath = path.resolve(cwd, relPath)
-		if (absPath.includes(" ")) {
-			let escapedSpace = osInfo === "win32" ? "/ " : "\\ "
-			absPath = absPath.replaceAll(escapedSpace, " ")
-		}
-		if (
-			((osInfo === "unix" || osInfo === undefined) && mention.endsWith("/")) ||
-			(osInfo === "win32" && mention.endsWith("\\"))
-		) {
+		const absPath = path.resolve(cwd, relPath)
+		if (mention.endsWith("/")) {
 			vscode.commands.executeCommand("revealInExplorer", vscode.Uri.file(absPath))
 		} else {
 			openFile(absPath)
@@ -45,29 +37,115 @@ export async function openMention(mention?: string, osInfo?: string): Promise<vo
 		vscode.env.openExternal(vscode.Uri.parse(mention))
 	}
 }
-// Utility functions
-export const createXmlTag = (name: string, attrs: Record<string, string> = {}): XmlTag => {
-	const attrString = Object.entries(attrs)
-		.map(([key, value]) => `${key}="${value}"`)
-		.join(" ")
-	return {
-		start: `\n\n<${name}${attrString ? " " + attrString : ""}>`,
-		end: `</${name}>`,
+
+export async function parseMentions(text: string, cwd: string, urlContentFetcher: UrlContentFetcher): Promise<string> {
+	const mentions: Set<string> = new Set()
+	let parsedText = text.replace(mentionRegexGlobal, (match, mention) => {
+		mentions.add(mention)
+		if (mention.startsWith("http")) {
+			return `'${mention}' (see below for site content)`
+		} else if (mention.startsWith("/")) {
+			const mentionPath = mention.slice(1)
+			return mentionPath.endsWith("/")
+				? `'${mentionPath}' (see below for folder content)`
+				: `'${mentionPath}' (see below for file content)`
+		} else if (mention === "problems") {
+			return `Workspace Problems (see below for diagnostics)`
+		} else if (mention === "git-changes") {
+			return `Working directory changes (see below for details)`
+		} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
+			return `Git commit '${mention}' (see below for commit info)`
+		} else if (mention === "terminal") {
+			return `Terminal Output (see below for output)`
+		}
+		return match
+	})
+
+	const urlMention = Array.from(mentions).find((mention) => mention.startsWith("http"))
+	let launchBrowserError: Error | undefined
+	if (urlMention) {
+		try {
+			await urlContentFetcher.launchBrowser()
+		} catch (error) {
+			launchBrowserError = error
+			vscode.window.showErrorMessage(`Error fetching content for ${urlMention}: ${error.message}`)
+		}
 	}
-}
 
-export const wrapContent = (content: string, tag: XmlTag): string => `${tag.start}\n${content}\n${tag.end}`
+	for (const mention of mentions) {
+		if (mention.startsWith("http")) {
+			let result: string
+			if (launchBrowserError) {
+				result = `Error fetching content: ${launchBrowserError.message}`
+			} else {
+				try {
+					const markdown = await urlContentFetcher.urlToMarkdown(mention)
+					result = markdown
+				} catch (error) {
+					vscode.window.showErrorMessage(`Error fetching content for ${mention}: ${error.message}`)
+					result = `Error fetching content: ${error.message}`
+				}
+			}
+			parsedText += `\n\n<url_content url="${mention}">\n${result}\n</url_content>`
+		} else if (mention.startsWith("/")) {
+			const mentionPath = mention.slice(1)
+			try {
+				const content = await getFileOrFolderContent(mentionPath, cwd)
+				if (mention.endsWith("/")) {
+					parsedText += `\n\n<folder_content path="${mentionPath}">\n${content}\n</folder_content>`
+				} else {
+					parsedText += `\n\n<file_content path="${mentionPath}">\n${content}\n</file_content>`
+				}
+			} catch (error) {
+				if (mention.endsWith("/")) {
+					parsedText += `\n\n<folder_content path="${mentionPath}">\nError fetching content: ${error.message}\n</folder_content>`
+				} else {
+					parsedText += `\n\n<file_content path="${mentionPath}">\nError fetching content: ${error.message}\n</file_content>`
+				}
+			}
+		} else if (mention === "problems") {
+			try {
+				const problems = await getWorkspaceProblems(cwd)
+				parsedText += `\n\n<workspace_diagnostics>\n${problems}\n</workspace_diagnostics>`
+			} catch (error) {
+				parsedText += `\n\n<workspace_diagnostics>\nError fetching diagnostics: ${error.message}\n</workspace_diagnostics>`
+			}
+		} else if (mention === "git-changes") {
+			try {
+				const workingState = await getWorkingState(cwd)
+				parsedText += `\n\n<git_working_state>\n${workingState}\n</git_working_state>`
+			} catch (error) {
+				parsedText += `\n\n<git_working_state>\nError fetching working state: ${error.message}\n</git_working_state>`
+			}
+		} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
+			try {
+				const commitInfo = await getCommitInfo(mention, cwd)
+				parsedText += `\n\n<git_commit hash="${mention}">\n${commitInfo}\n</git_commit>`
+			} catch (error) {
+				parsedText += `\n\n<git_commit hash="${mention}">\nError fetching commit info: ${error.message}\n</git_commit>`
+			}
+		} else if (mention === "terminal") {
+			try {
+				const terminalOutput = await getLatestTerminalOutput()
+				parsedText += `\n\n<terminal_output>\n${terminalOutput}\n</terminal_output>`
+			} catch (error) {
+				parsedText += `\n\n<terminal_output>\nError fetching terminal output: ${error.message}\n</terminal_output>`
+			}
+		}
+	}
 
-export const handleError = (error: Error, message: string): string => {
-	const errorMsg = `Error ${message}: ${error.message}`
-	if (error instanceof Error) {
-		vscode.window.showErrorMessage(errorMsg)
+	if (urlMention) {
+		try {
+			await urlContentFetcher.closeBrowser()
+		} catch (error) {
+			console.error(`Error closing browser: ${error.message}`)
+		}
 	}
-	return errorMsg
+
+	return parsedText
 }
 
-// File utilities
-export async function getFileOrFolderContent(mentionPath: string, cwd: string, osInfo: string): Promise<string> {
+async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise<string> {
 	const absPath = path.resolve(cwd, mentionPath)
 
 	try {
@@ -99,7 +177,7 @@ export async function getFileOrFolderContent(mentionPath: string, cwd: string, o
 									return undefined
 								}
 								const content = await extractTextFromFile(absoluteFilePath)
-								return `<file_content path="${filePath}">\n${content}\n</file_content>`
+								return `<file_content path="${filePath.toPosix()}">\n${content}\n</file_content>`
 							} catch (error) {
 								return undefined
 							}
@@ -121,8 +199,7 @@ export async function getFileOrFolderContent(mentionPath: string, cwd: string, o
 	}
 }
 
-// Workspace utilities
-export async function getWorkspaceProblems(cwd: string): Promise<string> {
+async function getWorkspaceProblems(cwd: string): Promise<string> {
 	const diagnostics = vscode.languages.getDiagnostics()
 	const result = await diagnosticsToProblemsString(
 		diagnostics,
@@ -134,193 +211,3 @@ export async function getWorkspaceProblems(cwd: string): Promise<string> {
 	}
 	return result
 }
-
-// Handler implementations
-const urlHandler: HandlerConfig = {
-	name: "url",
-	test: (mention: string) => mention.startsWith("http"),
-	handler: async (mention, { urlContentFetcher, launchBrowserError }) => {
-		const tag = createXmlTag("url_content", { url: mention })
-		let content: string
-
-		if (launchBrowserError) {
-			content = handleError(launchBrowserError, "fetching content")
-		} else {
-			try {
-				content = await urlContentFetcher.urlToMarkdown(mention)
-			} catch (error) {
-				content = handleError(error, `fetching content for ${mention}`)
-			}
-		}
-		return wrapContent(content, tag)
-	},
-}
-
-const fileHandler: HandlerConfig = {
-	name: "file",
-	test: (mention: string, { osInfo }) => (osInfo !== "win32" ? mention.startsWith("/") : mention.startsWith("\\")),
-	handler: async (mention, { cwd, osInfo }) => {
-		let mentionPath = mention.slice(1)
-		const isFolder = osInfo === "win32" ? mention.endsWith("\\") : mention.endsWith("/")
-		const tag = createXmlTag(isFolder ? "folder_content" : "file_content", { path: mentionPath })
-
-		if (mentionPath.includes(" ")) {
-			let escapedSpace = osInfo === "win32" ? "/ " : "\\ "
-			mentionPath = mentionPath.replaceAll(escapedSpace, " ")
-		}
-
-		try {
-			const content = await getFileOrFolderContent(mentionPath, cwd, osInfo)
-			return wrapContent(content, tag)
-		} catch (error) {
-			return wrapContent(handleError(error, "fetching content"), tag)
-		}
-	},
-}
-
-const problemsHandler: HandlerConfig = {
-	name: "problems",
-	test: (mention: string) => mention === "problems",
-	handler: async (mention, { cwd }) => {
-		const tag = createXmlTag("workspace_diagnostics")
-		try {
-			const problems = await getWorkspaceProblems(cwd)
-			return wrapContent(problems, tag)
-		} catch (error) {
-			return wrapContent(handleError(error, "fetching diagnostics"), tag)
-		}
-	},
-}
-
-const gitChangesHandler: HandlerConfig = {
-	name: "git-changes",
-	test: (mention: string) => mention === "git-changes",
-	handler: async (mention, { cwd }) => {
-		const tag = createXmlTag("git_working_state")
-		try {
-			const workingState = await getWorkingState(cwd)
-			return wrapContent(workingState, tag)
-		} catch (error) {
-			return wrapContent(handleError(error, "fetching working state"), tag)
-		}
-	},
-}
-
-const commitHandler: HandlerConfig = {
-	name: "commit",
-	test: (mention: string) => /^[a-f0-9]{7,40}$/.test(mention),
-	handler: async (mention, { cwd }) => {
-		const tag = createXmlTag("git_commit", { hash: mention })
-		try {
-			const commitInfo = await getCommitInfo(mention, cwd)
-			return wrapContent(commitInfo, tag)
-		} catch (error) {
-			return wrapContent(handleError(error, "fetching commit info"), tag)
-		}
-	},
-}
-
-const terminalHandler: HandlerConfig = {
-	name: "terminal",
-	test: (mention: string) => mention === "terminal",
-	handler: async (mention) => {
-		const tag = createXmlTag("terminal_output")
-		try {
-			const terminalOutput = await getLatestTerminalOutput()
-			return wrapContent(terminalOutput, tag)
-		} catch (error) {
-			return wrapContent(handleError(error, "fetching terminal output"), tag)
-		}
-	},
-}
-
-// Define handlers array
-const handlers: HandlerConfig[] = [
-	urlHandler,
-	fileHandler,
-	problemsHandler,
-	gitChangesHandler,
-	commitHandler,
-	terminalHandler,
-]
-
-export async function parseMentions(
-	text: string,
-	cwd: string,
-	urlContentFetcher: UrlContentFetcher,
-	osInfo: string = "unix",
-): Promise<string> {
-	const mentions: Set<string> = new Set()
-	let parsedText = text.replace(mentionRegexGlobal, (match, mention) => {
-		mentions.add(mention)
-		if (mention.startsWith("http")) {
-			return `'${mention}' (see below for site content)`
-		}
-
-		if (
-			(osInfo !== "win32" && osInfo !== undefined && mention.startsWith("/")) ||
-			(osInfo === "win32" && mention.startsWith("\\"))
-		) {
-			const mentionPath = mention.slice(1)
-			return mentionPath.endsWith("/") || mentionPath.endsWith("\\")
-				? `'${mentionPath}' (see below for folder content)`
-				: `'${mentionPath}' (see below for file content)`
-		}
-
-		if (mention === "problems") {
-			return `Workspace Problems (see below for diagnostics)`
-		}
-		if (mention === "git-changes") {
-			return `Working directory changes (see below for details)`
-		}
-		if (/^[a-f0-9]{7,40}$/.test(mention)) {
-			return `Git commit '${mention}' (see below for commit info)`
-		}
-
-		if (mention === "terminal") {
-			return `Terminal Output (see below for output)`
-		}
-		return match
-	})
-
-	const urlMention = Array.from(mentions).find((mention) => mention.startsWith("http"))
-	let launchBrowserError: Error | undefined
-	if (urlMention) {
-		try {
-			await urlContentFetcher.launchBrowser()
-		} catch (error) {
-			launchBrowserError = error
-			vscode.window.showErrorMessage(`Error fetching content for ${urlMention}: ${error.message}`)
-		}
-	}
-
-	const context: MentionContext = {
-		cwd,
-		urlContentFetcher,
-		launchBrowserError,
-		osInfo,
-	}
-
-	const mentionResults = await Promise.all(
-		Array.from(mentions).map(async (mention) => {
-			for (const handler of handlers) {
-				if (handler.test(mention, context)) {
-					return handler.handler(mention, context)
-				}
-			}
-			return ""
-		}),
-	)
-
-	parsedText += mentionResults.join("")
-
-	if (urlMention) {
-		try {
-			await urlContentFetcher.closeBrowser()
-		} catch (error) {
-			console.error(`Error closing browser: ${error.message}`)
-		}
-	}
-
-	return parsedText
-}

+ 0 - 21
src/core/mentions/types.ts

@@ -1,21 +0,0 @@
-import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
-
-export type MentionHandler = (mention: string) => Promise<string>
-
-export type XmlTag = {
-	start: string
-	end: string
-}
-
-export interface MentionContext {
-	cwd: string
-	urlContentFetcher: UrlContentFetcher
-	launchBrowserError?: Error
-	osInfo: string
-}
-
-export interface HandlerConfig {
-	name: string
-	test: (mention: string, context: MentionContext) => boolean
-	handler: (mention: string, context: MentionContext) => Promise<string>
-}

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

@@ -1188,7 +1188,6 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 
 		return {
 			version: this.context.extension?.packageJSON?.version ?? "",
-			osInfo: os.platform() === "win32" ? "win32" : "unix",
 			apiConfiguration,
 			customInstructions,
 			alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
@@ -1283,7 +1282,6 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 		// Return the same structure as before
 		return {
 			apiConfiguration: providerSettings,
-			osInfo: os.platform() === "win32" ? "win32" : "unix",
 			lastShownAnnouncementId: stateValues.lastShownAnnouncementId,
 			customInstructions: stateValues.customInstructions,
 			alwaysAllowReadOnly: stateValues.alwaysAllowReadOnly ?? false,

+ 0 - 1
src/core/webview/__tests__/ClineProvider.test.ts

@@ -388,7 +388,6 @@ describe("ClineProvider", () => {
 
 		const mockState: ExtensionState = {
 			version: "1.0.0",
-			osInfo: "unix",
 			clineMessages: [],
 			taskHistory: [],
 			shouldShowAnnouncement: false,

+ 1 - 4
src/core/webview/webviewMessageHandler.ts

@@ -467,10 +467,7 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
 			openFile(message.text!, message.values as { create?: boolean; content?: string })
 			break
 		case "openMention":
-			{
-				const { osInfo } = (await provider.getState()) || {}
-				openMention(message.text, osInfo)
-			}
+			openMention(message.text)
 			break
 		case "checkpointDiff":
 			const result = checkoutDiffPayloadSchema.safeParse(message.payload)

+ 1 - 6
src/integrations/workspace/__tests__/WorkspaceTracker.test.ts

@@ -15,12 +15,7 @@ let registeredTabChangeCallback: (() => Promise<void>) | null = null
 // Mock workspace path
 jest.mock("../../../utils/path", () => ({
 	getWorkspacePath: jest.fn().mockReturnValue("/test/workspace"),
-	toRelativePath: jest.fn((path, cwd) => {
-		// Simple mock that preserves the original behavior for tests
-		const relativePath = path.replace(`${cwd}/`, "")
-		// Add trailing slash if original path had one
-		return path.endsWith("/") ? relativePath + "/" : relativePath
-	}),
+	toRelativePath: jest.fn((path, cwd) => path.replace(`${cwd}/`, "")),
 }))
 
 // Mock watcher - must be defined after mockDispose but before jest.mock("vscode")

+ 0 - 1
src/shared/ExtensionMessage.ts

@@ -168,7 +168,6 @@ export type ExtensionState = Pick<
 	| "enhancementApiConfigId"
 > & {
 	version: string
-	osInfo: string
 	clineMessages: ClineMessage[]
 	currentTaskItem?: HistoryItem
 	apiConfiguration?: ApiConfiguration

+ 0 - 325
src/shared/__tests__/context-mentions.test.ts

@@ -1,325 +0,0 @@
-import { mentionRegex, mentionRegexGlobal } from "../context-mentions"
-
-interface TestResult {
-	actual: string | null
-	expected: string | null
-}
-
-function testMention(input: string, expected: string | null): TestResult {
-	const match = mentionRegex.exec(input)
-	return {
-		actual: match ? match[0] : null,
-		expected,
-	}
-}
-
-function expectMatch(result: TestResult) {
-	if (result.expected === null) {
-		return expect(result.actual).toBeNull()
-	}
-	if (result.actual !== result.expected) {
-		// Instead of console.log, use expect().toBe() with a descriptive message
-		expect(result.actual).toBe(result.expected)
-	}
-}
-
-describe("Mention Regex", () => {
-	describe("Windows Path Support", () => {
-		it("matches simple Windows paths", () => {
-			const cases: Array<[string, string]> = [
-				["@C:\\folder\\file.txt", "@C:\\folder\\file.txt"],
-				["@c:\\Program/ Files\\file.txt", "@c:\\Program/ Files\\file.txt"],
-				["@C:\\file.txt", "@C:\\file.txt"],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-
-		it("matches Windows network shares", () => {
-			const cases: Array<[string, string]> = [
-				["@\\\\server\\share\\file.txt", "@\\\\server\\share\\file.txt"],
-				["@\\\\127.0.0.1\\network-path\\file.txt", "@\\\\127.0.0.1\\network-path\\file.txt"],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-
-		it("matches mixed separators", () => {
-			const result = testMention("@C:\\folder\\file.txt", "@C:\\folder\\file.txt")
-			expectMatch(result)
-		})
-
-		it("matches Windows relative paths", () => {
-			const cases: Array<[string, string]> = [
-				["@folder\\file.txt", "@folder\\file.txt"],
-				["@.\\folder\\file.txt", "@.\\folder\\file.txt"],
-				["@..\\parent\\file.txt", "@..\\parent\\file.txt"],
-				["@path\\to\\directory\\", "@path\\to\\directory\\"],
-				["@.\\current\\path\\with/ space.txt", "@.\\current\\path\\with/ space.txt"],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-	})
-
-	describe("Escaped Spaces Support", () => {
-		it("matches Unix paths with escaped spaces", () => {
-			const cases: Array<[string, string]> = [
-				["@/path/to/file\\ with\\ spaces.txt", "@/path/to/file\\ with\\ spaces.txt"],
-				["@/path/with\\ \\ multiple\\ spaces.txt", "@/path/with\\ \\ multiple\\ spaces.txt"],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-
-		it("matches Windows paths with escaped spaces", () => {
-			const cases: Array<[string, string]> = [
-				["@C:\\path\\to\\file/ with/ spaces.txt", "@C:\\path\\to\\file/ with/ spaces.txt"],
-				["@C:\\Program/ Files\\app\\file.txt", "@C:\\Program/ Files\\app\\file.txt"],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-	})
-
-	describe("Combined Path Variations", () => {
-		it("matches complex path combinations", () => {
-			const cases: Array<[string, string]> = [
-				[
-					"@C:\\Users\\name\\Documents\\file/ with/ spaces.txt",
-					"@C:\\Users\\name\\Documents\\file/ with/ spaces.txt",
-				],
-				[
-					"@\\\\server\\share\\path/ with/ spaces\\file.txt",
-					"@\\\\server\\share\\path/ with/ spaces\\file.txt",
-				],
-				["@C:\\path/ with/ spaces\\file.txt", "@C:\\path/ with/ spaces\\file.txt"],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-	})
-
-	describe("Edge Cases", () => {
-		it("handles edge cases correctly", () => {
-			const cases: Array<[string, string]> = [
-				["@C:\\", "@C:\\"],
-				["@/path/to/folder", "@/path/to/folder"],
-				["@C:\\folder\\file with spaces.txt", "@C:\\folder\\file"],
-				["@C:\\Users\\name\\path\\to\\文件夹\\file.txt", "@C:\\Users\\name\\path\\to\\文件夹\\file.txt"],
-				["@/path123/file-name_2.0.txt", "@/path123/file-name_2.0.txt"],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-	})
-
-	describe("Existing Functionality", () => {
-		it("matches Unix paths", () => {
-			const cases: Array<[string, string]> = [
-				["@/usr/local/bin/file", "@/usr/local/bin/file"],
-				["@/path/to/file.txt", "@/path/to/file.txt"],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-
-		it("matches URLs", () => {
-			const cases: Array<[string, string]> = [
-				["@http://example.com", "@http://example.com"],
-				["@https://example.com/path/to/file.html", "@https://example.com/path/to/file.html"],
-				["@ftp://server.example.com/file.zip", "@ftp://server.example.com/file.zip"],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-
-		it("matches git hashes", () => {
-			const cases: Array<[string, string]> = [
-				["@a1b2c3d4e5f6g7h8i9j0", "@a1b2c3d4e5f6g7h8i9j0"],
-				["@abcdef1234567890abcdef1234567890abcdef12", "@abcdef1234567890abcdef1234567890abcdef12"],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-
-		it("matches special keywords", () => {
-			const cases: Array<[string, string]> = [
-				["@problems", "@problems"],
-				["@git-changes", "@git-changes"],
-				["@terminal", "@terminal"],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-	})
-
-	describe("Invalid Patterns", () => {
-		it("rejects invalid patterns", () => {
-			const cases: Array<[string, null]> = [
-				["C:\\folder\\file.txt", null],
-				["@", null],
-				["@ C:\\file.txt", null],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-
-		it("matches only until invalid characters", () => {
-			const result = testMention("@C:\\folder\\file.txt invalid suffix", "@C:\\folder\\file.txt")
-			expectMatch(result)
-		})
-	})
-
-	describe("In Context", () => {
-		it("matches mentions within text", () => {
-			const cases: Array<[string, string]> = [
-				["Check the file at @C:\\folder\\file.txt for details.", "@C:\\folder\\file.txt"],
-				["See @/path/to/file\\ with\\ spaces.txt for an example.", "@/path/to/file\\ with\\ spaces.txt"],
-				["Review @problems and @git-changes.", "@problems"],
-				["Multiple: @/file1.txt and @C:\\file2.txt and @terminal", "@/file1.txt"],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-	})
-
-	describe("Multiple Mentions", () => {
-		it("finds all mentions in a string using global regex", () => {
-			const text = "Check @/path/file1.txt and @C:\\folder\\file2.txt and report any @problems to @git-changes"
-			const matches = text.match(mentionRegexGlobal)
-			expect(matches).toEqual(["@/path/file1.txt", "@C:\\folder\\file2.txt", "@problems", "@git-changes"])
-		})
-	})
-
-	describe("Special Characters in Paths", () => {
-		it("handles special characters in file paths", () => {
-			const cases: Array<[string, string]> = [
-				["@/path/with-dash/file_underscore.txt", "@/path/with-dash/file_underscore.txt"],
-				["@C:\\folder+plus\\file(parens)[]brackets.txt", "@C:\\folder+plus\\file(parens)[]brackets.txt"],
-				["@/path/with/file#hash%percent.txt", "@/path/with/file#hash%percent.txt"],
-				["@/path/with/file@symbol$dollar.txt", "@/path/with/file@symbol$dollar.txt"],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-	})
-
-	describe("Mixed Path Types in Single String", () => {
-		it("correctly identifies the first path in a string with multiple path types", () => {
-			const text = "Check both @/unix/path and @C:\\windows\\path for details."
-			const result = mentionRegex.exec(text)
-			expect(result?.[0]).toBe("@/unix/path")
-
-			// Test starting from after the first match
-			const secondSearchStart = text.indexOf("@C:")
-			const secondResult = mentionRegex.exec(text.substring(secondSearchStart))
-			expect(secondResult?.[0]).toBe("@C:\\windows\\path")
-		})
-	})
-
-	describe("Non-Latin Character Support", () => {
-		it("handles international characters in paths", () => {
-			const cases: Array<[string, string]> = [
-				["@/path/to/你好/file.txt", "@/path/to/你好/file.txt"],
-				["@C:\\用户\\документы\\файл.txt", "@C:\\用户\\документы\\файл.txt"],
-				["@/путь/к/файлу.txt", "@/путь/к/файлу.txt"],
-				["@C:\\folder\\file_äöü.txt", "@C:\\folder\\file_äöü.txt"],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-	})
-
-	describe("Mixed Path Delimiters", () => {
-		// Modifying expectations to match current behavior
-		it("documents behavior with mixed forward and backward slashes in Windows paths", () => {
-			const cases: Array<[string, null]> = [
-				// Current implementation doesn't support mixed slashes
-				["@C:\\Users/Documents\\folder/file.txt", null],
-				["@C:/Windows\\System32/drivers\\etc/hosts", null],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-	})
-
-	describe("Extended Negative Tests", () => {
-		// Modifying expectations to match current behavior
-		it("documents behavior with potentially invalid characters", () => {
-			const cases: Array<[string, string]> = [
-				// Current implementation actually matches these patterns
-				["@/path/with<illegal>chars.txt", "@/path/with<illegal>chars.txt"],
-				["@C:\\folder\\file|with|pipe.txt", "@C:\\folder\\file|with|pipe.txt"],
-				['@/path/with"quotes".txt', '@/path/with"quotes".txt'],
-			]
-
-			cases.forEach(([input, expected]) => {
-				const result = testMention(input, expected)
-				expectMatch(result)
-			})
-		})
-	})
-
-	// // These are documented as "not implemented yet"
-	// describe("Future Enhancement Candidates", () => {
-	// 	it("identifies patterns that could be supported in future enhancements", () => {
-	// 		// These patterns aren't currently supported by the regex
-	// 		// but might be considered for future improvements
-	// 		console.log(
-	// 			"The following patterns are not currently supported but might be considered for future enhancements:",
-	// 		)
-	// 		console.log("- Paths with double slashes: @/path//with/double/slash.txt")
-	// 		console.log("- Complex path traversals: @/very/./long/../../path/.././traversal.txt")
-	// 		console.log("- Environment variables in paths: @$HOME/file.txt, @C:\\Users\\%USERNAME%\\file.txt")
-	// 	})
-	// })
-})

+ 49 - 82
src/shared/context-mentions.ts

@@ -1,90 +1,57 @@
 /*
-- **Regex Breakdown**:
-
-  1. **Pattern Components**:
-     - The regex is built from multiple patterns joined with OR (|) operators
-     - Each pattern handles a specific type of mention:
-       - Unix/Linux paths
-       - Windows paths with drive letters
-       - Windows relative paths
-       - Windows network shares
-       - URLs with protocols
-       - Git commit hashes
-       - Special keywords (problems, git-changes, terminal)
-
-  2. **Unix Path Pattern**:
-     - `(?:\\/|^)`: Starts with a forward slash or beginning of line
-     - `(?:[^\\/\\s\\\\]|\\\\[ \\t])+`: Path segment that can include escaped spaces
-     - `(?:\\/(?:[^\\/\\s\\\\]|\\\\[ \\t])+)*`: Additional path segments after slashes
-     - `\\/?`: Optional trailing slash
-
-  3. **Windows Path Pattern**:
-     - `[A-Za-z]:\\\\`: Drive letter followed by colon and double backslash
-     - `(?:(?:[^\\\\\\s/]+|\\/[ ])+`: Path segment that can include spaces escaped with forward slash
-     - `(?:\\\\(?:[^\\\\\\s/]+|\\/[ ])+)*)?`: Additional path segments after backslashes
-
-  4. **Windows Relative Path Pattern**:
-     - `(?:\\.{0,2}|[^\\\\\\s/]+)`: Path prefix that can be:
-       - Current directory (.)
-       - Parent directory (..)
-       - Any directory name not containing spaces, backslashes, or forward slashes
-     - `\\\\`: Backslash separator
-     - `(?:[^\\\\\\s/]+|\\\\[ \\t]|\\/[ ])+`: Path segment that can include spaces escaped with backslash or forward slash
-     - `(?:\\\\(?:[^\\\\\\s/]+|\\\\[ \\t]|\\/[ ])+)*`: Additional path segments after backslashes
-     - `\\\\?`: Optional trailing backslash
+Mention regex:
+- **Purpose**: 
+  - To identify and highlight specific mentions in text that start with '@'. 
+  - These mentions can be file paths, URLs, or the exact word 'problems'.
+  - Ensures that trailing punctuation marks (like commas, periods, etc.) are not included in the match, allowing punctuation to follow the mention without being part of it.
 
-  5. **Network Share Pattern**:
-     - `\\\\\\\\`: Double backslash (escaped) to start network path
-     - `[^\\\\\\s]+`: Server name
-     - `(?:\\\\(?:[^\\\\\\s/]+|\\/[ ])+)*`: Share name and additional path components
-     - `(?:\\\\)?`: Optional trailing backslash
-
-  6. **URL Pattern**:
-     - `\\w+:\/\/`: Protocol (http://, https://, etc.)
-     - `[^\\s]+`: Rest of the URL (non-whitespace characters)
-
-  7. **Git Hash Pattern**:
-     - `[a-zA-Z0-9]{7,40}\\b`: 7-40 alphanumeric characters followed by word boundary
-
-  8. **Special Keywords Pattern**:
-     - `problems\\b`, `git-changes\\b`, `terminal\\b`: Exact word matches with word boundaries
+- **Regex Breakdown**:
+  - `/@`: 
+	- **@**: The mention must start with the '@' symbol.
+  
+  - `((?:\/|\w+:\/\/)[^\s]+?|problems\b|git-changes\b)`:
+	- **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns.
+	- `(?:\/|\w+:\/\/)`: 
+	  - **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing.
+	  - `\/`: 
+		- **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'.
+	  - `|`: Logical OR.
+	  - `\w+:\/\/`: 
+		- **Protocol (`\w+://`)**: Matches URLs that start with a word character sequence followed by '://', such as 'http://', 'https://', 'ftp://', etc.
+	- `[^\s]+?`: 
+	  - **Non-Whitespace Characters (`[^\s]+`)**: Matches one or more characters that are not whitespace.
+	  - **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation.
+	- `|`: Logical OR.
+	- `problems\b`: 
+	  - **Exact Word ('problems')**: Matches the exact word 'problems'.
+	  - **Word Boundary (`\b`)**: Ensures that 'problems' is matched as a whole word and not as part of another word (e.g., 'problematic').
+		- `|`: Logical OR.
+    - `terminal\b`:
+      - **Exact Word ('terminal')**: Matches the exact word 'terminal'.
+      - **Word Boundary (`\b`)**: Ensures that 'terminal' is matched as a whole word and not as part of another word (e.g., 'terminals').
+  - `(?=[.,;:!?]?(?=[\s\r\n]|$))`:
+	- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
+	- `[.,;:!?]?`: 
+	  - **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks.
+	- `(?=[\s\r\n]|$)`: 
+	  - **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string.
+  
+- **Summary**:
+  - The regex effectively matches:
+	- Mentions that are file or folder paths starting with '/' and containing any non-whitespace characters (including periods within the path).
+	- URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters).
+	- The exact word 'problems'.
+	- The exact word 'git-changes'.
+    - The exact word 'terminal'.
+  - It ensures that any trailing punctuation marks (such as ',', '.', '!', etc.) are not included in the matched mention, allowing the punctuation to follow the mention naturally in the text.
 
-  9. **Termination Logic**:
-     - `(?=[.,;:!?]?(?=[\\s\\r\\n]|$))`: Positive lookahead that:
-       - Allows an optional punctuation mark after the mention
-       - Ensures the mention (and optional punctuation) is followed by whitespace or end of string
+- **Global Regex**:
+  - `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string.
 
-- **Behavior Summary**:
-  - Matches @-prefixed mentions
-  - Handles different path formats across operating systems
-  - Supports escaped spaces in paths using OS-appropriate conventions
-  - Cleanly terminates at whitespace or end of string
-  - Excludes trailing punctuation from the match
-  - Creates both single-match and global-match regex objects
 */
-
-const mentionPatterns = [
-	// Unix paths with escaped spaces using backslash
-	"(?:\\/|^)(?:[^\\/\\s\\\\]|\\\\[ \\t])+(?:\\/(?:[^\\/\\s\\\\]|\\\\[ \\t])+)*\\/?",
-	// Windows paths with drive letters (C:\path) with support for escaped spaces using forward slash
-	"[A-Za-z]:\\\\(?:(?:[^\\\\\\s/]+|\\/[ ])+(?:\\\\(?:[^\\\\\\s/]+|\\/[ ])+)*)?",
-	// Windows relative paths (folder\file or .\folder\file) with support for escaped spaces
-	"(?:\\.{0,2}|[^\\\\\\s/]+)\\\\(?:[^\\\\\\s/]+|\\\\[ \\t]|\\/[ ])+(?:\\\\(?:[^\\\\\\s/]+|\\\\[ \\t]|\\/[ ])+)*\\\\?",
-	// Windows network shares (\\server\share) with support for escaped spaces using forward slash
-	"\\\\\\\\[^\\\\\\s]+(?:\\\\(?:[^\\\\\\s/]+|\\/[ ])+)*(?:\\\\)?",
-	// URLs with protocols (http://, https://, etc.)
-	"\\w+:\/\/[^\\s]+",
-	// Git hashes (7-40 alphanumeric characters)
-	"[a-zA-Z0-9]{7,40}\\b",
-	// Special keywords
-	"problems\\b",
-	"git-changes\\b",
-	"terminal\\b",
-]
-// Build the full regex pattern by joining the patterns with OR operator
-const mentionRegexPattern = `@(${mentionPatterns.join("|")})(?=[.,;:!?]?(?=[\\s\\r\\n]|$))`
-export const mentionRegex = new RegExp(mentionRegexPattern)
-export const mentionRegexGlobal = new RegExp(mentionRegexPattern, "g")
+export const mentionRegex =
+	/@((?:\/|\w+:\/\/)[^\s]+?|[a-f0-9]{7,40}\b|problems\b|git-changes\b|terminal\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
+export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")
 
 export interface MentionSuggestion {
 	type: "file" | "folder" | "git" | "problems"

+ 0 - 17
src/shared/formatPath.ts

@@ -1,17 +0,0 @@
-export function formatPath(path: string, os?: string, handleSpace: boolean = true): string {
-	let formattedPath = path
-
-	// Handle path prefix
-	if (os === "win32") {
-		formattedPath = formattedPath.startsWith("\\") ? formattedPath : `\\${formattedPath}`
-	} else {
-		formattedPath = formattedPath.startsWith("/") ? formattedPath : `/${formattedPath}`
-	}
-
-	// Handle space escaping
-	if (handleSpace) {
-		formattedPath = formattedPath.replaceAll(" ", os === "win32" ? "/ " : "\\ ")
-	}
-
-	return formattedPath
-}

+ 2 - 12
src/utils/path.ts

@@ -1,7 +1,6 @@
 import * as path from "path"
 import os from "os"
 import * as vscode from "vscode"
-import { formatPath } from "../shared/formatPath"
 
 /*
 The Node.js 'path' module resolves and normalizes paths differently depending on the platform:
@@ -103,17 +102,8 @@ export function getReadablePath(cwd: string, relPath?: string): string {
 }
 
 export const toRelativePath = (filePath: string, cwd: string) => {
-	// Get the relative path
-	const relativePath = path.relative(cwd, filePath)
-
-	// Add trailing slash if the original path had one
-	const pathWithTrailingSlash =
-		filePath.endsWith("/") || filePath.endsWith("\\")
-			? relativePath + (process.platform === "win32" ? "\\" : "/")
-			: relativePath
-
-	// Format the path based on OS and handle spaces
-	return formatPath(pathWithTrailingSlash, process.platform)
+	const relativePath = path.relative(cwd, filePath).toPosix()
+	return filePath.endsWith("/") ? relativePath + "/" : relativePath
 }
 
 export const getWorkspacePath = (defaultCwdPath = "") => {

+ 15 - 19
webview-ui/src/components/chat/ChatTextArea.tsx

@@ -71,7 +71,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 			listApiConfigMeta,
 			customModes,
 			cwd,
-			osInfo,
 			pinnedApiConfigs,
 			togglePinnedApiConfig,
 		} = useExtensionState()
@@ -188,7 +187,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 					.filter((tab) => tab.path)
 					.map((tab) => ({
 						type: ContextMenuOptionType.OpenedFile,
-						value: tab.path,
+						value: "/" + tab.path,
 					})),
 				...filePaths
 					.map((file) => "/" + file)
@@ -308,7 +307,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 								queryItems,
 								fileSearchResults,
 								getAllModes(customModes),
-								osInfo,
 							)
 							const optionsLength = options.length
 
@@ -345,7 +343,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 							queryItems,
 							fileSearchResults,
 							getAllModes(customModes),
-							osInfo,
 						)[selectedMenuIndex]
 						if (
 							selectedOption &&
@@ -401,20 +398,19 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				}
 			},
 			[
+				onSend,
 				showContextMenu,
-				selectedMenuIndex,
 				searchQuery,
-				selectedType,
-				queryItems,
-				fileSearchResults,
-				customModes,
-				osInfo,
+				selectedMenuIndex,
 				handleMentionSelect,
-				onSend,
+				selectedType,
 				inputValue,
 				cursorPosition,
-				justDeletedSpaceAfterMention,
 				setInputValue,
+				justDeletedSpaceAfterMention,
+				queryItems,
+				customModes,
+				fileSearchResults,
 			],
 		)
 
@@ -628,7 +624,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						for (let i = 0; i < lines.length; i++) {
 							const line = lines[i]
 							// Convert each path to a mention-friendly format
-							const mentionText = convertToMentionPath(line, cwd, osInfo)
+							const mentionText = convertToMentionPath(line, cwd)
 							newValue += mentionText
 							totalLength += mentionText.length
 
@@ -695,15 +691,16 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				}
 			},
 			[
-				textAreaDisabled,
-				inputValue,
 				cursorPosition,
-				setInputValue,
 				cwd,
-				osInfo,
+				inputValue,
+				setInputValue,
+				setCursorPosition,
+				setIntendedCursorPosition,
+				textAreaDisabled,
 				shouldDisableImages,
-				t,
 				setSelectedImages,
+				t,
 			],
 		)
 
@@ -789,7 +786,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 									modes={getAllModes(customModes)}
 									loading={searchLoading}
 									dynamicSearchResults={fileSearchResults}
-									os={osInfo}
 								/>
 							</div>
 						)}

+ 3 - 6
webview-ui/src/components/chat/ContextMenu.tsx

@@ -19,7 +19,6 @@ interface ContextMenuProps {
 	modes?: ModeConfig[]
 	loading?: boolean // New loading prop
 	dynamicSearchResults?: SearchResult[] // New dynamic search results prop
-	os?: string
 }
 
 const ContextMenu: React.FC<ContextMenuProps> = ({
@@ -33,13 +32,12 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
 	modes,
 	loading = false,
 	dynamicSearchResults = [],
-	os,
 }) => {
 	const menuRef = useRef<HTMLDivElement>(null)
 
 	const filteredOptions = useMemo(() => {
-		return getContextMenuOptions(searchQuery, selectedType, queryItems, dynamicSearchResults, modes, os)
-	}, [searchQuery, selectedType, queryItems, dynamicSearchResults, modes, os])
+		return getContextMenuOptions(searchQuery, selectedType, queryItems, dynamicSearchResults, modes)
+	}, [searchQuery, selectedType, queryItems, dynamicSearchResults, modes])
 
 	useEffect(() => {
 		if (menuRef.current) {
@@ -113,8 +111,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
 				if (option.value) {
 					return (
 						<>
-							{/* check os === window to add the leading slash	 */}
-							<span>{os === "win32" ? "\\" : "/"}</span>
+							<span>/</span>
 							{option.value?.startsWith("/.") && <span>.</span>}
 							<span
 								style={{

+ 11 - 14
webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx

@@ -4,7 +4,6 @@ import { useExtensionState } from "../../../context/ExtensionStateContext"
 import { vscode } from "../../../utils/vscode"
 import { defaultModeSlug } from "../../../../../src/shared/modes"
 import * as pathMentions from "../../../utils/path-mentions"
-import { formatPath } from "../../../../../src/shared/formatPath"
 
 // Mock modules
 jest.mock("../../../utils/vscode", () => ({
@@ -17,11 +16,11 @@ 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 (path.startsWith(cwd)) {
+		if (cwd && path.toLowerCase().startsWith(cwd.toLowerCase())) {
 			const relativePath = path.substring(cwd.length)
-			// Ensure there's a slash after the @ symbol when we create the mention path
-			return "@" + formatPath(relativePath, "unix", false)
+			return "@" + (relativePath.startsWith("/") ? relativePath : "/" + relativePath)
 		}
+		return path
 	}),
 }))
 
@@ -68,7 +67,6 @@ describe("ChatTextArea", () => {
 			apiConfiguration: {
 				apiProvider: "anthropic",
 			},
-			osInfo: "unix",
 		})
 	})
 
@@ -194,7 +192,6 @@ describe("ChatTextArea", () => {
 				filePaths: [],
 				openedTabs: [],
 				cwd: mockCwd,
-				osInfo: "unix",
 			})
 			mockConvertToMentionPath.mockClear()
 		})
@@ -220,8 +217,8 @@ describe("ChatTextArea", () => {
 
 			// Verify convertToMentionPath was called for each file path
 			expect(mockConvertToMentionPath).toHaveBeenCalledTimes(2)
-			expect(mockConvertToMentionPath).toHaveBeenCalledWith("/Users/test/project/file1.js", mockCwd, "unix")
-			expect(mockConvertToMentionPath).toHaveBeenCalledWith("/Users/test/project/file2.js", mockCwd, "unix")
+			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
@@ -307,7 +304,7 @@ describe("ChatTextArea", () => {
 			})
 
 			// Verify convertToMentionPath was called with the long path
-			expect(mockConvertToMentionPath).toHaveBeenCalledWith(longPath, mockCwd, "unix")
+			expect(mockConvertToMentionPath).toHaveBeenCalledWith(longPath, mockCwd)
 
 			// The mock implementation will convert it to @/very/long/path/...
 			expect(setInputValue).toHaveBeenCalledWith(
@@ -342,10 +339,10 @@ describe("ChatTextArea", () => {
 
 			// Verify convertToMentionPath was called for each path
 			expect(mockConvertToMentionPath).toHaveBeenCalledTimes(4)
-			expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath1, mockCwd, "unix")
-			expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath2, mockCwd, "unix")
-			expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath3, mockCwd, "unix")
-			expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath4, mockCwd, "unix")
+			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(
@@ -379,7 +376,7 @@ describe("ChatTextArea", () => {
 			})
 
 			// Verify convertToMentionPath was called with the outside path
-			expect(mockConvertToMentionPath).toHaveBeenCalledWith(outsidePath, mockCwd, "unix")
+			expect(mockConvertToMentionPath).toHaveBeenCalledWith(outsidePath, mockCwd)
 
 			// Verify setInputValue was called with the original path
 			expect(setInputValue).toHaveBeenCalledWith("/Users/other/project/file.js ")

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

@@ -118,7 +118,6 @@ export const mergeExtensionState = (prevState: ExtensionState, newState: Extensi
 export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
 	const [state, setState] = useState<ExtensionState>({
 		version: "",
-		osInfo: "",
 		clineMessages: [],
 		taskHistory: [],
 		shouldShowAnnouncement: false,

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

@@ -184,7 +184,6 @@ describe("mergeExtensionState", () => {
 	it("should correctly merge extension states", () => {
 		const baseState: ExtensionState = {
 			version: "",
-			osInfo: "unix",
 			mcpEnabled: false,
 			enableMcpServerCreation: false,
 			clineMessages: [],

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

@@ -3,10 +3,10 @@ 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", () => {
-			// win32-style paths
-			expect(
-				convertToMentionPath("C:\\Users\\user\\project\\file.txt", "C:\\Users\\user\\project", "win32"),
-			).toBe("@\\file.txt")
+			// 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")
@@ -31,9 +31,9 @@ describe("path-mentions", () => {
 		})
 
 		it("should normalize backslashes to forward slashes", () => {
-			expect(
-				convertToMentionPath("C:\\Users\\user\\project\\subdir\\file.txt", "C:\\Users\\user\\project", "win32"),
-			).toBe("@\\subdir\\file.txt")
+			expect(convertToMentionPath("C:\\Users\\user\\project\\subdir\\file.txt", "C:\\Users\\user\\project")).toBe(
+				"@/subdir/file.txt",
+			)
 		})
 
 		it("should handle nested paths correctly", () => {

+ 13 - 25
webview-ui/src/utils/context-mentions.ts

@@ -2,7 +2,6 @@ import { mentionRegex } from "../../../src/shared/context-mentions"
 import { Fzf } from "fzf"
 import { ModeConfig } from "../../../src/shared/modes"
 import * as path from "path"
-import { formatPath } from "../../../src/shared/formatPath"
 
 export interface SearchResult {
 	path: string
@@ -83,24 +82,12 @@ export interface ContextMenuQueryItem {
 	icon?: string
 }
 
-function mapSearchResult(result: SearchResult, os?: string): ContextMenuQueryItem {
-	const formattedPath = formatPath(result.path, os)
-
-	return {
-		type: result.type === "folder" ? ContextMenuOptionType.Folder : ContextMenuOptionType.File,
-		value: formattedPath,
-		label: result.label || path.basename(result.path),
-		description: formattedPath,
-	}
-}
-
 export function getContextMenuOptions(
 	query: string,
 	selectedType: ContextMenuOptionType | null = null,
 	queryItems: ContextMenuQueryItem[],
 	dynamicSearchResults: SearchResult[] = [],
 	modes?: ModeConfig[],
-	os?: string,
 ): ContextMenuQueryItem[] {
 	// Handle slash commands for modes
 	if (query.startsWith("/")) {
@@ -242,24 +229,25 @@ export function getContextMenuOptions(
 	const gitMatches = matchingItems.filter((item) => item.type === ContextMenuOptionType.Git)
 
 	// Convert search results to queryItems format
-	const searchResultItems = dynamicSearchResults.map((result) => mapSearchResult(result, os))
+	const searchResultItems = dynamicSearchResults.map((result) => {
+		const formattedPath = result.path.startsWith("/") ? result.path : `/${result.path}`
+
+		return {
+			type: result.type === "folder" ? ContextMenuOptionType.Folder : ContextMenuOptionType.File,
+			value: formattedPath,
+			label: result.label || path.basename(result.path),
+			description: formattedPath,
+		}
+	})
 
 	const allItems = [...suggestions, ...openedFileMatches, ...searchResultItems, ...gitMatches]
 
 	// Remove duplicates - normalize paths by ensuring all have leading slashes
 	const seen = new Set()
 	const deduped = allItems.filter((item) => {
-		const normalizedValue = item.value
-		let key = ""
-		if (
-			item.type === ContextMenuOptionType.File ||
-			item.type === ContextMenuOptionType.Folder ||
-			item.type === ContextMenuOptionType.OpenedFile
-		) {
-			key = normalizedValue!
-		} else {
-			key = `${item.type}-${normalizedValue}`
-		}
+		// Normalize paths for deduplication by ensuring leading slashes
+		const normalizedValue = item.value && !item.value.startsWith("/") ? `/${item.value}` : item.value
+		const key = `${item.type}-${normalizedValue}`
 		if (seen.has(key)) return false
 		seen.add(key)
 		return true

+ 5 - 7
webview-ui/src/utils/path-mentions.ts

@@ -2,8 +2,6 @@
  * Utilities for handling path-related operations in mentions
  */
 
-import { formatPath } from "../../../src/shared/formatPath"
-
 /**
  * Converts an absolute path to a mention-friendly path
  * If the provided path starts with the current working directory,
@@ -13,16 +11,16 @@ import { formatPath } from "../../../src/shared/formatPath"
  * @param cwd The current working directory
  * @returns A mention-friendly path
  */
-export function convertToMentionPath(path: string, cwd?: string, os?: string): string {
-	const normalizedPath = formatPath(path, os)
-	let normalizedCwd = cwd ? formatPath(cwd, os) : ""
+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 ((os !== "win32" && normalizedCwd.endsWith("/")) || (os === "win32" && normalizedCwd.endsWith("\\"))) {
+	if (normalizedCwd.endsWith("/")) {
 		normalizedCwd = normalizedCwd.slice(0, -1)
 	}
 
@@ -33,7 +31,7 @@ export function convertToMentionPath(path: string, cwd?: string, os?: string): s
 	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 "@" + formatPath(relativePath, os, false)
+		return "@" + (relativePath.startsWith("/") ? relativePath : "/" + relativePath)
 	}
 
 	return path