diff --git a/packages/ui/src/components/prompt-input/usePromptPicker.ts b/packages/ui/src/components/prompt-input/usePromptPicker.ts index 1e4b239f..b0056bc9 100644 --- a/packages/ui/src/components/prompt-input/usePromptPicker.ts +++ b/packages/ui/src/components/prompt-input/usePromptPicker.ts @@ -204,13 +204,16 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr } const folderMention = - relativePath === "." || relativePath === "" - ? "/" - : relativePath.replace(/\/+$/, "") + "/" + relativePath === "." || relativePath === "" || relativePath === "./" + ? "./" + : (relativePath.startsWith("./") ? relativePath.replace(/\/+$/, "") + "/" : relativePath.replace(/^\.\//, "").replace(/\/+$/, "") + "/") const normalizedFolderPath = (() => { const trimmed = relativePath.replace(/\/+$/, "") - return trimmed.length > 0 ? trimmed : "." + // If it's root "./", just return "./" + if (trimmed === "" || trimmed === ".") return "./" + // Otherwise remove any leading ./ and add ./ prefix + return "./" + trimmed.replace(/^\.\//, "") })() const addPathOnlyAttachment = (value: string) => { @@ -237,12 +240,13 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr if (action === "shiftEnter") { // SHIFT+ENTER on directory: keep @path in prompt, add text attachment, remove @ when sending - addPathOnlyAttachment(folderMention) + // Always prefix with ./ for consistency + const normalizedFolderPathWithPrefix = normalizedFolderPath.startsWith("./") ? normalizedFolderPath : "./" + normalizedFolderPath + addPathOnlyAttachment(normalizedFolderPathWithPrefix) replaceMentionToken(mentionText, { trailingSpace: true }) } else { // ENTER/click on directory: attach as a file part pointing at a file:// directory URL. - const dirLabel = - normalizedFolderPath === "." ? "/" : normalizedFolderPath.split("/").pop() || normalizedFolderPath + const dirLabel = normalizedFolderPath === "./" ? "./" : normalizedFolderPath.split("/").pop() || normalizedFolderPath const dirFilename = dirLabel.endsWith("/") ? dirLabel : `${dirLabel}/` const existingAttachments = getAttachments(options.instanceId(), options.sessionId()) @@ -275,10 +279,14 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr if (action === "shiftEnter") { // SHIFT+ENTER on file: keep @path in prompt, add text attachment, remove @ when sending - addPathOnlyAttachment(normalizedPath) - replaceMentionToken(`@${normalizedPath}`, { trailingSpace: true }) + // Always prefix with ./ for consistency + const normalizedPathWithPrefix = normalizedPath.startsWith("./") ? normalizedPath : "./" + normalizedPath + addPathOnlyAttachment(normalizedPathWithPrefix) + replaceMentionToken(`@${normalizedPathWithPrefix}`, { trailingSpace: true }) } else { // ENTER/click on file: attach file (existing behavior). + // Always prefix with ./ for consistency + const normalizedPathWithPrefix = normalizedPath.startsWith("./") ? normalizedPath : "./" + normalizedPath const pathSegments = normalizedPath.split("/") const filename = (() => { const candidate = pathSegments[pathSegments.length - 1] || normalizedPath @@ -287,12 +295,12 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr const existingAttachments = getAttachments(options.instanceId(), options.sessionId()) const alreadyAttached = existingAttachments.some( - (att) => att.source.type === "file" && att.source.path === normalizedPath, + (att) => att.source.type === "file" && att.source.path === normalizedPathWithPrefix, ) if (!alreadyAttached) { const attachment = createFileAttachment( - normalizedPath, + normalizedPathWithPrefix, filename, "text/plain", undefined, @@ -301,7 +309,7 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr addAttachment(options.instanceId(), options.sessionId(), attachment) } - replaceMentionToken(`@${normalizedPath}`, { trailingSpace: true }) + replaceMentionToken(`@${normalizedPathWithPrefix}`, { trailingSpace: true }) } } } diff --git a/packages/ui/src/components/unified-picker.tsx b/packages/ui/src/components/unified-picker.tsx index 81411418..810771b3 100644 --- a/packages/ui/src/components/unified-picker.tsx +++ b/packages/ui/src/components/unified-picker.tsx @@ -51,9 +51,7 @@ function normalizeQuery(rawQuery: string) { if (!trimmed) { return "" } - if (trimmed === "." || trimmed === "./") { - return "" - } + // Don't normalize "." - it's used for workspace root return trimmed.replace(/^(\.\/)+/, "").replace(/^\/+/, "") } @@ -350,18 +348,22 @@ const UnifiedPicker: Component = (props) => { return items } - // Add root directory as first item when query is "/" - if (mode() === "mention" && props.searchQuery === "/") { + // Add root directory as first item only when query is EXACTLY "." or "./" (not "./docs/") + const isExactRootQuery = props.searchQuery === "." || props.searchQuery === "./" + if (mode() === "mention" && isExactRootQuery) { const rootFile: FileItem = { - path: "/", - relativePath: "/", + path: ".", + relativePath: ".", isDirectory: true, isGitFile: false, } items.push({ type: "file", file: rootFile }) } - filteredAgents().forEach((agent) => items.push({ type: "agent", agent })) + // Don't show agents for exact root path queries + if (!isExactRootQuery) { + filteredAgents().forEach((agent) => items.push({ type: "agent", agent })) + } files().forEach((file) => items.push({ type: "file", file })) return items } @@ -485,7 +487,7 @@ const UnifiedPicker: Component = (props) => { - 0}> + 0 && !(props.searchQuery === "." || props.searchQuery === "./")}> @@ -540,11 +542,11 @@ const UnifiedPicker: Component = (props) => { - 0}> + 0 || props.searchQuery === "." || props.searchQuery === "./")}> - +
= (props) => { data-picker-selected={selectedIndex() === 0} onClick={() => { const rootFile: FileItem = { - path: "/", - relativePath: "/", + path: ".", + relativePath: ".", isDirectory: true, isGitFile: false, } @@ -569,7 +571,7 @@ const UnifiedPicker: Component = (props) => { d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /> - / (root) + . (workspace root)
diff --git a/packages/ui/src/lib/prompt-placeholders.ts b/packages/ui/src/lib/prompt-placeholders.ts index 3e07f574..008eef09 100644 --- a/packages/ui/src/lib/prompt-placeholders.ts +++ b/packages/ui/src/lib/prompt-placeholders.ts @@ -23,15 +23,52 @@ export function resolvePastedPlaceholders(prompt: string, attachments: Attachmen let result = prompt // For each path attachment (SHIFT+ENTER), find and replace @path with path in the prompt - // We ALWAYS strip @ for SHIFT+ENTER paths, even if there's also a file attachment for (const path of pathAttachments) { - // Try both with and without trailing slash - const variants = [path, path + "/"] + if (!path) continue + + // The path should already have ./ prefix from usePromptPicker + // We need to find @path in prompt and replace with path + + // For "./docs/" path, try to match @docs/, @./docs/, @docs, etc. + const basePath = path.replace(/^\.\//, "").replace(/\/+$/, "") // "docs" + const withSlash = basePath + "/" // "docs/" + + const patterns = [ + "@" + path, // @./docs/ + "@" + basePath, // @docs + "@" + withSlash, // @docs/ + ] + + for (const pattern of patterns) { + if (result.includes(pattern)) { + result = result.replace(pattern, path) + } + } + } - for (const variant of variants) { - // Replace @path with path (exact match) - const searchPattern = "@" + variant - result = result.split(searchPattern).join(variant) + // Also strip @ for paths that have file attachments (ENTER case) + for (const filePath of fileAttachments) { + if (!filePath || filePath.length === 0) continue + + // Special case: if attachment is "./" or ".", handle separately + if (filePath === "./" || filePath === ".") { + result = result.replace("@./", "./") + result = result.replace("@.", "./") + continue + } + + // Normal path handling + const pathToFind = filePath.replace(/^\.\//, "") + const patterns = [ + "@" + filePath, + "@./" + pathToFind, + "@" + pathToFind, + ] + + for (const pattern of patterns) { + if (result.includes(pattern)) { + result = result.replace(pattern, filePath) + } } }