@@ -15,6 +15,7 @@ import { paintStreamRow } from "./stream"
1515import {
1616 appendStreamRow ,
1717 appendTranscript ,
18+ applyShellCancelLast ,
1819 closeInsetOverlay ,
1920 createAppShell ,
2021 interruptShell ,
@@ -478,6 +479,99 @@ describe("product skin: stream + queue + overlay", () => {
478479 )
479480 } )
480481
482+ test ( "Ctrl+G cancels the last queued message and the screen shows it" , async ( ) => {
483+ await withTestRenderer (
484+ async ( h ) => {
485+ const shell = createAppShell ( h . renderer , {
486+ terminal : { columns : 80 , rows : 24 } ,
487+ wireKeys : false ,
488+ run : "busy" ,
489+ } )
490+ try {
491+ shell . prompt . value = "keep this one"
492+ submitPrompt ( shell , "queue" )
493+ shell . prompt . value = "oops wrong message"
494+ submitPrompt ( shell , "queue" )
495+ expect ( shell . pendingQueue ) . toBe ( 2 )
496+
497+ const before = shell . streamLog . map ( ( row ) => ( {
498+ text : row . text ,
499+ meta : row . meta ,
500+ cancelled : row . cancelled ,
501+ } ) )
502+ expect ( before ) . toEqual ( [
503+ { text : "keep this one" , meta : "queue" , cancelled : undefined } ,
504+ { text : "oops wrong message" , meta : "queue" , cancelled : undefined } ,
505+ ] )
506+ await h . renderOnce ( )
507+ const frameBefore = h . captureCharFrame ( )
508+ expect ( frameBefore ) . toContain ( "keep this one" )
509+ expect ( frameBefore ) . toContain ( "oops wrong message" )
510+ expect ( frameBefore ) . not . toContain ( "[cancelled]" )
511+
512+ applyShellCancelLast ( shell )
513+
514+ expect ( shell . pendingQueue ) . toBe ( 1 )
515+ expect ( shell . session . items [ 0 ] ! . text ) . toBe ( "keep this one" )
516+
517+ const after = shell . streamLog . map ( ( row ) => ( {
518+ text : row . text ,
519+ meta : row . meta ,
520+ cancelled : row . cancelled ,
521+ } ) )
522+ // The stored text is untouched — the cancel is a flag the paint
523+ // layer reads, not a rewrite of what the operator typed.
524+ expect ( after ) . toEqual ( [
525+ { text : "keep this one" , meta : "queue" , cancelled : undefined } ,
526+ { text : "oops wrong message" , meta : "cancelled" , cancelled : true } ,
527+ ] )
528+
529+ // The screen, not just the model, is asserted on: this is exactly
530+ // what the first attempt at this issue got wrong (the row read
531+ // back unchanged from streamLog while the model looked cancelled).
532+ await h . renderOnce ( )
533+ const frameAfter = h . captureCharFrame ( )
534+ expect ( frameAfter ) . toContain ( "[cancelled] oops wrong message" )
535+ expect ( frameAfter ) . toContain ( "keep this one" )
536+ } finally {
537+ shell . dispose ( )
538+ }
539+ } ,
540+ { width : 80 , height : 24 } ,
541+ )
542+ } )
543+
544+ test ( "Ctrl+G cancels a steered message the same way" , async ( ) => {
545+ await withTestRenderer (
546+ async ( h ) => {
547+ const shell = createAppShell ( h . renderer , {
548+ terminal : { columns : 80 , rows : 24 } ,
549+ wireKeys : false ,
550+ run : "busy" ,
551+ } )
552+ try {
553+ shell . prompt . value = "steer me now"
554+ submitPrompt ( shell , "steer" )
555+ expect ( shell . pendingQueue ) . toBe ( 1 )
556+ expect ( shell . session . items [ 0 ] ! . kind ) . toBe ( "steer" )
557+
558+ applyShellCancelLast ( shell )
559+
560+ expect ( shell . pendingQueue ) . toBe ( 0 )
561+ expect ( shell . session . items ) . toHaveLength ( 0 )
562+ expect ( shell . streamLog [ 0 ] ?. cancelled ) . toBe ( true )
563+ expect ( shell . streamLog [ 0 ] ?. meta ) . toBe ( "cancelled" )
564+
565+ await h . renderOnce ( )
566+ expect ( h . captureCharFrame ( ) ) . toContain ( "[cancelled] steer me now" )
567+ } finally {
568+ shell . dispose ( )
569+ }
570+ } ,
571+ { width : 80 , height : 24 } ,
572+ )
573+ } )
574+
481575 test ( "inset overlay opens; Esc restores prompt focus" , async ( ) => {
482576 await withTestRenderer (
483577 async ( h ) => {
0 commit comments