diff --git a/packages/opencode-config/package.json b/packages/opencode-config/package.json index a3a9bcac..4feb72e9 100644 --- a/packages/opencode-config/package.json +++ b/packages/opencode-config/package.json @@ -3,6 +3,6 @@ "version": "0.5.0", "private": true, "dependencies": { - "@opencode-ai/plugin": "1.1.10" + "@opencode-ai/plugin": "1.1.12" } } diff --git a/packages/ui/src/components/expand-button.tsx b/packages/ui/src/components/expand-button.tsx new file mode 100644 index 00000000..0b6e4fb0 --- /dev/null +++ b/packages/ui/src/components/expand-button.tsx @@ -0,0 +1,30 @@ +import { Show } from "solid-js" +import { Maximize2, Minimize2 } from "lucide-solid" + +interface ExpandButtonProps { + expandState: () => "normal" | "expanded" + onToggleExpand: (nextState: "normal" | "expanded") => void +} + +export default function ExpandButton(props: ExpandButtonProps) { + function handleClick() { + const current = props.expandState() + props.onToggleExpand(current === "normal" ? "expanded" : "normal") + } + + return ( + + ) +} diff --git a/packages/ui/src/components/prompt-input.tsx b/packages/ui/src/components/prompt-input.tsx index 6bd83631..00ea57a5 100644 --- a/packages/ui/src/components/prompt-input.tsx +++ b/packages/ui/src/components/prompt-input.tsx @@ -1,6 +1,7 @@ -import { createSignal, Show, onMount, For, onCleanup, createEffect, on, untrack } from "solid-js" +import { createSignal, Show, onMount, For, onCleanup, createEffect, on, untrack, createMemo } from "solid-js" import { ArrowBigUp, ArrowBigDown } from "lucide-solid" import UnifiedPicker from "./unified-picker" +import ExpandButton from "./expand-button" import { addToHistory, getHistory } from "../stores/message-history" import { getAttachments, addAttachment, clearAttachments, removeAttachment } from "../stores/attachments" import { resolvePastedPlaceholders } from "../lib/prompt-placeholders" @@ -46,10 +47,56 @@ export default function PromptInput(props: PromptInputProps) { const [pasteCount, setPasteCount] = createSignal(0) const [imageCount, setImageCount] = createSignal(0) const [mode, setMode] = createSignal<"normal" | "shell">("normal") + const [expandState, setExpandState] = createSignal<"normal" | "expanded">("normal") const SELECTION_INSERT_MAX_LENGTH = 2000 let textareaRef: HTMLTextAreaElement | undefined let containerRef: HTMLDivElement | undefined + // Fixed line height for expanded state (15 lines as suggested by dev) + + // Fixed line height for web/mobile expanded state (15 lines as suggested) + const EXPANDED_LINES = 15 + const LINE_HEIGHT = 24 + const FIXED_EXPANDED_HEIGHT = EXPANDED_LINES * LINE_HEIGHT // 360px + + const calculateExpandedHeight = () => { + if (!containerRef) { + return 0 + } + + const root = containerRef.closest(".session-view") + if (!root) { + return 0 + } + const rootRect = root.getBoundingClientRect() + + // Reserve minimum space for message section + // Use larger reserve for landscape orientation (less vertical space) + const isLandscape = typeof window !== "undefined" && window.innerWidth > window.innerHeight + const MIN_MESSAGE_SPACE = isLandscape ? 150 : 200 + const availableForInput = rootRect.height - MIN_MESSAGE_SPACE + + return availableForInput + } + + const expandedHeight = createMemo(() => { + const state = expandState() + if (state === "normal") return "auto" + + // Use fixed height, but cap at available space + // This prevents overflow in landscape or small screens + const availableHeight = calculateExpandedHeight() + const maxHeight = Math.min(FIXED_EXPANDED_HEIGHT, availableHeight * 0.6) + return `${Math.max(maxHeight, 150)}px` // Minimum 150px to be useful + }) + + const getPlaceholder = () => { + if (mode() === "shell") { + return "Run a shell command (Esc to exit)..." + } + return "Type your message, @file, @agent, or paste images and text..." + } + @@ -615,7 +662,7 @@ export default function PromptInput(props: PromptInputProps) { // Record attempted slash commands even if execution fails. void refreshHistory() } - + try { if (isShellMode) { if (props.onRunShell) { @@ -642,7 +689,7 @@ export default function PromptInput(props: PromptInputProps) { textareaRef?.focus() } } - + function focusTextareaEnd() { if (!textareaRef) return setTimeout(() => { @@ -652,7 +699,7 @@ export default function PromptInput(props: PromptInputProps) { textareaRef.focus() }, 0) } - + function canUseHistory(force = false) { if (force) return true if (showPicker()) return false @@ -660,29 +707,29 @@ export default function PromptInput(props: PromptInputProps) { if (!textarea) return false return textarea.selectionStart === 0 && textarea.selectionEnd === 0 } - + function selectPreviousHistory(force = false) { const entries = history() if (entries.length === 0) return false if (!canUseHistory(force)) return false - + if (historyIndex() === -1) { setHistoryDraft(prompt()) } - + const newIndex = historyIndex() === -1 ? 0 : Math.min(historyIndex() + 1, entries.length - 1) setHistoryIndex(newIndex) setPrompt(entries[newIndex]) focusTextareaEnd() return true } - + function selectNextHistory(force = false) { const entries = history() if (entries.length === 0) return false if (!canUseHistory(force)) return false if (historyIndex() === -1) return false - + const newIndex = historyIndex() - 1 if (newIndex >= 0) { setHistoryIndex(newIndex) @@ -696,12 +743,18 @@ export default function PromptInput(props: PromptInputProps) { focusTextareaEnd() return true } - + function handleAbort() { if (!props.onAbortSession || !props.isSessionBusy) return void props.onAbortSession() } - + + function handleExpandToggle(nextState: "normal" | "expanded") { + setExpandState(nextState) + // Keep focus on textarea + textareaRef?.focus() + } + function handleInput(e: Event) { const target = e.target as HTMLTextAreaElement @@ -765,9 +818,9 @@ export default function PromptInput(props: PromptInputProps) { item: | { type: "agent"; agent: Agent } | { - type: "file" - file: { path: string; relativePath?: string; isGitFile: boolean; isDirectory?: boolean } - } + type: "file" + file: { path: string; relativePath?: string; isGitFile: boolean; isDirectory?: boolean } + } | { type: "command"; command: SDKCommand }, ) { if (item.type === "command") { @@ -1018,18 +1071,18 @@ export default function PromptInput(props: PromptInputProps) { } const canStop = () => Boolean(props.isSessionBusy && props.onAbortSession) - + const hasHistory = () => history().length > 0 const canHistoryGoPrevious = () => hasHistory() && (historyIndex() === -1 || historyIndex() < history().length - 1) const canHistoryGoNext = () => historyIndex() >= 0 - + 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: "Shell mode" }) const commandHint = () => ({ key: "/", text: "Commands" }) @@ -1161,94 +1214,101 @@ export default function PromptInput(props: PromptInputProps) { -