@@ -115,9 +115,10 @@ export function remoteTmuxSessionNames(): string[] {
115115 return names ;
116116}
117117
118- /// One step of a tmux command chain: the arguments of a single tmux call, or a
119- /// pause between two of them.
120- export type TmuxStep = string [ ] | { sleep : number | string } ;
118+ /// One step of a tmux command chain: the arguments of a single tmux call, a
119+ /// pause between two of them, or a raw shell fragment (with `%TMUX%` standing
120+ /// in for the tmux binary) for the checks a plain call cannot express.
121+ export type TmuxStep = string [ ] | { sleep : number | string } | { raw : string } ;
121122
122123export interface TmuxRunOptions {
123124 /// Run detached and return immediately, for chains that sleep first.
@@ -151,11 +152,11 @@ export function buildTmuxCommand(
151152 const machine = remoteMachineForSession ( sessionName ) ;
152153 const tmux = machine ? "tmux" : findTmux ( ) ;
153154 const script = steps
154- . map ( ( step ) =>
155- Array . isArray ( step )
156- ? ` ${ tmux } ${ step . map ( shellToken ) . join ( " " ) } `
157- : `sleep ${ shellToken ( String ( step . sleep ) ) } `
158- )
155+ . map ( ( step ) => {
156+ if ( Array . isArray ( step ) ) return ` ${ tmux } ${ step . map ( shellToken ) . join ( " " ) } ` ;
157+ if ( "raw" in step ) return step . raw . replaceAll ( "%TMUX%" , tmux ) ;
158+ return `sleep ${ shellToken ( String ( step . sleep ) ) } ` ;
159+ } )
159160 . join ( " && " ) ;
160161 const command = machine
161162 ? `boxd machine exec ${ shellToken ( machine ) } -- ${ shellEscape ( script ) } `
@@ -347,6 +348,34 @@ export function sendTmuxKey(sessionName: string, key: string): { ok: boolean; er
347348 }
348349}
349350
351+ /// Paste text into the composer, submit it, and make sure the submit landed.
352+ ///
353+ /// Under load the TUI can drop an Enter that arrives while it is still
354+ /// processing the paste before it, and the text left sitting in the composer
355+ /// concatenates with whatever is pasted next — a scheduled "/compact" plus its
356+ /// follow-up became one "/compactYou are..." line this way. The check re-sends
357+ /// Enter while the pasted text still shows at the bottom of the pane. After a
358+ /// real submit the transcript echo can match too, but those retries land
359+ /// within seconds of the submit, on an empty composer, where Enter is a no-op.
360+ function submitSteps ( sessionName : string , text : string ) : TmuxStep [ ] {
361+ const steps : TmuxStep [ ] = [
362+ ...pasteSteps ( sessionName , text ) ,
363+ { sleep : 1 } ,
364+ [ "send-keys" , "-t" , sessionName , "Enter" ] ,
365+ ] ;
366+ const probe = text . split ( "\n" ) [ 0 ] . trim ( ) . slice ( 0 , 40 ) ;
367+ if ( probe . length >= 4 ) {
368+ const session = shellToken ( sessionName ) ;
369+ steps . push ( {
370+ raw :
371+ `{ i=0; while [ "$i" -lt 2 ]; do sleep 1.5; ` +
372+ `%TMUX% capture-pane -t ${ session } -p | tail -8 | grep -qF -- ${ shellToken ( probe ) } || break; ` +
373+ `%TMUX% send-keys -t ${ session } Enter; i=$((i+1)); done; }` ,
374+ } ) ;
375+ }
376+ return steps ;
377+ }
378+
350379export function scheduleTmuxPrompt (
351380 sessionName : string ,
352381 text : string ,
@@ -356,12 +385,7 @@ export function scheduleTmuxPrompt(
356385 try {
357386 runTmux (
358387 sessionName ,
359- [
360- { sleep : delay } ,
361- ...pasteSteps ( sessionName , text ) ,
362- { sleep : 0.1 } ,
363- [ "send-keys" , "-t" , sessionName , "Enter" ] ,
364- ] ,
388+ [ { sleep : delay } , ...submitSteps ( sessionName , text ) ] ,
365389 { detached : true }
366390 ) ;
367391 return { ok : true } ;
@@ -386,19 +410,15 @@ export function scheduleTmuxSelfCompact(
386410 // recognised as a slash command. 2s gives a comfortable buffer.
387411 { sleep : 2 } ,
388412 [ "send-keys" , "-t" , sessionName , "Escape" ] ,
389- ...pasteSteps ( sessionName , "/compact" ) ,
390- { sleep : 0.15 } ,
391- [ "send-keys" , "-t" , sessionName , "Enter" ] ,
413+ // The interrupt redraws the composer; pasting into the middle of that is
414+ // how an Enter gets dropped, so let it settle first.
415+ { sleep : 0.5 } ,
416+ ...submitSteps ( sessionName , "/compact" ) ,
392417 ] ;
393418
394419 const followUpSteps : TmuxStep [ ] = followUp . trim ( ) . length === 0
395420 ? [ ]
396- : [
397- { sleep : delay } ,
398- ...pasteSteps ( sessionName , followUp ) ,
399- { sleep : 0.1 } ,
400- [ "send-keys" , "-t" , sessionName , "Enter" ] ,
401- ] ;
421+ : [ { sleep : delay } , ...submitSteps ( sessionName , followUp ) ] ;
402422
403423 try {
404424 runTmux ( sessionName , [ ...compactSteps , ...followUpSteps ] , { detached : true } ) ;
0 commit comments