@@ -77,6 +77,18 @@ function formatToolOutputLines( lines: string[] ): string {
7777 . join ( '\n' ) ;
7878}
7979
80+ // Faint variant of the user bubble used by `addUserMessage`, for prompts that
81+ // were staged during an active turn and haven't been dispatched yet.
82+ function formatQueuedPrompt ( text : string ) : string {
83+ const lines = text . split ( '\n' ) ;
84+ return lines
85+ . map ( ( line , i ) => {
86+ const body = i === 0 ? '↳ ' + line + ' ' : ' ' + line + ' ' ;
87+ return ' ' + chalk . bgHex ( '#e8eef5' ) . hex ( '#5a6b7d' ) ( body ) ;
88+ } )
89+ . join ( '\n' ) ;
90+ }
91+
8092class PromptEditor implements Component , Focusable {
8193 private editor : Editor ;
8294 private borderColorFn : ( text : string ) => string ;
@@ -365,6 +377,8 @@ export class AiChatUI implements AiOutputAdapter {
365377 private editor : PromptEditor ;
366378 private loader : Loader ;
367379 private messages : Container ;
380+ private queuedContainer : Container ;
381+ private queuedPrompts : string [ ] = [ ] ;
368382 private currentResponseText = '' ;
369383 private currentMarkdown : Markdown | null = null ;
370384 private submitResolve : ( ( text : string ) => void ) | null = null ;
@@ -476,6 +490,10 @@ export class AiChatUI implements AiOutputAdapter {
476490 this . currentMarkdown = null ;
477491 this . currentResponseText = '' ;
478492 this . messages . clear ( ) ;
493+ if ( this . queuedPrompts . length > 0 ) {
494+ this . queuedPrompts = [ ] ;
495+ this . renderQueuedContainer ( ) ;
496+ }
479497 this . tui . requestRender ( ) ;
480498 }
481499
@@ -497,6 +515,12 @@ export class AiChatUI implements AiOutputAdapter {
497515 this . messages = new Container ( ) ;
498516 this . tui . addChild ( this . messages ) ;
499517
518+ // Always mounted just after `messages` and (once shown) just before the
519+ // editor, so staged follow-up prompts render in that gap regardless of
520+ // whether the loader is currently visible.
521+ this . queuedContainer = new Container ( ) ;
522+ this . tui . addChild ( this . queuedContainer ) ;
523+
500524 this . loader = new Loader (
501525 this . tui ,
502526 ( str ) => chalk . yellow ( str ) ,
@@ -537,10 +561,21 @@ export class AiChatUI implements AiOutputAdapter {
537561
538562 this . editor . onSubmit = ( text ) => {
539563 const trimmed = text . trim ( ) ;
540- if ( trimmed && this . submitResolve ) {
564+ if ( ! trimmed ) {
565+ return ;
566+ }
567+ if ( this . submitResolve ) {
541568 const resolve = this . submitResolve ;
542569 this . submitResolve = null ;
543570 resolve ( trimmed ) ;
571+ return ;
572+ }
573+ // No waiter → we're mid-turn. Stage the prompt so it fires after
574+ // the current run ends; `waitForInput` drains the head.
575+ if ( this . _inAgentTurn ) {
576+ this . queuedPrompts . push ( trimmed ) ;
577+ this . editor . setText ( '' ) ;
578+ this . renderQueuedContainer ( ) ;
544579 }
545580 } ;
546581 // Ctrl+C to exit, Escape to interrupt/close picker, arrow keys for picker
@@ -650,6 +685,19 @@ export class AiChatUI implements AiOutputAdapter {
650685 this . renderSitePicker ( ) ;
651686 return { consume : true } ;
652687 }
688+ // Backspace on an empty editor pops the most recent queued prompt.
689+ // Mirrors the × discard affordance in the GUI — lightweight undo
690+ // for staged follow-ups without adding a new keybinding.
691+ if (
692+ matchesKey ( data , 'backspace' ) &&
693+ this . editorVisible &&
694+ this . queuedPrompts . length > 0 &&
695+ this . editor . getText ( ) === ''
696+ ) {
697+ this . queuedPrompts . pop ( ) ;
698+ this . renderQueuedContainer ( ) ;
699+ return { consume : true } ;
700+ }
653701 if ( matchesKey ( data , 'escape' ) && this . interruptCallback ) {
654702 this . wasInterrupted = true ;
655703 this . interruptCallback ( ) ;
@@ -1203,6 +1251,11 @@ export class AiChatUI implements AiOutputAdapter {
12031251 this . editor . setText ( '' ) ;
12041252 this . hideLoader ( ) ;
12051253 this . showEditor ( ) ;
1254+ if ( this . queuedPrompts . length > 0 ) {
1255+ const next = this . queuedPrompts . shift ( ) ! ;
1256+ this . renderQueuedContainer ( ) ;
1257+ return Promise . resolve ( next ) ;
1258+ }
12061259 return new Promise ( ( resolve ) => {
12071260 this . submitResolve = resolve ;
12081261 } ) ;
@@ -1222,6 +1275,15 @@ export class AiChatUI implements AiOutputAdapter {
12221275 this . tui . requestRender ( ) ;
12231276 }
12241277
1278+ private renderQueuedContainer ( ) : void {
1279+ this . queuedContainer . clear ( ) ;
1280+ for ( const prompt of this . queuedPrompts ) {
1281+ this . queuedContainer . addChild ( new Text ( '\n' + formatQueuedPrompt ( prompt ) , 0 , 0 ) ) ;
1282+ }
1283+ this . updateHints ( ) ;
1284+ this . tui . requestRender ( ) ;
1285+ }
1286+
12251287 private lastProgressText : Text | null = null ;
12261288
12271289 setLoaderMessage ( message : string , update ?: boolean ) : void {
@@ -1240,12 +1302,16 @@ export class AiChatUI implements AiOutputAdapter {
12401302
12411303 private showLoader ( message ?: string ) : void {
12421304 if ( ! this . loaderVisible ) {
1243- // Ensure editor is removed first so loader appears above it
1305+ // Re-attach trailing children in order so the final stack is
1306+ // [..., loader, queuedContainer, editor?] — loader just above the
1307+ // staged follow-ups, which sit just above the editor.
12441308 const wasEditorVisible = this . editorVisible ;
12451309 if ( wasEditorVisible ) {
12461310 this . tui . removeChild ( this . editor ) ;
12471311 }
1312+ this . tui . removeChild ( this . queuedContainer ) ;
12481313 this . tui . addChild ( this . loader ) ;
1314+ this . tui . addChild ( this . queuedContainer ) ;
12491315 if ( wasEditorVisible ) {
12501316 this . tui . addChild ( this . editor ) ;
12511317 }
@@ -1278,6 +1344,9 @@ export class AiChatUI implements AiOutputAdapter {
12781344 this . activeExpandablePreview . isExpanded ? __ ( 'ctrl+o collapse' ) : __ ( 'ctrl+o expand' )
12791345 ) ;
12801346 }
1347+ if ( this . queuedPrompts . length > 0 ) {
1348+ hints . push ( __ ( 'backspace to unqueue' ) ) ;
1349+ }
12811350 hints . push ( __ ( 'esc to interrupt' ) ) ;
12821351 this . editor . hints = hints ;
12831352 }
0 commit comments