fix(ui): improve picker deletion, ESC cancel, and SHIFT+ENTER path handling
This commit is contained in:
@@ -183,9 +183,25 @@ export function usePromptKeyDown(options: UsePromptKeyDownOptions) {
|
|||||||
|
|
||||||
if (isDeletingFromEnd || isDeletingFromStart || isSelected) {
|
if (isDeletingFromEnd || isDeletingFromStart || isSelected) {
|
||||||
const currentAttachments = options.getAttachments()
|
const currentAttachments = options.getAttachments()
|
||||||
const attachment = currentAttachments.find(
|
const attachment = currentAttachments.find((a) => {
|
||||||
(a) => (a.source.type === "file" || a.source.type === "agent") && a.filename === name,
|
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) {
|
if (attachment) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -205,6 +221,14 @@ export function usePromptKeyDown(options: UsePromptKeyDownOptions) {
|
|||||||
textarea.setSelectionRange(mentionStart, mentionStart)
|
textarea.setSelectionRange(mentionStart, mentionStart)
|
||||||
}, 0)
|
}, 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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,8 +236,7 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr
|
|||||||
const mentionText = `@${folderMention}`
|
const mentionText = `@${folderMention}`
|
||||||
|
|
||||||
if (action === "shiftEnter") {
|
if (action === "shiftEnter") {
|
||||||
// SHIFT+ENTER on directory: attach path as text only.
|
// SHIFT+ENTER on directory: keep @path in prompt (BACKSPACE works), remove @ when sending
|
||||||
addPathOnlyAttachment(folderMention)
|
|
||||||
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.
|
||||||
@@ -274,8 +273,7 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action === "shiftEnter") {
|
if (action === "shiftEnter") {
|
||||||
// SHIFT+ENTER on file: attach path as text only.
|
// SHIFT+ENTER on file: keep @path in prompt (BACKSPACE works), remove @ when sending
|
||||||
addPathOnlyAttachment(normalizedPath)
|
|
||||||
replaceMentionToken(`@${normalizedPath}`, { trailingSpace: true })
|
replaceMentionToken(`@${normalizedPath}`, { trailingSpace: true })
|
||||||
} else {
|
} else {
|
||||||
// ENTER/click on file: attach file (existing behavior).
|
// ENTER/click on file: attach file (existing behavior).
|
||||||
@@ -316,6 +314,25 @@ export function usePromptPicker(options: PromptPickerOptions): PromptPickerContr
|
|||||||
const pos = atPosition()
|
const pos = atPosition()
|
||||||
if (pickerMode() === "mention" && pos !== null) {
|
if (pickerMode() === "mention" && pos !== null) {
|
||||||
setIgnoredAtPositions((prev) => new Set(prev).add(pos))
|
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)
|
setShowPicker(false)
|
||||||
setAtPosition(null)
|
setAtPosition(null)
|
||||||
|
|||||||
@@ -268,6 +268,13 @@ const UnifiedPicker: Component<UnifiedPickerProps> = (props) => {
|
|||||||
const workspaceChanged = lastWorkspaceId !== props.workspaceId
|
const workspaceChanged = lastWorkspaceId !== props.workspaceId
|
||||||
const queryChanged = lastQuery !== props.searchQuery
|
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) {
|
if (!isInitialized() || workspaceChanged || queryChanged) {
|
||||||
setIsInitialized(true)
|
setIsInitialized(true)
|
||||||
lastWorkspaceId = props.workspaceId
|
lastWorkspaceId = props.workspaceId
|
||||||
|
|||||||
@@ -1,12 +1,34 @@
|
|||||||
import type { Attachment } from "../types/attachment"
|
import type { Attachment } from "../types/attachment"
|
||||||
|
|
||||||
export function resolvePastedPlaceholders(prompt: string, attachments: Attachment[] = []): string {
|
export function resolvePastedPlaceholders(prompt: string, attachments: Attachment[] = []): string {
|
||||||
if (!prompt || !prompt.includes("[pasted #")) {
|
if (!prompt) {
|
||||||
return 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) {
|
if (!attachments || attachments.length === 0) {
|
||||||
return prompt
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
const lookup = new Map<string, string>()
|
const lookup = new Map<string, string>()
|
||||||
@@ -26,10 +48,10 @@ export function resolvePastedPlaceholders(prompt: string, attachments: Attachmen
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (lookup.size === 0) {
|
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)
|
const replacement = lookup.get(fullMatch)
|
||||||
return typeof replacement === "string" ? replacement : fullMatch
|
return typeof replacement === "string" ? replacement : fullMatch
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user