@@ -31,6 +31,15 @@ import {
3131} from "../stores/conversation-speech"
3232const log = getLogger ( "actions" )
3333const LazyUnifiedPicker = lazy ( ( ) => import ( "./unified-picker" ) )
34+ const DEFAULT_PROMPT_FIELD_HEIGHT = 104
35+ const MAX_PROMPT_FIELD_HEIGHT_RATIO = 0.6
36+
37+ type ResizeDragState = {
38+ pointerId : number
39+ startY : number
40+ startHeight : number
41+ maxHeight : number
42+ }
3443
3544function getConsumedPastedTextAttachmentIds ( text : string , attachments : Attachment [ ] ) : string [ ] {
3645 if ( ! text || attachments . length === 0 ) return [ ]
@@ -65,11 +74,16 @@ export default function PromptInput(props: PromptInputProps) {
6574 const [ , setIsFocused ] = createSignal ( false )
6675 const [ mode , setMode ] = createSignal < PromptMode > ( "normal" )
6776 const [ expandState , setExpandState ] = createSignal < ExpandState > ( "normal" )
77+ const [ inputHeight , setInputHeight ] = createSignal < number | null > ( null )
78+ const [ isResizing , setIsResizing ] = createSignal ( false )
6879 const [ isFileBrowserOpen , setIsFileBrowserOpen ] = createSignal ( false )
6980 const SELECTION_INSERT_MAX_LENGTH = 2000
7081 const MAX_READABLE_PICKED_FILE_BYTES = 5 * 1024 * 1024
7182 let textareaRef : HTMLTextAreaElement | undefined
7283 let fileInputRef : HTMLInputElement | undefined
84+ let wrapperRef : HTMLDivElement | undefined
85+ let fieldContainerRef : HTMLDivElement | undefined
86+ let resizeDragState : ResizeDragState | undefined
7387
7488 const getPlaceholder = ( ) => {
7589 if ( mode ( ) === "shell" ) {
@@ -216,6 +230,7 @@ export default function PromptInput(props: PromptInputProps) {
216230 draftLoadedNonce ,
217231 ( ) => {
218232 // Session switch resets (picker/counters/ignored positions) stay in the component.
233+ setInputHeight ( null )
219234 setIgnoredAtPositions ( new Set < number > ( ) )
220235 setShowPicker ( false )
221236 setPickerMode ( "mention" )
@@ -294,6 +309,61 @@ export default function PromptInput(props: PromptInputProps) {
294309 } )
295310 } )
296311
312+ function computeMaxFieldHeight ( ) : number {
313+ if ( typeof window === "undefined" ) return DEFAULT_PROMPT_FIELD_HEIGHT
314+
315+ const sessionCenter = wrapperRef ?. closest ( "[data-session-center-width]" )
316+ const availableHeight = sessionCenter ?. getBoundingClientRect ( ) . height ?? window . innerHeight
317+ const maxHeight = Math . floor ( availableHeight * MAX_PROMPT_FIELD_HEIGHT_RATIO )
318+ return Math . max ( DEFAULT_PROMPT_FIELD_HEIGHT , maxHeight )
319+ }
320+
321+ function handleResizeStart ( event : PointerEvent ) {
322+ event . preventDefault ( )
323+ const target = event . currentTarget as HTMLElement
324+
325+ resizeDragState = {
326+ pointerId : event . pointerId ,
327+ startY : event . clientY ,
328+ startHeight : fieldContainerRef ?. getBoundingClientRect ( ) . height ?? DEFAULT_PROMPT_FIELD_HEIGHT ,
329+ maxHeight : computeMaxFieldHeight ( ) ,
330+ }
331+
332+ setIsResizing ( true )
333+
334+ try {
335+ target . setPointerCapture ( event . pointerId )
336+ } catch {
337+ resizeDragState = undefined
338+ setIsResizing ( false )
339+ }
340+ }
341+
342+ function handleResizeMove ( event : PointerEvent ) {
343+ if ( ! resizeDragState || resizeDragState . pointerId !== event . pointerId ) return
344+
345+ event . preventDefault ( )
346+ const deltaY = resizeDragState . startY - event . clientY
347+ const nextHeight = Math . max (
348+ DEFAULT_PROMPT_FIELD_HEIGHT ,
349+ Math . min ( resizeDragState . maxHeight , resizeDragState . startHeight + deltaY ) ,
350+ )
351+ setInputHeight ( nextHeight )
352+ }
353+
354+ function handleResizeEnd ( event : PointerEvent ) {
355+ if ( ! resizeDragState || resizeDragState . pointerId !== event . pointerId ) return
356+
357+ event . preventDefault ( )
358+ resizeDragState = undefined
359+ setIsResizing ( false )
360+ textareaRef ?. focus ( )
361+ }
362+
363+ onCleanup ( ( ) => {
364+ resizeDragState = undefined
365+ } )
366+
297367 async function handleSend ( ) {
298368 const text = prompt ( ) . trim ( )
299369 const currentAttachments = attachments ( )
@@ -324,6 +394,7 @@ export default function PromptInput(props: PromptInputProps) {
324394 const refreshHistory = ( ) => recordHistoryEntry ( historyEntry )
325395
326396 setExpandState ( "normal" )
397+ setInputHeight ( null )
327398 clearPrompt ( )
328399 clearHistoryDraft ( )
329400 setMode ( "normal" )
@@ -386,6 +457,7 @@ export default function PromptInput(props: PromptInputProps) {
386457 }
387458
388459 function handleExpandToggle ( nextState : "normal" | "expanded" ) {
460+ setInputHeight ( null )
389461 setExpandState ( nextState )
390462 // Keep focus on textarea
391463 textareaRef ?. focus ( )
@@ -603,6 +675,7 @@ export default function PromptInput(props: PromptInputProps) {
603675 return (
604676 < div class = "prompt-input-container" >
605677 < div
678+ ref = { wrapperRef }
606679 class = { `prompt-input-wrapper relative ${ isDragging ( ) ? "border-2" : "" } ` }
607680 style = {
608681 isDragging ( )
@@ -635,9 +708,26 @@ export default function PromptInput(props: PromptInputProps) {
635708 </ Show >
636709
637710 < div class = "prompt-input-main flex flex-1 flex-col" >
638- < div class = { `prompt-input-field-container ${ expandState ( ) === "expanded" ? "is-expanded" : "" } ` } >
711+ < div
712+ ref = { fieldContainerRef }
713+ class = { `prompt-input-field-container ${ expandState ( ) === "expanded" ? "is-expanded" : "" } ${ inputHeight ( ) !== null ? "is-resized" : "" } ` }
714+ style = { inputHeight ( ) !== null ? { height : `${ inputHeight ( ) } px` , "min-height" : `${ inputHeight ( ) } px` } : undefined }
715+ >
716+ < div
717+ class = { `prompt-resize-handle ${ isResizing ( ) ? "is-resizing" : "" } ` }
718+ onPointerDown = { handleResizeStart }
719+ onPointerMove = { handleResizeMove }
720+ onPointerUp = { handleResizeEnd }
721+ onPointerCancel = { handleResizeEnd }
722+ aria-hidden = "true"
723+ role = "presentation"
724+ title = { t ( "promptInput.resizeHandle.title" ) }
725+ />
639726
640- < div class = { `prompt-input-field ${ expandState ( ) === "expanded" ? "is-expanded" : "" } ` } >
727+ < div
728+ class = { `prompt-input-field ${ expandState ( ) === "expanded" ? "is-expanded" : "" } ` }
729+ style = { inputHeight ( ) !== null ? { height : `${ inputHeight ( ) } px` , "min-height" : `${ inputHeight ( ) } px` } : undefined }
730+ >
641731 < textarea
642732 ref = { textareaRef }
643733 class = { `prompt-input ${ mode ( ) === "shell" ? "shell-mode" : "" } ${ expandState ( ) === "expanded" ? "is-expanded" : "" } ` }
@@ -655,6 +745,7 @@ export default function PromptInput(props: PromptInputProps) {
655745 autocorrect = "off"
656746 autoCapitalize = "off"
657747 autocomplete = "off"
748+ style = { inputHeight ( ) !== null ? { height : `${ inputHeight ( ) } px` , "min-height" : `${ inputHeight ( ) } px` , "overflow-y" : "auto" } : undefined }
658749 />
659750 < button
660751 type = "button"
0 commit comments