@@ -989,35 +989,30 @@ export function leavePromptSurface(
989989function handlePasteComplete (
990990 pasteState : PasteState ,
991991 rl : readline . Interface ,
992- output : NodeJS . WriteStream
992+ output : NodeJS . WriteStream ,
993+ renderActivePrompt : ( ) => void
993994) : void {
994995 const display = getContentDisplay ( pasteState . buffer ) ;
995- const rlAny = rl as readline . Interface & { line : string ; cursor : number ; _refreshLine ?: ( ) => void } ;
996+ const rlAny = rl as readline . Interface & { line : string ; cursor : number } ;
996997
997998 // Get any prefix content that was typed before the paste
998999 const prefix = pasteState . prefixContent || '' ;
9991000
1000- // Count newlines to know how many extra prompt lines were printed
1001- const newlineCount = ( pasteState . buffer . match ( / \n / g) || [ ] ) . length ;
1002-
1003- // If readline echoed pasted rows, clear that transient output.
1004- // When output is suppressed, skip this so we don't move into chat logs.
1001+ // If readline echoed pasted rows (timeout fallback path where output was
1002+ // not suppressed), clear those transient lines. On the normal bracketed-
1003+ // paste path outputSuppressed is true so the box was never disturbed.
10051004 if ( ! pasteState . outputSuppressed ) {
1006- // Clear all the extra lines that readline printed during paste
1007- // Move cursor up for each newline, clearing as we go
1005+ const newlineCount = ( pasteState . buffer . match ( / \n / g) || [ ] ) . length ;
10081006 for ( let i = 0 ; i < newlineCount ; i ++ ) {
1009- readline . moveCursor ( output , 0 , - 1 ) ; // Move up one line
1010- readline . clearLine ( output , 0 ) ; // Clear that line
1007+ readline . moveCursor ( output , 0 , - 1 ) ;
1008+ readline . clearLine ( output , 0 ) ;
10111009 }
1012-
1013- // Now we're back at the original prompt line - clear it too
10141010 readline . cursorTo ( output , 0 ) ;
10151011 readline . clearLine ( output , 0 ) ;
10161012 }
10171013
10181014 if ( display . isPasted ) {
10191015 // Large paste: show indicator, store actual content
1020- // Prepend prefix to hidden content so it's included in submission
10211016 pasteState . hiddenContent = prefix + display . actual ;
10221017 rlAny . line = prefix + display . visual ;
10231018 rlAny . cursor = rlAny . line . length ;
@@ -1027,8 +1022,9 @@ function handlePasteComplete(
10271022 rlAny . cursor = rlAny . line . length ;
10281023 }
10291024
1030- // Refresh the display with clean prompt
1031- rl . prompt ( true ) ;
1025+ // Use the boxed renderer — NOT rl.prompt(true) — so the composer stays
1026+ // anchored at the bottom and the 3-row layout is preserved.
1027+ renderActivePrompt ( ) ;
10321028
10331029 // Clear the buffer and prefix
10341030 pasteState . buffer = '' ;
@@ -1045,7 +1041,6 @@ async function promptOnce(options: PromptOnceOptions): Promise<PromptResult> {
10451041 // Initialize paste state for bracketed paste detection
10461042 const pasteState = createPasteState ( ) ;
10471043 let contextualHelpVisible = false ;
1048- let renderedContextualHelpLines = 0 ;
10491044
10501045 const applyPlanModePrefix = ( line : string ) : string => {
10511046 const planPrefix = getPlanModeManager ( ) . isEnabled ( ) ? 'plan:on' : 'plan:off' ;
@@ -1066,26 +1061,8 @@ async function promptOnce(options: PromptOnceOptions): Promise<PromptResult> {
10661061 return applyPlanModePrefix ( statusLine ?? '' ) ;
10671062 } ;
10681063
1069- const getActiveContextualHelpLines = ( ) : string [ ] => {
1070- if ( ! contextualHelpVisible ) {
1071- return [ ] ;
1072- }
1073- const rlAny = rl as readline . Interface & { line ?: string } ;
1074- const currentLine = rlAny . line ?? '' ;
1075- const width = getPromptBlockWidth ( stdOutput . columns ) ;
1076- return buildContextualHelpPanelLines ( currentLine , width , files , slashCommands ) ;
1077- } ;
1078-
10791064 const renderPromptSurface = ( isResize = false , hasExistingPromptBlock = true ) : void => {
1080- const helpLines = getActiveContextualHelpLines ( ) ;
1081- renderPromptLine (
1082- rl ,
1083- getActiveStatusLine ( ) ,
1084- stdOutput ,
1085- isResize ,
1086- hasExistingPromptBlock ,
1087- suggestionText
1088- ) ;
1065+ renderPromptLine ( rl , getActiveStatusLine ( ) , stdOutput , isResize , hasExistingPromptBlock , suggestionText ) ;
10891066 } ;
10901067
10911068 const resizeWatcher = new TerminalResizeWatcher ( stdOutput , ( ) => {
@@ -1104,23 +1081,6 @@ async function promptOnce(options: PromptOnceOptions): Promise<PromptResult> {
11041081 renderPromptSurface ( false , true ) ;
11051082 }
11061083
1107- // Override readline's _refreshLine to use our renderPromptLine instead.
1108- // readline's default _refreshLine miscalculates cursor position when the
1109- // prompt contains ANSI escape codes (chalk styling), causing the cursor
1110- // to appear on top of typed text rather than after it.
1111- const rlInternal = rl as readline . Interface & { _refreshLine ?: ( ) => void } ;
1112- const originalRefreshLine = typeof rlInternal . _refreshLine === 'function'
1113- ? rlInternal . _refreshLine . bind ( rlInternal )
1114- : undefined ;
1115-
1116- if ( typeof rlInternal . _refreshLine === 'function' ) {
1117- rlInternal . _refreshLine = ( ) => {
1118- if ( ! pasteState . isInPaste ) {
1119- renderPromptLine ( rl , statusLine , stdOutput ) ;
1120- }
1121- } ;
1122- }
1123-
11241084 return new Promise < PromptResult > ( ( resolve ) => {
11251085 let ctrlCCount = 0 ;
11261086 let closed = false ;
@@ -1327,7 +1287,7 @@ async function promptOnce(options: PromptOnceOptions): Promise<PromptResult> {
13271287 pasteState . buffer , onImageDetected , { announce : false , output : stdOutput }
13281288 ) ;
13291289 }
1330- handlePasteComplete ( pasteState , rl , stdOutput ) ;
1290+ handlePasteComplete ( pasteState , rl , stdOutput , renderActivePrompt ) ;
13311291 // Schedule a deferred fallback scan in case the synchronous
13321292 // replacement missed (e.g. file not yet materialized).
13331293 scheduleInlineImageScan ( 10 ) ;
@@ -1358,7 +1318,7 @@ async function promptOnce(options: PromptOnceOptions): Promise<PromptResult> {
13581318 pasteState . buffer , onImageDetected , { announce : false , output : stdOutput }
13591319 ) ;
13601320 }
1361- handlePasteComplete ( pasteState , rl , stdOutput ) ;
1321+ handlePasteComplete ( pasteState , rl , stdOutput , renderActivePrompt ) ;
13621322 scheduleInlineImageScan ( 10 ) ;
13631323 }
13641324 } , 50 ) ;
@@ -1620,6 +1580,10 @@ function getComposerBorderStyle(line: string): InputBorderStyle {
16201580 return 'default' ;
16211581}
16221582
1583+ // Track the width used by the last renderPromptLine call so we can detect
1584+ // width changes (resize) and compute reflow line counts for clearing.
1585+ let lastRenderedPromptWidth = 0 ;
1586+
16231587function renderPromptLine (
16241588 rl : readline . Interface ,
16251589 statusLine : string | { left : string ; right : string } | undefined ,
@@ -1638,25 +1602,41 @@ function renderPromptLine(
16381602 const bottomBorder = drawInputBottomBorder ( width , borderStyle ) ;
16391603 const statusRow = formatPromptStatusRow ( statusLine , width ) ;
16401604
1605+ // Detect width change even when called from _refreshLine (which passes
1606+ // isResize=false). Readline triggers _refreshLine on resize before our
1607+ // debounced handler fires, so we must use the reflow-aware clearing path
1608+ // whenever the width has actually changed.
1609+ const widthChanged = lastRenderedPromptWidth > 0 && width !== lastRenderedPromptWidth ;
1610+ const effectiveResize = isResize || widthChanged ;
1611+
16411612 // Keep readline's prompt in sync for line editing internals.
16421613 rl . setPrompt ( PROMPT_PREFIX ) ;
16431614
16441615 // Hide cursor during rendering to prevent flicker/slow blinking.
1645- // The cursor visibly jumps as lines are cleared and rewritten;
1646- // hiding it produces a clean, natural blink at the final position.
16471616 output . write ( '\x1b[?25l' ) ;
16481617
1649- if ( isResize ) {
1650- // Cursor sits on the input row. Move up to include the top border
1651- // before clearing, otherwise old borders at previous width remain.
1652- for ( let i = 0 ; i < PROMPT_LINES_ABOVE_INPUT ; i ++ ) {
1653- readline . moveCursor ( output , 0 , - 1 ) ;
1654- }
1618+ if ( effectiveResize && hasExistingPromptBlock ) {
1619+ // When the terminal resizes, it reflows all previously written content.
1620+ // A line of N chars wraps to ceil(N / newCols) physical rows at the new
1621+ // terminal width. We must move up enough to reach above ALL reflowed
1622+ // remnants of the old prompt block before clearing.
1623+ const termCols = output . columns ?? 80 ;
1624+ const oldWidth = lastRenderedPromptWidth || width ;
1625+ const logicalLines = PROMPT_BLOCK_LINE_COUNT + STATUS_LINE_COUNT ;
1626+ // Use actual terminal columns (not prompt width) since that's what
1627+ // the terminal uses for reflow calculations.
1628+ const rowsPerOldLine = Math . max ( 1 , Math . ceil ( oldWidth / Math . max ( 1 , termCols ) ) ) ;
1629+ const totalReflowedRows = logicalLines * rowsPerOldLine ;
1630+ // Move up generously to reach above all reflowed prompt content.
1631+ // Add extra margin because the cursor's physical position within the
1632+ // reflowed input row is uncertain.
1633+ const moveUp = totalReflowedRows + rowsPerOldLine ;
1634+ readline . moveCursor ( output , 0 , - moveUp ) ;
16551635 readline . cursorTo ( output , 0 ) ;
16561636 readline . clearScreenDown ( output ) ;
16571637 } else if ( hasExistingPromptBlock ) {
1658- // Cursor normally sits on the input row.
1659- // Clear the full prompt block in place before re-drawing .
1638+ // Same-width redraw: cursor sits on the input row.
1639+ // Clear the fixed 4-line prompt block in place.
16601640 readline . cursorTo ( output , 0 ) ;
16611641 for ( let i = 0 ; i < PROMPT_LINES_ABOVE_INPUT ; i ++ ) {
16621642 readline . moveCursor ( output , 0 , - 1 ) ;
@@ -1673,9 +1653,7 @@ function renderPromptLine(
16731653 }
16741654 readline . cursorTo ( output , 0 ) ;
16751655 } else {
1676- // Initial render: the cursor has been placed on a fresh row by createReadline,
1677- // but defensively clear the current line before drawing the top border to
1678- // eliminate any residual characters that could cause a one-frame flash.
1656+ // Initial render: defensively clear current line before drawing.
16791657 readline . cursorTo ( output , 0 ) ;
16801658 readline . clearLine ( output , 0 ) ;
16811659 }
@@ -1687,11 +1665,11 @@ function renderPromptLine(
16871665 output . write ( statusRow ) ;
16881666
16891667 // Move cursor back to input row, inside the box.
1690- // drawInputBox uses chalk background only (no │ side borders),
1691- // so cursorColumn from buildPromptRenderState is already correct.
16921668 readline . moveCursor ( output , 0 , - ( PROMPT_LINES_BELOW_INPUT + STATUS_LINE_COUNT ) ) ;
16931669 readline . cursorTo ( output , prompt . cursorColumn ) ;
16941670
16951671 // Show cursor at its final, correct position.
16961672 output . write ( '\x1b[?25h' ) ;
1673+
1674+ lastRenderedPromptWidth = width ;
16971675}
0 commit comments