import { useFilteredList } from "@opencode-ai/ui/hooks" import { createEffect, on, Component, Show, For, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js" import { createStore } from "solid-js/store" import { createFocusSignal } from "@solid-primitives/active-element" import { useLocal } from "@/context/local" import { useFile } from "@/context/file" import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, usePrompt, ImageAttachmentPart, AgentPart, FileAttachmentPart, } from "@/context/prompt" import { useLayout } from "@/context/layout" import { useSDK } from "@/context/sdk" import { useParams } from "@solidjs/router" import { useSync } from "@/context/sync" import { useComments } from "@/context/comments" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import type { IconName } from "@opencode-ai/ui/icons/provider" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { IconButton } from "@opencode-ai/ui/icon-button" import { Select } from "@opencode-ai/ui/select" import { useDialog } from "@opencode-ai/ui/context/dialog" import { ModelSelectorPopover } from "@/components/dialog-select-model" import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid" import { useProviders } from "@/hooks/use-providers" import { useCommand } from "@/context/command" import { Persist, persisted } from "@/utils/persist" import { SessionContextUsage } from "@/components/session-context-usage" import { usePermission } from "@/context/permission" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom" import { createPromptAttachments, ACCEPTED_FILE_TYPES } from "./prompt-input/attachments" import { navigatePromptHistory, prependHistoryEntry, promptLength } from "./prompt-input/history" import { createPromptSubmit } from "./prompt-input/submit" import { PromptPopover, type AtOption, type SlashCommand } from "./prompt-input/slash-popover" import { PromptContextItems } from "./prompt-input/context-items" import { PromptImageAttachments } from "./prompt-input/image-attachments" import { PromptDragOverlay } from "./prompt-input/drag-overlay" import { promptPlaceholder } from "./prompt-input/placeholder" import { ImagePreview } from "@opencode-ai/ui/image-preview" interface PromptInputProps { class?: string ref?: (el: HTMLDivElement) => void newSessionWorktree?: string onNewSessionWorktreeReset?: () => void onSubmit?: () => void } const EXAMPLES = [ "prompt.example.1", "prompt.example.2", "prompt.example.3", "prompt.example.4", "prompt.example.5", "prompt.example.6", "prompt.example.7", "prompt.example.8", "prompt.example.9", "prompt.example.10", "prompt.example.11", "prompt.example.12", "prompt.example.13", "prompt.example.14", "prompt.example.15", "prompt.example.16", "prompt.example.17", "prompt.example.18", "prompt.example.19", "prompt.example.20", "prompt.example.21", "prompt.example.22", "prompt.example.23", "prompt.example.24", "prompt.example.25", ] as const export const PromptInput: Component = (props) => { const sdk = useSDK() const sync = useSync() const local = useLocal() const files = useFile() const prompt = usePrompt() const commentCount = createMemo(() => prompt.context.items().filter((item) => !!item.comment?.trim()).length) const layout = useLayout() const comments = useComments() const params = useParams() const dialog = useDialog() const providers = useProviders() const command = useCommand() const permission = usePermission() const language = useLanguage() const platform = usePlatform() let editorRef!: HTMLDivElement let fileInputRef!: HTMLInputElement let scrollRef!: HTMLDivElement let slashPopoverRef!: HTMLDivElement const mirror = { input: false } const scrollCursorIntoView = () => { const container = scrollRef const selection = window.getSelection() if (!container || !selection || selection.rangeCount === 0) return const range = selection.getRangeAt(0) if (!editorRef.contains(range.startContainer)) return const rect = range.getBoundingClientRect() if (!rect.height) return const containerRect = container.getBoundingClientRect() const top = rect.top - containerRect.top + container.scrollTop const bottom = rect.bottom - containerRect.top + container.scrollTop const padding = 12 if (top < container.scrollTop + padding) { container.scrollTop = Math.max(0, top - padding) return } if (bottom > container.scrollTop + container.clientHeight - padding) { container.scrollTop = bottom - container.clientHeight + padding } } const queueScroll = () => { requestAnimationFrame(scrollCursorIntoView) } const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey)) const view = createMemo(() => layout.view(sessionKey)) const commentInReview = (path: string) => { const sessionID = params.id if (!sessionID) return false const diffs = sync.data.session_diff[sessionID] if (!diffs) return false return diffs.some((diff) => diff.file === path) } const openComment = (item: { path: string; commentID?: string; commentOrigin?: "review" | "file" }) => { if (!item.commentID) return const focus = { file: item.path, id: item.commentID } comments.setActive(focus) const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path)) if (wantsReview) { if (!view().reviewPanel.opened()) view().reviewPanel.open() layout.fileTree.setTab("changes") tabs().setActive("review") requestAnimationFrame(() => comments.setFocus(focus)) return } if (!view().reviewPanel.opened()) view().reviewPanel.open() layout.fileTree.setTab("all") const tab = files.tab(item.path) tabs().open(tab) files.load(item.path) requestAnimationFrame(() => comments.setFocus(focus)) } const recent = createMemo(() => { const all = tabs().all() const active = tabs().active() const order = active ? [active, ...all.filter((x) => x !== active)] : all const seen = new Set() const paths: string[] = [] for (const tab of order) { const path = files.pathFromTab(tab) if (!path) continue if (seen.has(path)) continue seen.add(path) paths.push(path) } return paths }) const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const status = createMemo( () => sync.data.session_status[params.id ?? ""] ?? { type: "idle", }, ) const working = createMemo(() => status()?.type !== "idle") const imageAttachments = createMemo(() => prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"), ) const [store, setStore] = createStore<{ popover: "at" | "slash" | null historyIndex: number savedPrompt: Prompt | null placeholder: number draggingType: "image" | "@mention" | null mode: "normal" | "shell" applyingHistory: boolean }>({ popover: null, historyIndex: -1, savedPrompt: null, placeholder: Math.floor(Math.random() * EXAMPLES.length), draggingType: null, mode: "normal", applyingHistory: false, }) const placeholder = createMemo(() => promptPlaceholder({ mode: store.mode, commentCount: commentCount(), example: language.t(EXAMPLES[store.placeholder]), t: (key, params) => language.t(key as Parameters[0], params as never), }), ) const MAX_HISTORY = 100 const [history, setHistory] = persisted( Persist.global("prompt-history", ["prompt-history.v1"]), createStore<{ entries: Prompt[] }>({ entries: [], }), ) const [shellHistory, setShellHistory] = persisted( Persist.global("prompt-history-shell", ["prompt-history-shell.v1"]), createStore<{ entries: Prompt[] }>({ entries: [], }), ) const applyHistoryPrompt = (p: Prompt, position: "start" | "end") => { const length = position === "start" ? 0 : promptLength(p) setStore("applyingHistory", true) prompt.set(p, length) requestAnimationFrame(() => { editorRef.focus() setCursorPosition(editorRef, length) setStore("applyingHistory", false) queueScroll() }) } const getCaretState = () => { const selection = window.getSelection() const textLength = promptLength(prompt.current()) if (!selection || selection.rangeCount === 0) { return { collapsed: false, cursorPosition: 0, textLength } } const anchorNode = selection.anchorNode if (!anchorNode || !editorRef.contains(anchorNode)) { return { collapsed: false, cursorPosition: 0, textLength } } return { collapsed: selection.isCollapsed, cursorPosition: getCursorPosition(editorRef), textLength, } } const isFocused = createFocusSignal(() => editorRef) const closePopover = () => setStore("popover", null) const resetHistoryNavigation = (force = false) => { if (!force && (store.historyIndex < 0 || store.applyingHistory)) return setStore("historyIndex", -1) setStore("savedPrompt", null) } const clearEditor = () => { editorRef.innerHTML = "" } const setEditorText = (text: string) => { clearEditor() editorRef.textContent = text } const focusEditorEnd = () => { requestAnimationFrame(() => { editorRef.focus() const range = document.createRange() const selection = window.getSelection() range.selectNodeContents(editorRef) range.collapse(false) selection?.removeAllRanges() selection?.addRange(range) }) } const currentCursor = () => { const selection = window.getSelection() if (!selection || selection.rangeCount === 0 || !editorRef.contains(selection.anchorNode)) return null return getCursorPosition(editorRef) } const renderEditorWithCursor = (parts: Prompt) => { const cursor = currentCursor() renderEditor(parts) if (cursor !== null) setCursorPosition(editorRef, cursor) } createEffect(() => { params.id if (params.id) return const interval = setInterval(() => { setStore("placeholder", (prev) => (prev + 1) % EXAMPLES.length) }, 6500) onCleanup(() => clearInterval(interval)) }) const [composing, setComposing] = createSignal(false) const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229 createEffect(() => { if (!isFocused()) closePopover() }) // Safety: reset composing state on focus change to prevent stuck state // This handles edge cases where compositionend event may not fire createEffect(() => { if (!isFocused()) setComposing(false) }) const agentList = createMemo(() => sync.data.agent .filter((agent) => !agent.hidden && agent.mode !== "primary") .map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })), ) const agentNames = createMemo(() => local.agent.list().map((agent) => agent.name)) const handleAtSelect = (option: AtOption | undefined) => { if (!option) return if (option.type === "agent") { addPart({ type: "agent", name: option.name, content: "@" + option.name, start: 0, end: 0 }) } else { addPart({ type: "file", path: option.path, content: "@" + option.path, start: 0, end: 0 }) } } const atKey = (x: AtOption | undefined) => { if (!x) return "" return x.type === "agent" ? `agent:${x.name}` : `file:${x.path}` } const { flat: atFlat, active: atActive, setActive: setAtActive, onInput: atOnInput, onKeyDown: atOnKeyDown, } = useFilteredList({ items: async (query) => { const agents = agentList() const open = recent() const seen = new Set(open) const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true })) const paths = await files.searchFilesAndDirectories(query) const fileOptions: AtOption[] = paths .filter((path) => !seen.has(path)) .map((path) => ({ type: "file", path, display: path })) return [...agents, ...pinned, ...fileOptions] }, key: atKey, filterKeys: ["display"], groupBy: (item) => { if (item.type === "agent") return "agent" if (item.recent) return "recent" return "file" }, sortGroupsBy: (a, b) => { const rank = (category: string) => { if (category === "agent") return 0 if (category === "recent") return 1 return 2 } return rank(a.category) - rank(b.category) }, onSelect: handleAtSelect, }) const slashCommands = createMemo(() => { const builtin = command.options .filter((opt) => !opt.disabled && !opt.id.startsWith("suggested.") && opt.slash) .map((opt) => ({ id: opt.id, trigger: opt.slash!, title: opt.title, description: opt.description, keybind: opt.keybind, type: "builtin" as const, })) const custom = sync.data.command.map((cmd) => ({ id: `custom.${cmd.name}`, trigger: cmd.name, title: cmd.name, description: cmd.description, type: "custom" as const, source: cmd.source, })) return [...custom, ...builtin] }) const handleSlashSelect = (cmd: SlashCommand | undefined) => { if (!cmd) return closePopover() if (cmd.type === "custom") { const text = `/${cmd.trigger} ` setEditorText(text) prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) focusEditorEnd() return } clearEditor() prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0) command.trigger(cmd.id, "slash") } const { flat: slashFlat, active: slashActive, setActive: setSlashActive, onInput: slashOnInput, onKeyDown: slashOnKeyDown, refetch: slashRefetch, } = useFilteredList({ items: slashCommands, key: (x) => x?.id, filterKeys: ["trigger", "title"], onSelect: handleSlashSelect, }) const createPill = (part: FileAttachmentPart | AgentPart) => { const pill = document.createElement("span") pill.textContent = part.content pill.setAttribute("data-type", part.type) if (part.type === "file") pill.setAttribute("data-path", part.path) if (part.type === "agent") pill.setAttribute("data-name", part.name) pill.setAttribute("contenteditable", "false") pill.style.userSelect = "text" pill.style.cursor = "default" return pill } const isNormalizedEditor = () => Array.from(editorRef.childNodes).every((node) => { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent ?? "" if (!text.includes("\u200B")) return true if (text !== "\u200B") return false const prev = node.previousSibling const next = node.nextSibling const prevIsBr = prev?.nodeType === Node.ELEMENT_NODE && (prev as HTMLElement).tagName === "BR" const nextIsBr = next?.nodeType === Node.ELEMENT_NODE && (next as HTMLElement).tagName === "BR" if (!prevIsBr && !nextIsBr) return false if (nextIsBr && !prevIsBr && prev) return false return true } if (node.nodeType !== Node.ELEMENT_NODE) return false const el = node as HTMLElement if (el.dataset.type === "file") return true if (el.dataset.type === "agent") return true return el.tagName === "BR" }) const renderEditor = (parts: Prompt) => { clearEditor() for (const part of parts) { if (part.type === "text") { editorRef.appendChild(createTextFragment(part.content)) continue } if (part.type === "file" || part.type === "agent") { editorRef.appendChild(createPill(part)) } } } createEffect( on( () => sync.data.command, () => slashRefetch(), { defer: true }, ), ) // Auto-scroll active command into view when navigating with keyboard createEffect(() => { const activeId = slashActive() if (!activeId || !slashPopoverRef) return requestAnimationFrame(() => { const element = slashPopoverRef.querySelector(`[data-slash-id="${activeId}"]`) element?.scrollIntoView({ block: "nearest", behavior: "smooth" }) }) }) const selectPopoverActive = () => { if (store.popover === "at") { const items = atFlat() if (items.length === 0) return const active = atActive() const item = items.find((entry) => atKey(entry) === active) ?? items[0] handleAtSelect(item) return } if (store.popover === "slash") { const items = slashFlat() if (items.length === 0) return const active = slashActive() const item = items.find((entry) => entry.id === active) ?? items[0] handleSlashSelect(item) } } createEffect( on( () => prompt.current(), (currentParts) => { const inputParts = currentParts.filter((part) => part.type !== "image") if (mirror.input) { mirror.input = false if (isNormalizedEditor()) return renderEditorWithCursor(inputParts) return } const domParts = parseFromDOM() if (isNormalizedEditor() && isPromptEqual(inputParts, domParts)) return renderEditorWithCursor(inputParts) }, ), ) const parseFromDOM = (): Prompt => { const parts: Prompt = [] let position = 0 let buffer = "" const flushText = () => { const content = buffer.replace(/\r\n?/g, "\n").replace(/\u200B/g, "") buffer = "" if (!content) return parts.push({ type: "text", content, start: position, end: position + content.length }) position += content.length } const pushFile = (file: HTMLElement) => { const content = file.textContent ?? "" parts.push({ type: "file", path: file.dataset.path!, content, start: position, end: position + content.length, }) position += content.length } const pushAgent = (agent: HTMLElement) => { const content = agent.textContent ?? "" parts.push({ type: "agent", name: agent.dataset.name!, content, start: position, end: position + content.length, }) position += content.length } const visit = (node: Node) => { if (node.nodeType === Node.TEXT_NODE) { buffer += node.textContent ?? "" return } if (node.nodeType !== Node.ELEMENT_NODE) return const el = node as HTMLElement if (el.dataset.type === "file") { flushText() pushFile(el) return } if (el.dataset.type === "agent") { flushText() pushAgent(el) return } if (el.tagName === "BR") { buffer += "\n" return } for (const child of Array.from(el.childNodes)) { visit(child) } } const children = Array.from(editorRef.childNodes) children.forEach((child, index) => { const isBlock = child.nodeType === Node.ELEMENT_NODE && ["DIV", "P"].includes((child as HTMLElement).tagName) visit(child) if (isBlock && index < children.length - 1) { buffer += "\n" } }) flushText() if (parts.length === 0) parts.push(...DEFAULT_PROMPT) return parts } const handleInput = () => { const rawParts = parseFromDOM() const images = imageAttachments() const cursorPosition = getCursorPosition(editorRef) const rawText = rawParts.map((p) => ("content" in p ? p.content : "")).join("") const trimmed = rawText.replace(/\u200B/g, "").trim() const hasNonText = rawParts.some((part) => part.type !== "text") const shouldReset = trimmed.length === 0 && !hasNonText && images.length === 0 if (shouldReset) { closePopover() resetHistoryNavigation() if (prompt.dirty()) { mirror.input = true prompt.set(DEFAULT_PROMPT, 0) } queueScroll() return } const shellMode = store.mode === "shell" if (!shellMode) { const atMatch = rawText.substring(0, cursorPosition).match(/@(\S*)$/) const slashMatch = rawText.match(/^\/(\S*)$/) if (atMatch) { atOnInput(atMatch[1]) setStore("popover", "at") } else if (slashMatch) { slashOnInput(slashMatch[1]) setStore("popover", "slash") } else { closePopover() } } else { closePopover() } resetHistoryNavigation() mirror.input = true prompt.set([...rawParts, ...images], cursorPosition) queueScroll() } const addPart = (part: ContentPart) => { const selection = window.getSelection() if (!selection || selection.rangeCount === 0) return const cursorPosition = getCursorPosition(editorRef) const currentPrompt = prompt.current() const rawText = currentPrompt.map((p) => ("content" in p ? p.content : "")).join("") const textBeforeCursor = rawText.substring(0, cursorPosition) const atMatch = textBeforeCursor.match(/@(\S*)$/) if (part.type === "file" || part.type === "agent") { const pill = createPill(part) const gap = document.createTextNode(" ") const range = selection.getRangeAt(0) if (atMatch) { const start = atMatch.index ?? cursorPosition - atMatch[0].length setRangeEdge(editorRef, range, "start", start) setRangeEdge(editorRef, range, "end", cursorPosition) } range.deleteContents() range.insertNode(gap) range.insertNode(pill) range.setStartAfter(gap) range.collapse(true) selection.removeAllRanges() selection.addRange(range) } else if (part.type === "text") { const range = selection.getRangeAt(0) const fragment = createTextFragment(part.content) const last = fragment.lastChild range.deleteContents() range.insertNode(fragment) if (last) { if (last.nodeType === Node.TEXT_NODE) { const text = last.textContent ?? "" if (text === "\u200B") { range.setStart(last, 0) } if (text !== "\u200B") { range.setStart(last, text.length) } } if (last.nodeType !== Node.TEXT_NODE) { range.setStartAfter(last) } } range.collapse(true) selection.removeAllRanges() selection.addRange(range) } handleInput() closePopover() } const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => { const currentHistory = mode === "shell" ? shellHistory : history const setCurrentHistory = mode === "shell" ? setShellHistory : setHistory const next = prependHistoryEntry(currentHistory.entries, prompt) if (next === currentHistory.entries) return setCurrentHistory("entries", next) } const navigateHistory = (direction: "up" | "down") => { const result = navigatePromptHistory({ direction, entries: store.mode === "shell" ? shellHistory.entries : history.entries, historyIndex: store.historyIndex, currentPrompt: prompt.current(), savedPrompt: store.savedPrompt, }) if (!result.handled) return false setStore("historyIndex", result.historyIndex) setStore("savedPrompt", result.savedPrompt) applyHistoryPrompt(result.prompt, result.cursor) return true } const { addImageAttachment, removeImageAttachment, handlePaste } = createPromptAttachments({ editor: () => editorRef, isFocused, isDialogActive: () => !!dialog.active, setDraggingType: (type) => setStore("draggingType", type), focusEditor: () => { editorRef.focus() setCursorPosition(editorRef, promptLength(prompt.current())) }, addPart, readClipboardImage: platform.readClipboardImage, }) const { abort, handleSubmit } = createPromptSubmit({ info, imageAttachments, commentCount, mode: () => store.mode, working, editor: () => editorRef, queueScroll, promptLength, addToHistory, resetHistoryNavigation: () => { resetHistoryNavigation(true) }, setMode: (mode) => setStore("mode", mode), setPopover: (popover) => setStore("popover", popover), newSessionWorktree: () => props.newSessionWorktree, onNewSessionWorktreeReset: props.onNewSessionWorktreeReset, onSubmit: props.onSubmit, }) const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Backspace") { const selection = window.getSelection() if (selection && selection.isCollapsed) { const node = selection.anchorNode const offset = selection.anchorOffset if (node && node.nodeType === Node.TEXT_NODE) { const text = node.textContent ?? "" if (/^\u200B+$/.test(text) && offset > 0) { const range = document.createRange() range.setStart(node, 0) range.collapse(true) selection.removeAllRanges() selection.addRange(range) } } } } if (event.key === "!" && store.mode === "normal") { const cursorPosition = getCursorPosition(editorRef) if (cursorPosition === 0) { setStore("mode", "shell") setStore("popover", null) event.preventDefault() return } } if (store.mode === "shell") { const { collapsed, cursorPosition, textLength } = getCaretState() if (event.key === "Escape") { setStore("mode", "normal") event.preventDefault() return } if (event.key === "Backspace" && collapsed && cursorPosition === 0 && textLength === 0) { setStore("mode", "normal") event.preventDefault() return } } // Handle Shift+Enter BEFORE IME check - Shift+Enter is never used for IME input // and should always insert a newline regardless of composition state if (event.key === "Enter" && event.shiftKey) { addPart({ type: "text", content: "\n", start: 0, end: 0 }) event.preventDefault() return } if (event.key === "Enter" && isImeComposing(event)) { return } const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey if (store.popover) { if (event.key === "Tab") { selectPopoverActive() event.preventDefault() return } const nav = event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter" const ctrlNav = ctrl && (event.key === "n" || event.key === "p") if (nav || ctrlNav) { if (store.popover === "at") { atOnKeyDown(event) event.preventDefault() return } if (store.popover === "slash") { slashOnKeyDown(event) } event.preventDefault() return } } if (ctrl && event.code === "KeyG") { if (store.popover) { closePopover() event.preventDefault() return } if (working()) { abort() event.preventDefault() } return } if (event.key === "ArrowUp" || event.key === "ArrowDown") { if (event.altKey || event.ctrlKey || event.metaKey) return const { collapsed } = getCaretState() if (!collapsed) return const cursorPosition = getCursorPosition(editorRef) const textLength = promptLength(prompt.current()) const textContent = prompt .current() .map((part) => ("content" in part ? part.content : "")) .join("") const isEmpty = textContent.trim() === "" || textLength <= 1 const hasNewlines = textContent.includes("\n") const inHistory = store.historyIndex >= 0 const atStart = cursorPosition <= (isEmpty ? 1 : 0) const atEnd = cursorPosition >= (isEmpty ? textLength - 1 : textLength) const allowUp = isEmpty || atStart || (!hasNewlines && !inHistory) || (inHistory && atEnd) const allowDown = isEmpty || atEnd || (!hasNewlines && !inHistory) || (inHistory && atStart) if (event.key === "ArrowUp") { if (!allowUp) return if (navigateHistory("up")) { event.preventDefault() } return } if (!allowDown) return if (navigateHistory("down")) { event.preventDefault() } return } // Note: Shift+Enter is handled earlier, before IME check if (event.key === "Enter" && !event.shiftKey) { handleSubmit(event) } if (event.key === "Escape") { if (store.popover) { closePopover() } else if (working()) { abort() } } } return (
(slashPopoverRef = el)} atFlat={atFlat()} atActive={atActive() ?? undefined} atKey={atKey} setAtActive={setAtActive} onAtSelect={handleAtSelect} slashFlat={slashFlat()} slashActive={slashActive() ?? undefined} setSlashActive={setSlashActive} onSlashSelect={handleSlashSelect} commandKeybind={command.keybind} t={(key) => language.t(key as Parameters[0])} />
{ const active = comments.active() return !!item.commentID && item.commentID === active?.id && item.path === active?.file }} openComment={openComment} remove={(item) => { if (item.commentID) comments.remove(item.path, item.commentID) prompt.context.remove(item.key) }} t={(key) => language.t(key as Parameters[0])} /> dialog.show(() => ) } onRemove={removeImageAttachment} removeLabel={language.t("prompt.attachment.remove")} />
(scrollRef = el)}>
{ editorRef = el props.ref?.(el) }} role="textbox" aria-multiline="true" aria-label={placeholder()} contenteditable="true" autocapitalize="off" autocorrect="off" spellcheck={false} onInput={handleInput} onPaste={handlePaste} onCompositionStart={() => setComposing(true)} onCompositionEnd={() => setComposing(false)} onKeyDown={handleKeyDown} classList={{ "select-text": true, "w-full p-3 pr-12 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, "[&_[data-type=file]]:text-syntax-property": true, "[&_[data-type=agent]]:text-syntax-type": true, "font-mono!": store.mode === "shell", }} />
{placeholder()}
{language.t("prompt.mode.shell")} {language.t("prompt.mode.shell.exit")}
{ const file = e.currentTarget.files?.[0] if (file) addImageAttachment(file) e.currentTarget.value = "" }} />
{language.t("prompt.action.stop")} {language.t("common.key.esc")}
{language.t("prompt.action.send")}
} >
) }