diff --git a/src/components/prompt-input.tsx b/src/components/prompt-input.tsx index 24c585b6..6cd2ee34 100644 --- a/src/components/prompt-input.tsx +++ b/src/components/prompt-input.tsx @@ -16,6 +16,7 @@ interface PromptInputProps { instanceFolder: string sessionId: string onSend: (prompt: string, attachments: Attachment[]) => Promise + onRunShell?: (command: string) => Promise disabled?: boolean escapeInDebounce?: boolean } @@ -33,6 +34,7 @@ export default function PromptInput(props: PromptInputProps) { const [ignoredAtPositions, setIgnoredAtPositions] = createSignal>(new Set()) const [pasteCount, setPasteCount] = createSignal(0) const [imageCount, setImageCount] = createSignal(0) + const [mode, setMode] = createSignal<"normal" | "shell">("normal") let textareaRef: HTMLTextAreaElement | undefined let containerRef: HTMLDivElement | undefined @@ -51,6 +53,7 @@ export default function PromptInput(props: PromptInputProps) { clearSessionDraftPrompt(props.instanceId, props.sessionId) setPromptInternal("") setHistoryDraft(null) + setMode("normal") } function syncAttachmentCounters(currentPrompt: string, sessionAttachments: Attachment[]) { @@ -295,9 +298,40 @@ export default function PromptInput(props: PromptInputProps) { return } + const currentText = prompt() + const cursorAtBufferStart = textarea.selectionStart === 0 && textarea.selectionEnd === 0 + const isShellMode = mode() === "shell" + + if (!isShellMode && e.key === "!" && cursorAtBufferStart && currentText.length === 0 && !props.disabled) { + e.preventDefault() + setMode("shell") + return + } + + if (showPicker() && e.key === "Escape") { + e.preventDefault() + e.stopPropagation() + handlePickerClose() + return + } + + if (isShellMode) { + if (e.key === "Escape") { + e.preventDefault() + e.stopPropagation() + setMode("normal") + return + } + if (e.key === "Backspace" && cursorAtBufferStart && currentText.length === 0) { + e.preventDefault() + setMode("normal") + return + } + } + if (e.key === "Backspace" || e.key === "Delete") { const cursorPos = textarea.selectionStart - const text = prompt() + const text = currentText const pastePlaceholderRegex = /\[pasted #(\d+)\]/g let pasteMatch @@ -464,9 +498,10 @@ export default function PromptInput(props: PromptInputProps) { async function handleSend() { const text = prompt().trim() const currentAttachments = attachments() - if (!text || props.disabled) return + if (props.disabled || !text) return const resolvedPrompt = resolvePastedPlaceholders(text, currentAttachments) + const isShellMode = mode() === "shell" clearPrompt() clearAttachments(props.instanceId, props.sessionId) @@ -480,7 +515,15 @@ export default function PromptInput(props: PromptInputProps) { const updated = await getHistory(props.instanceFolder) setHistory(updated) setHistoryIndex(-1) - await props.onSend(text, currentAttachments) + if (isShellMode) { + if (props.onRunShell) { + await props.onRunShell(resolvedPrompt) + } else { + await props.onSend(resolvedPrompt, []) + } + } else { + await props.onSend(text, currentAttachments) + } } catch (error) { console.error("Failed to send message:", error) alert("Failed to send message: " + (error instanceof Error ? error.message : String(error))) @@ -667,7 +710,14 @@ export default function PromptInput(props: PromptInputProps) { textareaRef?.focus() } - const canSend = () => (prompt().trim().length > 0 || attachments().length > 0) && !props.disabled + const canSend = () => { + if (props.disabled) return false + const hasText = prompt().trim().length > 0 + if (mode() === "shell") return hasText + return hasText || attachments().length > 0 + } + + const shellHint = () => (mode() === "shell" ? { key: "Esc", text: "to exit shell mode" } : { key: "!", text: "for shell mode" }) const instance = () => getActiveInstance() @@ -676,7 +726,11 @@ export default function PromptInput(props: PromptInputProps) {