Improve file picker UX: inline chips, focus preservation, escape behavior

- Move attachment chips inside textarea wrapper (appears inline)
- Keep textarea focused when pressing Escape (no focus stealing)
- Track ignored @ positions after Escape to prevent re-triggering picker
- Clear ignored positions on message send
- Use capture phase for Escape key handling (true flag on addEventListener)
- Escape now works like email: type @example.com naturally after dismissing picker
- Chips show above textarea input with better spacing (pt-2 pb-1)
This commit is contained in:
Shantur Rathore
2025-10-24 00:27:36 +01:00
parent d735067042
commit fb9d3a08eb
2 changed files with 26 additions and 14 deletions

View File

@@ -174,6 +174,14 @@ const FilePicker: Component<FilePickerProps> = (props) => {
const listener = (e: KeyboardEvent) => {
if (!props.open) return
const fileList = files()
if (e.key === "Escape") {
e.preventDefault()
e.stopPropagation()
props.onClose()
return
}
if (fileList.length === 0) return
if (e.key === "ArrowDown") {
@@ -189,14 +197,11 @@ const FilePicker: Component<FilePickerProps> = (props) => {
if (fileList[selectedIndex()]) {
handleSelect(fileList[selectedIndex()].path)
}
} else if (e.key === "Escape") {
e.preventDefault()
props.onClose()
}
}
document.addEventListener("keydown", listener)
onCleanup(() => document.removeEventListener("keydown", listener))
document.addEventListener("keydown", listener, true)
onCleanup(() => document.removeEventListener("keydown", listener, true))
})
return (

View File

@@ -34,6 +34,7 @@ export default function PromptInput(props: PromptInputProps) {
const [fileSearchQuery, setFileSearchQuery] = createSignal("")
const [atPosition, setAtPosition] = createSignal<number | null>(null)
const [isDragging, setIsDragging] = createSignal(false)
const [ignoredAtPositions, setIgnoredAtPositions] = createSignal<Set<number>>(new Set())
let textareaRef: HTMLTextAreaElement | undefined
let containerRef: HTMLDivElement | undefined
@@ -103,6 +104,7 @@ export default function PromptInput(props: PromptInputProps) {
await props.onSend(text, currentAttachments)
setPrompt("")
clearAttachments(props.instanceId, props.sessionId)
setIgnoredAtPositions(new Set<number>())
if (textareaRef) {
textareaRef.style.height = "auto"
@@ -129,7 +131,7 @@ export default function PromptInput(props: PromptInputProps) {
const textBeforeCursor = value.substring(0, cursorPos)
const lastAtIndex = textBeforeCursor.lastIndexOf("@")
if (lastAtIndex !== -1) {
if (lastAtIndex !== -1 && !ignoredAtPositions().has(lastAtIndex)) {
const textAfterAt = value.substring(lastAtIndex + 1, cursorPos)
const hasSpace = textAfterAt.includes(" ") || textAfterAt.includes("\n")
@@ -176,10 +178,14 @@ export default function PromptInput(props: PromptInputProps) {
}
function handleFilePickerClose() {
const pos = atPosition()
if (pos !== null) {
setIgnoredAtPositions((prev) => new Set(prev).add(pos))
}
setShowFilePicker(false)
setAtPosition(null)
setFileSearchQuery("")
textareaRef?.focus()
setTimeout(() => textareaRef?.focus(), 0)
}
function handleFilePickerNavigate(_direction: "up" | "down") {}
@@ -227,13 +233,6 @@ export default function PromptInput(props: PromptInputProps) {
return (
<div class="prompt-input-container">
<Show when={attachments().length > 0}>
<div class="flex flex-wrap gap-2 border-b border-gray-200 p-2 dark:border-gray-700">
<For each={attachments()}>
{(att) => <AttachmentChip attachment={att} onRemove={() => handleRemoveAttachment(att.id)} />}
</For>
</div>
</Show>
<div
ref={containerRef}
class={`prompt-input-wrapper relative ${isDragging() ? "border-2 border-blue-500 bg-blue-50 dark:bg-blue-900/10" : ""}`}
@@ -253,6 +252,14 @@ export default function PromptInput(props: PromptInputProps) {
/>
</Show>
<Show when={attachments().length > 0}>
<div class="flex flex-wrap gap-1 px-3 pt-2 pb-1">
<For each={attachments()}>
{(att) => <AttachmentChip attachment={att} onRemove={() => handleRemoveAttachment(att.id)} />}
</For>
</div>
</Show>
<textarea
ref={textareaRef}
class="prompt-input"