@@ -908,6 +908,8 @@ describe("same-turn failover after inference.error", () => {
908908 const port = createRecordingPort ( ) ;
909909 const bridge = attachSessionBridge ( shell , port ) ;
910910 try {
911+ bridge . submit ( "retry this" , "immediate" ) ;
912+ bridge . handle ( { type : "message.received" , data : { message : { content : "retry this" } } } ) ;
911913 bridge . handle ( { type : "inference.start" , data : { } } ) ;
912914 bridge . handle ( {
913915 type : "inference.error" ,
@@ -928,6 +930,10 @@ describe("same-turn failover after inference.error", () => {
928930
929931 expect ( errorRows ( shell ) ) . toEqual ( [ ] ) ;
930932 expect ( shell . streamLog . map ( ( r ) => r . text ) . join ( "\n" ) ) . toContain ( "recovered" ) ;
933+ // The replay duplicates the operator's prompt; rollback drops the copy.
934+ expect ( shell . streamLog . filter ( ( r ) => r . role === "user" ) . map ( ( r ) => r . text ) ) . toEqual ( [
935+ "retry this" ,
936+ ] ) ;
931937 // Same-turn failover, not an operator stop — recovery must not borrow interrupt.
932938 expect ( port . calls . some ( ( c ) => c . op === "interrupt" ) ) . toBe ( false ) ;
933939 expect ( shell . streamLog . some ( ( r ) => r . meta === "stop" ) ) . toBe ( false ) ;
@@ -940,6 +946,56 @@ describe("same-turn failover after inference.error", () => {
940946 ) ;
941947 } ) ;
942948
949+ test ( "a steer echo at a tool boundary opens a new thinking row" , async ( ) => {
950+ await withTestRenderer (
951+ async ( h ) => {
952+ const shell = createAppShell ( h . renderer , {
953+ terminal : { columns : 80 , rows : 24 } ,
954+ wireKeys : false ,
955+ run : "idle" ,
956+ } ) ;
957+ const bridge = attachSessionBridge ( shell , createRecordingPort ( ) ) ;
958+ try {
959+ bridge . submit ( "first prompt" , "immediate" ) ;
960+ bridge . handle ( {
961+ type : "message.received" ,
962+ data : { message : { content : "first prompt" } } ,
963+ } ) ;
964+ bridge . handle ( { type : "inference.start" , data : { } } ) ;
965+ bridge . handle ( { type : "inference.thinking.delta" , data : { token : "planning" } } ) ;
966+ bridge . handle ( {
967+ type : "inference.tool_call.end" ,
968+ data : { name : "run_shell" , callId : "c1" , arguments : "{}" } ,
969+ } ) ;
970+ bridge . handle ( { type : "inference.done" , data : { } } ) ;
971+ bridge . submit ( "steer this" , "steer" ) ;
972+ bridge . handle ( {
973+ type : "tool.done" ,
974+ data : { result : { callId : "c1" , content : "ok" , isError : false } } ,
975+ } ) ;
976+ bridge . handle ( { type : "message.received" , data : { message : { content : "steer this" } } } ) ;
977+ bridge . handle ( { type : "inference.start" , data : { } } ) ;
978+ bridge . handle ( { type : "inference.thinking.delta" , data : { token : "after steer" } } ) ;
979+ bridge . handle ( { type : "inference.text.delta" , data : { token : "done" } } ) ;
980+
981+ const rows = shell . streamLog . map ( ( r ) => `${ r . meta ?? r . role } :${ r . text } ` ) ;
982+ expect ( rows . indexOf ( "thinking:planning" ) ) . toBeGreaterThan ( - 1 ) ;
983+ expect ( rows . indexOf ( "steering:steer this" ) ) . toBeGreaterThan (
984+ rows . indexOf ( "thinking:planning" ) ,
985+ ) ;
986+ expect ( rows . indexOf ( "thinking:after steer" ) ) . toBeGreaterThan (
987+ rows . indexOf ( "steering:steer this" ) ,
988+ ) ;
989+ expect ( shell . streamLog . filter ( ( r ) => r . meta === "thinking" ) ) . toHaveLength ( 2 ) ;
990+ } finally {
991+ bridge . dispose ( ) ;
992+ shell . dispose ( ) ;
993+ }
994+ } ,
995+ { width : 80 , height : 24 } ,
996+ ) ;
997+ } ) ;
998+
943999 test ( "interrupt then a new prompt keeps the prompt and the classified error" , async ( ) => {
9441000 await withTestRenderer (
9451001 async ( h ) => {
0 commit comments