fix(ui): fix ./ path prefix for SHIFT+ENTER
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<UnifiedPickerProps> = (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<UnifiedPickerProps> = (props) => {
|
||||
</For>
|
||||
</Show>
|
||||
|
||||
<Show when={mode() === "mention" && agentCount() > 0}>
|
||||
<Show when={mode() === "mention" && agentCount() > 0 && !(props.searchQuery === "." || props.searchQuery === "./")}>
|
||||
<div class="dropdown-section-header">
|
||||
{t("unifiedPicker.sections.agents")}
|
||||
</div>
|
||||
@@ -540,11 +542,11 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (props) => {
|
||||
</For>
|
||||
</Show>
|
||||
|
||||
<Show when={mode() === "mention" && fileCount() > 0}>
|
||||
<Show when={mode() === "mention" && (fileCount() > 0 || props.searchQuery === "." || props.searchQuery === "./")}>
|
||||
<div class="dropdown-section-header">
|
||||
{props.searchQuery === "/" ? t("unifiedPicker.sections.directories") : t("unifiedPicker.sections.files")}
|
||||
{t("unifiedPicker.sections.files")}
|
||||
</div>
|
||||
<Show when={props.searchQuery === "/"}>
|
||||
<Show when={props.searchQuery === "." || props.searchQuery === "./"}>
|
||||
<div
|
||||
class={`dropdown-item py-1.5 ${
|
||||
selectedIndex() === 0 ? "dropdown-item-highlight" : ""
|
||||
@@ -552,8 +554,8 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (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<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"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-mono">/ (root)</span>
|
||||
<span class="font-mono">. (workspace root)</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user