/** * PromptInput is the message composer: Enter sends (Shift+Enter for a new * line); while a turn runs the send button becomes a stop button, matching * the behaviour of Zed's agent panel. Typing "@" opens the workspace file * selector (the pick is sent as an ACP resource_link attachment) and a * leading "/" opens the skill / command selector. * * @example * …} onCancel={() => {}} /> */ import { type KeyboardEvent, useEffect, useRef, useState } from "react"; import { type CompletionItem, activeAttachments, applyCompletion, } from "../mentions"; import type { AcpAvailableCommand, PromptAttachment } from "../protocol"; import { useCompletion } from "../useCompletion"; import { CompletionPopup } from "./CompletionPopup"; interface PromptInputProps { turnActive: boolean; /** Slash commands announced by the agent, merged into the "/" popup. */ commands: AcpAvailableCommand[]; onSend: (text: string, attachments: PromptAttachment[]) => void; onCancel: () => void; } export function PromptInput({ turnActive, commands, onSend, onCancel, }: PromptInputProps) { const [text, setText] = useState(""); const [caret, setCaret] = useState(0); const [attachments, setAttachments] = useState([]); const textareaRef = useRef(null); const pendingCaret = useRef(null); const completion = useCompletion({ text, caret, commands }); // After a completion the caret must land right after the insertion; the // pending value is consumed on the render that follows the text update. useEffect(() => { if (pendingCaret.current !== null && textareaRef.current) { textareaRef.current.setSelectionRange( pendingCaret.current, pendingCaret.current, ); pendingCaret.current = null; } }); const syncCaret = () => { setCaret(textareaRef.current?.selectionStart ?? text.length); }; const pick = (item: CompletionItem) => { if (!completion.trigger) { return; } const next = applyCompletion(text, completion.trigger, item.insert); setText(next.text); setCaret(next.caret); pendingCaret.current = next.caret; if (item.attachment) { setAttachments((current) => [ ...current, item.attachment as PromptAttachment, ]); } textareaRef.current?.focus(); }; const submit = () => { const trimmed = text.trim(); if (trimmed === "" || turnActive) { return; } onSend(trimmed, activeAttachments(trimmed, attachments)); setText(""); setCaret(0); setAttachments([]); }; const onKeyDown = (event: KeyboardEvent) => { if (completion.open && handleCompletionKey(event, completion, pick)) { event.preventDefault(); return; } if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); submit(); } }; return (
{ event.preventDefault(); submit(); }} >
{completion.open && ( )}