@@ -6,16 +6,13 @@ import {
66 EMPTY_BUFFER ,
77 PASTE_MARKER_REGEX ,
88 backspace ,
9- cleanPasteContent ,
109 deleteForward ,
1110 deletePasteMarkerBackward ,
1211 deletePasteMarkerForward ,
1312 deleteWordBefore ,
1413 deleteWordAfter ,
1514 expandPasteMarkers ,
16- findPasteMarkerContaining ,
1715 getCurrentSlashToken ,
18- hasActivePasteMarkers ,
1916 insertText ,
2017 isEmpty ,
2118 killLine ,
@@ -53,6 +50,8 @@ export type { InputKey } from "./prompt";
5350
5451import { useTerminalInput } from "./prompt" ;
5552import type { InputKey } from "./prompt" ;
53+ import { usePasteHandling } from "./prompt/paste-handling" ;
54+ import { useHistoryNavigation } from "./prompt/history-navigation" ;
5655import {
5756 useHiddenTerminalCursor ,
5857 useTerminalExtendedKeys ,
@@ -150,21 +149,22 @@ export const PromptInput = React.memo(function PromptInput({
150149 const [ showModelDropdown , setShowModelDropdown ] = useState ( false ) ;
151150 const [ fileMentionItems , setFileMentionItems ] = useState < FileMentionItem [ ] > ( ( ) => scanFileMentionItems ( projectRoot ) ) ;
152151 const [ dismissedFileMentionKey , setDismissedFileMentionKey ] = useState < string | null > ( null ) ;
153- const [ historyCursor , setHistoryCursor ] = useState ( - 1 ) ;
154- const [ draftBeforeHistory , setDraftBeforeHistory ] = useState < string | null > ( null ) ;
155152 const [ hasTerminalFocus , setHasTerminalFocus ] = useState ( true ) ;
156153 const lastCtrlDAt = React . useRef < number > ( 0 ) ;
157154 const undoRedoRef = React . useRef ( createPromptUndoRedoState ( ) ) ;
158155 const wasBusyRef = React . useRef ( busy ) ;
159156 const hadFileMentionTokenRef = React . useRef ( false ) ;
160157 const appliedDraftNonceRef = React . useRef < number | null > ( null ) ;
161- const pastesRef = React . useRef < Map < number , string > > ( new Map ( ) ) ;
162- const pasteCounterRef = React . useRef < number > ( 0 ) ;
163- // Track expanded paste regions for toggle (Ctrl+O expand / collapse).
164- const expandedRegionsRef = React . useRef < Map < number , { start : number ; end : number ; content : string ; marker : string } > > (
165- new Map ( )
158+
159+ const { historyCursor , navigateHistory , exitHistoryBrowsing } = useHistoryNavigation (
160+ buffer ,
161+ setBuffer ,
162+ promptHistory
166163 ) ;
167164
165+ const { pastesRef, handlePaste, expandPasteMarkerAtCursor, resetPastes, hasCollapsedMarkers, hasExpandedRegions } =
166+ usePasteHandling ( buffer , updateBuffer , setStatusMessage ) ;
167+
168168 const fileMentionToken = getCurrentFileMentionToken ( buffer ) ;
169169 const hasFileMentionToken = fileMentionToken !== null ;
170170 const fileMentionKey = fileMentionToken ? `${ fileMentionToken . start } :${ fileMentionToken . query } ` : null ;
@@ -191,8 +191,6 @@ export const PromptInput = React.memo(function PromptInput({
191191 const showMenu = slashMenu . length > 0 ;
192192 const promptHistoryKey = React . useMemo ( ( ) => promptHistory . join ( "\0" ) , [ promptHistory ] ) ;
193193 const hasRunningProcess = runningProcesses && runningProcesses . size > 0 ;
194- const hasCollapsedMarkers = hasActivePasteMarkers ( buffer . text , pastesRef . current ) ;
195- const hasExpandedRegions = expandedRegionsRef . current . size > 0 ;
196194 const processOrPasteHint = hasRunningProcess
197195 ? " · ctrl+o view output"
198196 : hasCollapsedMarkers
@@ -268,17 +266,14 @@ export const PromptInput = React.memo(function PromptInput({
268266 setSelectedSkills ( [ ] ) ;
269267 setShowSkillsDropdown ( false ) ;
270268 setOpenRawModelDropdown ( false ) ;
271- setHistoryCursor ( - 1 ) ;
272- setDraftBeforeHistory ( null ) ;
269+ exitHistoryBrowsing ( ) ;
273270 clearPromptUndoRedoState ( undoRedoRef . current ) ;
274- pastesRef . current . clear ( ) ;
275- expandedRegionsRef . current . clear ( ) ;
276- } , [ promptDraft ] ) ;
271+ resetPastes ( ) ;
272+ } , [ promptDraft , exitHistoryBrowsing , resetPastes ] ) ;
277273
278274 useEffect ( ( ) => {
279- setHistoryCursor ( - 1 ) ;
280- setDraftBeforeHistory ( null ) ;
281- } , [ promptHistoryKey ] ) ;
275+ exitHistoryBrowsing ( ) ;
276+ } , [ promptHistoryKey , exitHistoryBrowsing ] ) ;
282277
283278 useTerminalInput (
284279 ( input , key ) => {
@@ -338,8 +333,7 @@ export const PromptInput = React.memo(function PromptInput({
338333 } else if ( ! isEmpty ( buffer ) ) {
339334 setBuffer ( EMPTY_BUFFER ) ;
340335 clearUndoRedoStacks ( ) ;
341- pastesRef . current . clear ( ) ;
342- expandedRegionsRef . current . clear ( ) ;
336+ resetPastes ( ) ;
343337 } else {
344338 setStatusMessage ( "press ctrl+d to exit" ) ;
345339 }
@@ -529,8 +523,7 @@ export const PromptInput = React.memo(function PromptInput({
529523 }
530524 if ( key . ctrl && ( input === "u" || input === "U" ) ) {
531525 updateBuffer ( ( ) => EMPTY_BUFFER ) ;
532- pastesRef . current . clear ( ) ;
533- expandedRegionsRef . current . clear ( ) ;
526+ resetPastes ( ) ;
534527 return ;
535528 }
536529 if ( key . ctrl && ( input === "w" || input === "W" ) ) {
@@ -594,11 +587,6 @@ export const PromptInput = React.memo(function PromptInput({
594587 clearPromptUndoRedoState ( undoRedoRef . current ) ;
595588 }
596589
597- function exitHistoryBrowsing ( ) : void {
598- setHistoryCursor ( - 1 ) ;
599- setDraftBeforeHistory ( null ) ;
600- }
601-
602590 function updateBuffer ( updater : ( state : PromptBufferState ) => PromptBufferState ) : void {
603591 exitHistoryBrowsing ( ) ;
604592 setBuffer ( ( current ) => {
@@ -608,107 +596,6 @@ export const PromptInput = React.memo(function PromptInput({
608596 } ) ;
609597 }
610598
611- function handlePaste ( pastedText : string ) : void {
612- const totalChars = pastedText . length ;
613-
614- if ( totalChars <= 1000 ) {
615- const newlineCount = ( pastedText . match ( / \n / g) ?? [ ] ) . length ;
616- if ( newlineCount <= 9 ) {
617- const clean = pastedText
618- . replace ( / \r \n | \r / g, "\n" )
619- . replace ( / [ \x00 - \x08 \x0b \x0c \x0e - \x1f \x7f ] / g, "" )
620- . replace ( / \t / g, " " ) ;
621- updateBuffer ( ( s ) => insertText ( s , clean ) ) ;
622- return ;
623- }
624- }
625-
626- // Large paste: store raw text, insert marker with line/char count.
627- const lineCount = ( pastedText . match ( / \n / g) ?? [ ] ) . length + 1 ;
628- pasteCounterRef . current += 1 ;
629- const pasteId = pasteCounterRef . current ;
630- pastesRef . current . set ( pasteId , pastedText ) ;
631-
632- const marker =
633- lineCount > 10 ? `[paste #${ pasteId } +${ lineCount } lines]` : `[paste #${ pasteId } ${ totalChars } chars]` ;
634-
635- updateBuffer ( ( s ) => insertText ( s , marker ) ) ;
636- }
637-
638- function expandPasteMarkerAtCursor ( ) : void {
639- // First, try to collapse an already-expanded region at the cursor.
640- for ( const [ id , region ] of expandedRegionsRef . current ) {
641- if ( buffer . cursor >= region . start && buffer . cursor <= region . end ) {
642- // Collapse back to marker.
643- expandedRegionsRef . current . delete ( id ) ;
644- pastesRef . current . set ( id , region . content ) ;
645- setTimeout ( ( ) => {
646- updateBuffer ( ( s ) => {
647- const text = s . text . slice ( 0 , region . start ) + region . marker + s . text . slice ( region . end ) ;
648- return { text, cursor : region . start + region . marker . length } ;
649- } ) ;
650- } , 0 ) ;
651- return ;
652- }
653- }
654-
655- // No expanded region at cursor — try to expand a paste marker.
656- const marker = findPasteMarkerContaining ( buffer ) ;
657- if ( ! marker ) {
658- setStatusMessage ( "No paste marker at cursor" ) ;
659- return ;
660- }
661- const content = pastesRef . current . get ( marker . id ) ;
662- if ( ! content ) {
663- setStatusMessage ( "Paste content not found" ) ;
664- return ;
665- }
666-
667- const pasteId = marker . id ;
668- const originalMarker = buffer . text . slice ( marker . start , marker . end ) ;
669- pastesRef . current . delete ( pasteId ) ;
670-
671- setTimeout ( ( ) => {
672- updateBuffer ( ( s ) => {
673- const text = s . text . slice ( 0 , marker . start ) + cleanPasteContent ( content ) + s . text . slice ( marker . end ) ;
674- const newEnd = marker . start + content . length ;
675- expandedRegionsRef . current . set ( pasteId , {
676- start : marker . start ,
677- end : newEnd ,
678- content,
679- marker : originalMarker ,
680- } ) ;
681- return { text, cursor : marker . start } ;
682- } ) ;
683- } , 0 ) ;
684- }
685-
686- function navigateHistory ( direction : - 1 | 1 ) : void {
687- if ( promptHistory . length === 0 ) {
688- return ;
689- }
690-
691- const previousCursor = historyCursor === - 1 ? promptHistory . length : historyCursor ;
692- const nextCursor = Math . max ( 0 , Math . min ( promptHistory . length , previousCursor + direction ) ) ;
693- const draft = historyCursor === - 1 ? buffer . text : draftBeforeHistory ;
694-
695- if ( historyCursor === - 1 ) {
696- setDraftBeforeHistory ( buffer . text ) ;
697- }
698-
699- if ( nextCursor === promptHistory . length ) {
700- const text = draft ?? "" ;
701- setBuffer ( { text, cursor : text . length } ) ;
702- setHistoryCursor ( - 1 ) ;
703- setDraftBeforeHistory ( null ) ;
704- return ;
705- }
706-
707- const text = promptHistory [ nextCursor ] ?? "" ;
708- setBuffer ( { text, cursor : text . length } ) ;
709- setHistoryCursor ( nextCursor ) ;
710- }
711-
712599 function insertFileMentionSelection ( item : FileMentionItem ) : void {
713600 if ( ! fileMentionToken ) {
714601 return ;
@@ -723,9 +610,8 @@ export const PromptInput = React.memo(function PromptInput({
723610 setImageUrls ( [ ] ) ;
724611 setSelectedSkills ( [ ] ) ;
725612 setShowSkillsDropdown ( false ) ;
726- pastesRef . current . clear ( ) ;
727- expandedRegionsRef . current . clear ( ) ;
728- pasteCounterRef . current = 0 ;
613+ exitHistoryBrowsing ( ) ;
614+ resetPastes ( ) ;
729615 }
730616
731617 function handleSlashSelection ( item : SlashCommandItem ) : void {
0 commit comments