fix(ui): improve picker deletion, ESC cancel, and SHIFT+ENTER path handling

This commit is contained in:
VooDisss
2026-02-14 06:53:31 +02:00
parent 1ef01da019
commit 8f6d4c8b09
4 changed files with 81 additions and 11 deletions

View File

@@ -183,9 +183,25 @@ export function usePromptKeyDown(options: UsePromptKeyDownOptions) {
if (isDeletingFromEnd || isDeletingFromStart || isSelected) {
const currentAttachments = options.getAttachments()
const attachment = currentAttachments.find(
(a) => (a.source.type === "file" || a.source.type === "agent") && a.filename === name,
)
const attachment = currentAttachments.find((a) => {
if (a.source.type === "agent") {
return a.filename === name
}
if (a.source.type === "file") {
// Match either by filename (basename) or by path (for full paths like @docs/file.txt)
return (
a.filename === name ||
a.source.path === name ||
a.source.path.endsWith("/" + name) ||
a.source.path === name.replace(/\/$/, "")
)
}
if (a.source.type === "text") {
// For text attachments (path-only mentions), match by value
return a.source.value === name || a.source.value.endsWith("/" + name)
}
return false
})
if (attachment) {
e.preventDefault()
@@ -205,6 +221,14 @@ export function usePromptKeyDown(options: UsePromptKeyDownOptions) {
textarea.setSelectionRange(mentionStart, mentionStart)
}, 0)
// Check if there are any @ remaining in the text - if not, close the picker
if (!newText.includes("@") && options.isPickerOpen()) {
options.closePicker()
// Clear ignoredAtPositions since we deleted the entire @mention
// This ensures typing @ again will open the picker
options.setIgnoredAtPositions(new Set())
}
return
}
}

View File

@@ -236,8 +236,7 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr
const mentionText = `@${folderMention}`
if (action === "shiftEnter") {
// SHIFT+ENTER on directory: attach path as text only.
addPathOnlyAttachment(folderMention)
// SHIFT+ENTER on directory: keep @path in prompt (BACKSPACE works), remove @ when sending
replaceMentionToken(mentionText, { trailingSpace: true })
} else {
// ENTER/click on directory: attach as a file part pointing at a file:// directory URL.
@@ -274,8 +273,7 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr
}
if (action === "shiftEnter") {
// SHIFT+ENTER on file: attach path as text only.
addPathOnlyAttachment(normalizedPath)
// SHIFT+ENTER on file: keep @path in prompt (BACKSPACE works), remove @ when sending
replaceMentionToken(`@${normalizedPath}`, { trailingSpace: true })
} else {
// ENTER/click on file: attach file (existing behavior).
@@ -316,6 +314,25 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr
const pos = atPosition()
if (pickerMode() === "mention" && pos !== null) {
setIgnoredAtPositions((prev) => new Set(prev).add(pos))
// Remove the partial @mention text from the textarea when ESC is pressed
const textarea = options.getTextarea()
if (textarea) {
const currentPrompt = options.prompt()
const cursorPos = textarea.selectionStart
// Remove text from @ position to cursor position
const before = currentPrompt.substring(0, pos)
const after = currentPrompt.substring(cursorPos)
options.setPrompt(before + after)
// Restore cursor position to where @ was
setTimeout(() => {
const nextTextarea = options.getTextarea()
if (nextTextarea) {
nextTextarea.setSelectionRange(pos, pos)
}
}, 0)
}
}
setShowPicker(false)
setAtPosition(null)

View File

@@ -268,6 +268,13 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (props) => {
const workspaceChanged = lastWorkspaceId !== props.workspaceId
const queryChanged = lastQuery !== props.searchQuery
if (queryChanged) {
// Reset selectedIndex to 0 when query changes to avoid ghost state
// This ensures proper highlighting when navigating back to root or changing queries
setSelectedIndex(0)
resetScrollPosition()
}
if (!isInitialized() || workspaceChanged || queryChanged) {
setIsInitialized(true)
lastWorkspaceId = props.workspaceId

View File

@@ -1,12 +1,34 @@
import type { Attachment } from "../types/attachment"
export function resolvePastedPlaceholders(prompt: string, attachments: Attachment[] = []): string {
if (!prompt || !prompt.includes("[pasted #")) {
if (!prompt) {
return prompt
}
// First, strip @ from file/directory paths that don't have file attachments
// This handles SHIFT+ENTER case where @path should become path
const fileAttachments = new Set(
attachments
.filter((a) => a.source.type === "file" && "path" in a.source)
.map((a) => (a.source as { path: string }).path),
)
let result = prompt.replace(/@([^\s@]+)/g, (match, path) => {
// If this path has a file attachment, keep the @ (attachment is sent separately)
if (fileAttachments.has(path) || fileAttachments.has(path.replace(/\/$/, ""))) {
return match
}
// Otherwise (SHIFT+ENTER case), strip the @
return path
})
// Then, resolve [pasted #N] placeholders
if (!result.includes("[pasted #")) {
return result
}
if (!attachments || attachments.length === 0) {
return prompt
return result
}
const lookup = new Map<string, string>()
@@ -26,10 +48,10 @@ export function resolvePastedPlaceholders(prompt: string, attachments: Attachmen
}
if (lookup.size === 0) {
return prompt
return result
}
return prompt.replace(/\[pasted #(\d+)\]/g, (fullMatch) => {
return result.replace(/\[pasted #(\d+)\]/g, (fullMatch) => {
const replacement = lookup.get(fullMatch)
return typeof replacement === "string" ? replacement : fullMatch
})