fix(ui): fix ./ path prefix for SHIFT+ENTER

This commit is contained in:
VooDisss
2026-02-16 04:29:24 +02:00
parent f58267dd30
commit b31135f622
3 changed files with 81 additions and 34 deletions

View File

@@ -204,13 +204,16 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr
} }
const folderMention = const folderMention =
relativePath === "." || relativePath === "" relativePath === "." || relativePath === "" || relativePath === "./"
? "/" ? "./"
: relativePath.replace(/\/+$/, "") + "/" : (relativePath.startsWith("./") ? relativePath.replace(/\/+$/, "") + "/" : relativePath.replace(/^\.\//, "").replace(/\/+$/, "") + "/")
const normalizedFolderPath = (() => { const normalizedFolderPath = (() => {
const trimmed = relativePath.replace(/\/+$/, "") 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) => { const addPathOnlyAttachment = (value: string) => {
@@ -237,12 +240,13 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr
if (action === "shiftEnter") { if (action === "shiftEnter") {
// SHIFT+ENTER on directory: keep @path in prompt, add text attachment, remove @ when sending // 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 }) replaceMentionToken(mentionText, { trailingSpace: true })
} else { } else {
// ENTER/click on directory: attach as a file part pointing at a file:// directory URL. // ENTER/click on directory: attach as a file part pointing at a file:// directory URL.
const dirLabel = const dirLabel = normalizedFolderPath === "./" ? "./" : normalizedFolderPath.split("/").pop() || normalizedFolderPath
normalizedFolderPath === "." ? "/" : normalizedFolderPath.split("/").pop() || normalizedFolderPath
const dirFilename = dirLabel.endsWith("/") ? dirLabel : `${dirLabel}/` const dirFilename = dirLabel.endsWith("/") ? dirLabel : `${dirLabel}/`
const existingAttachments = getAttachments(options.instanceId(), options.sessionId()) const existingAttachments = getAttachments(options.instanceId(), options.sessionId())
@@ -275,10 +279,14 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr
if (action === "shiftEnter") { if (action === "shiftEnter") {
// SHIFT+ENTER on file: keep @path in prompt, add text attachment, remove @ when sending // SHIFT+ENTER on file: keep @path in prompt, add text attachment, remove @ when sending
addPathOnlyAttachment(normalizedPath) // Always prefix with ./ for consistency
replaceMentionToken(`@${normalizedPath}`, { trailingSpace: true }) const normalizedPathWithPrefix = normalizedPath.startsWith("./") ? normalizedPath : "./" + normalizedPath
addPathOnlyAttachment(normalizedPathWithPrefix)
replaceMentionToken(`@${normalizedPathWithPrefix}`, { trailingSpace: true })
} else { } else {
// ENTER/click on file: attach file (existing behavior). // ENTER/click on file: attach file (existing behavior).
// Always prefix with ./ for consistency
const normalizedPathWithPrefix = normalizedPath.startsWith("./") ? normalizedPath : "./" + normalizedPath
const pathSegments = normalizedPath.split("/") const pathSegments = normalizedPath.split("/")
const filename = (() => { const filename = (() => {
const candidate = pathSegments[pathSegments.length - 1] || normalizedPath 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 existingAttachments = getAttachments(options.instanceId(), options.sessionId())
const alreadyAttached = existingAttachments.some( const alreadyAttached = existingAttachments.some(
(att) => att.source.type === "file" && att.source.path === normalizedPath, (att) => att.source.type === "file" && att.source.path === normalizedPathWithPrefix,
) )
if (!alreadyAttached) { if (!alreadyAttached) {
const attachment = createFileAttachment( const attachment = createFileAttachment(
normalizedPath, normalizedPathWithPrefix,
filename, filename,
"text/plain", "text/plain",
undefined, undefined,
@@ -301,7 +309,7 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr
addAttachment(options.instanceId(), options.sessionId(), attachment) addAttachment(options.instanceId(), options.sessionId(), attachment)
} }
replaceMentionToken(`@${normalizedPath}`, { trailingSpace: true }) replaceMentionToken(`@${normalizedPathWithPrefix}`, { trailingSpace: true })
} }
} }
} }

View File

@@ -51,9 +51,7 @@ function normalizeQuery(rawQuery: string) {
if (!trimmed) { if (!trimmed) {
return "" return ""
} }
if (trimmed === "." || trimmed === "./") { // Don't normalize "." - it's used for workspace root
return ""
}
return trimmed.replace(/^(\.\/)+/, "").replace(/^\/+/, "") return trimmed.replace(/^(\.\/)+/, "").replace(/^\/+/, "")
} }
@@ -350,18 +348,22 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (props) => {
return items return items
} }
// Add root directory as first item when query is "/" // Add root directory as first item only when query is EXACTLY "." or "./" (not "./docs/")
if (mode() === "mention" && props.searchQuery === "/") { const isExactRootQuery = props.searchQuery === "." || props.searchQuery === "./"
if (mode() === "mention" && isExactRootQuery) {
const rootFile: FileItem = { const rootFile: FileItem = {
path: "/", path: ".",
relativePath: "/", relativePath: ".",
isDirectory: true, isDirectory: true,
isGitFile: false, isGitFile: false,
} }
items.push({ type: "file", file: rootFile }) 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 })) files().forEach((file) => items.push({ type: "file", file }))
return items return items
} }
@@ -485,7 +487,7 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (props) => {
</For> </For>
</Show> </Show>
<Show when={mode() === "mention" && agentCount() > 0}> <Show when={mode() === "mention" && agentCount() > 0 && !(props.searchQuery === "." || props.searchQuery === "./")}>
<div class="dropdown-section-header"> <div class="dropdown-section-header">
{t("unifiedPicker.sections.agents")} {t("unifiedPicker.sections.agents")}
</div> </div>
@@ -540,11 +542,11 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (props) => {
</For> </For>
</Show> </Show>
<Show when={mode() === "mention" && fileCount() > 0}> <Show when={mode() === "mention" && (fileCount() > 0 || props.searchQuery === "." || props.searchQuery === "./")}>
<div class="dropdown-section-header"> <div class="dropdown-section-header">
{props.searchQuery === "/" ? t("unifiedPicker.sections.directories") : t("unifiedPicker.sections.files")} {t("unifiedPicker.sections.files")}
</div> </div>
<Show when={props.searchQuery === "/"}> <Show when={props.searchQuery === "." || props.searchQuery === "./"}>
<div <div
class={`dropdown-item py-1.5 ${ class={`dropdown-item py-1.5 ${
selectedIndex() === 0 ? "dropdown-item-highlight" : "" selectedIndex() === 0 ? "dropdown-item-highlight" : ""
@@ -552,8 +554,8 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (props) => {
data-picker-selected={selectedIndex() === 0} data-picker-selected={selectedIndex() === 0}
onClick={() => { onClick={() => {
const rootFile: FileItem = { const rootFile: FileItem = {
path: "/", path: ".",
relativePath: "/", relativePath: ".",
isDirectory: true, isDirectory: true,
isGitFile: false, isGitFile: false,
} }
@@ -569,7 +571,7 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (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" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
/> />
</svg> </svg>
<span class="font-mono">/ (root)</span> <span class="font-mono">. (workspace root)</span>
</div> </div>
</div> </div>
</Show> </Show>

View File

@@ -23,15 +23,52 @@ export function resolvePastedPlaceholders(prompt: string, attachments: Attachmen
let result = prompt let result = prompt
// For each path attachment (SHIFT+ENTER), find and replace @path with path in the 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) { for (const path of pathAttachments) {
// Try both with and without trailing slash if (!path) continue
const variants = [path, path + "/"]
// 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) { // Also strip @ for paths that have file attachments (ENTER case)
// Replace @path with path (exact match) for (const filePath of fileAttachments) {
const searchPattern = "@" + variant if (!filePath || filePath.length === 0) continue
result = result.split(searchPattern).join(variant)
// 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)
}
} }
} }