import { createSignal, Show, onMount, createEffect, For } from "solid-js" import AgentSelector from "./agent-selector" import ModelSelector from "./model-selector" import FilePicker from "./file-picker" import AttachmentChip from "./attachment-chip" import { addToHistory, getHistory } from "../stores/message-history" import { getAttachments, addAttachment, removeAttachment, clearAttachments } from "../stores/attachments" import { createFileAttachment } from "../types/attachment" import type { Attachment } from "../types/attachment" import Kbd from "./kbd" import HintRow from "./hint-row" import { isMac } from "../lib/keyboard-utils" import { getActiveInstance } from "../stores/instances" interface PromptInputProps { instanceId: string instanceFolder: string sessionId: string onSend: (prompt: string, attachments: Attachment[]) => Promise disabled?: boolean agent: string model: { providerId: string; modelId: string } onAgentChange: (agent: string) => Promise onModelChange: (model: { providerId: string; modelId: string }) => Promise } export default function PromptInput(props: PromptInputProps) { const [prompt, setPrompt] = createSignal("") const [sending, setSending] = createSignal(false) const [history, setHistory] = createSignal([]) const [historyIndex, setHistoryIndex] = createSignal(-1) const [isFocused, setIsFocused] = createSignal(false) const [showFilePicker, setShowFilePicker] = createSignal(false) const [fileSearchQuery, setFileSearchQuery] = createSignal("") const [atPosition, setAtPosition] = createSignal(null) const [isDragging, setIsDragging] = createSignal(false) const [ignoredAtPositions, setIgnoredAtPositions] = createSignal>(new Set()) let textareaRef: HTMLTextAreaElement | undefined let containerRef: HTMLDivElement | undefined const attachments = () => getAttachments(props.instanceId, props.sessionId) onMount(async () => { const loaded = await getHistory(props.instanceFolder) setHistory(loaded) }) function handleKeyDown(e: KeyboardEvent) { if (e.key === "Enter" && !e.shiftKey && !showFilePicker()) { e.preventDefault() handleSend() return } const textarea = textareaRef if (!textarea) return const atStart = textarea.selectionStart === 0 && textarea.selectionEnd === 0 const currentHistory = history() if (e.key === "ArrowUp" && !showFilePicker() && atStart && currentHistory.length > 0) { e.preventDefault() const newIndex = historyIndex() === -1 ? 0 : Math.min(historyIndex() + 1, currentHistory.length - 1) setHistoryIndex(newIndex) setPrompt(currentHistory[newIndex]) setTimeout(() => { textarea.style.height = "auto" textarea.style.height = Math.min(textarea.scrollHeight, 200) + "px" }, 0) return } if (e.key === "ArrowDown" && !showFilePicker() && historyIndex() >= 0) { e.preventDefault() const newIndex = historyIndex() - 1 if (newIndex >= 0) { setHistoryIndex(newIndex) setPrompt(currentHistory[newIndex]) } else { setHistoryIndex(-1) setPrompt("") } setTimeout(() => { textarea.style.height = "auto" textarea.style.height = Math.min(textarea.scrollHeight, 200) + "px" }, 0) return } } async function handleSend() { const text = prompt().trim() const currentAttachments = attachments() if (!text || sending() || props.disabled) return setSending(true) try { await addToHistory(props.instanceFolder, text) const updated = await getHistory(props.instanceFolder) setHistory(updated) setHistoryIndex(-1) await props.onSend(text, currentAttachments) setPrompt("") clearAttachments(props.instanceId, props.sessionId) setIgnoredAtPositions(new Set()) if (textareaRef) { textareaRef.style.height = "auto" } } catch (error) { console.error("Failed to send message:", error) alert("Failed to send message: " + (error instanceof Error ? error.message : String(error))) } finally { setSending(false) textareaRef?.focus() } } function handleInput(e: Event) { const target = e.target as HTMLTextAreaElement const value = target.value setPrompt(value) setHistoryIndex(-1) target.style.height = "auto" target.style.height = Math.min(target.scrollHeight, 200) + "px" const cursorPos = target.selectionStart const textBeforeCursor = value.substring(0, cursorPos) const lastAtIndex = textBeforeCursor.lastIndexOf("@") if (lastAtIndex !== -1 && !ignoredAtPositions().has(lastAtIndex)) { const textAfterAt = value.substring(lastAtIndex + 1, cursorPos) const hasSpace = textAfterAt.includes(" ") || textAfterAt.includes("\n") if (!hasSpace && cursorPos === lastAtIndex + textAfterAt.length + 1) { setAtPosition(lastAtIndex) setFileSearchQuery(textAfterAt) setShowFilePicker(true) return } } setShowFilePicker(false) setAtPosition(null) } function handleFileSelect(path: string) { const filename = path.split("/").pop() || path const attachment = createFileAttachment(path, filename) addAttachment(props.instanceId, props.sessionId, attachment) const currentPrompt = prompt() const pos = atPosition() const cursorPos = textareaRef?.selectionStart || 0 if (pos !== null) { const before = currentPrompt.substring(0, pos) const after = currentPrompt.substring(cursorPos) const newPrompt = before + " " + after setPrompt(newPrompt) setTimeout(() => { if (textareaRef) { const newCursorPos = pos + 1 textareaRef.setSelectionRange(newCursorPos, newCursorPos) } }, 0) } setShowFilePicker(false) setAtPosition(null) setFileSearchQuery("") textareaRef?.focus() } function handleFilePickerClose() { const pos = atPosition() if (pos !== null) { setIgnoredAtPositions((prev) => new Set(prev).add(pos)) } setShowFilePicker(false) setAtPosition(null) setFileSearchQuery("") setTimeout(() => textareaRef?.focus(), 0) } function handleFilePickerNavigate(_direction: "up" | "down") {} function handleRemoveAttachment(attachmentId: string) { removeAttachment(props.instanceId, props.sessionId, attachmentId) } function handleDragOver(e: DragEvent) { e.preventDefault() e.stopPropagation() setIsDragging(true) } function handleDragLeave(e: DragEvent) { e.preventDefault() e.stopPropagation() setIsDragging(false) } function handleDrop(e: DragEvent) { e.preventDefault() e.stopPropagation() setIsDragging(false) const files = e.dataTransfer?.files if (!files || files.length === 0) return for (let i = 0; i < files.length; i++) { const file = files[i] const path = (file as any).path || file.name const filename = file.name const mime = file.type || "text/plain" const attachment = createFileAttachment(path, filename, mime) addAttachment(props.instanceId, props.sessionId, attachment) } textareaRef?.focus() } const canSend = () => (prompt().trim().length > 0 || attachments().length > 0) && !sending() && !props.disabled const instance = () => getActiveInstance() return (
0}>
{(att) => handleRemoveAttachment(att.id)} />}