@@ -363,6 +363,9 @@ program
363363 // Pending reload info — set by onReload, checked after waitUntilExit
364364 let pendingReload : { sessionId : string ; prefillText ?: string } | null = null ;
365365
366+ // Resize remount: UI-only unmount/re-render without touching app/soul.
367+ let pendingResizeRemount = false ;
368+
366369 // pushEvent will be set by Shell's onWireReady callback
367370 pushEvent = null ;
368371
@@ -375,8 +378,11 @@ program
375378 inkUnmountFn ?.( ) ;
376379 } ;
377380
378- // Create a UILoopFn that translates Wire messages → WireUIEvent for Shell
379- const createShellUILoopFn = ( pe : ( event : WireUIEvent ) => void ) : UILoopFn => {
381+ // Create a UILoopFn that translates Wire messages → WireUIEvent for Shell.
382+ // Uses the outer `pushEvent` variable (not a parameter) so that after a
383+ // resize remount, the running UILoopFn automatically routes events to the
384+ // new Shell instance's wire once onWireReady updates pushEvent.
385+ const createShellUILoopFn = ( ) : UILoopFn => {
380386 return async ( wire ) => {
381387 const uiSide = wire . uiSide ( true ) ;
382388 while ( true ) {
@@ -387,6 +393,8 @@ program
387393 if ( err instanceof QueueShutDown ) break ;
388394 throw err ;
389395 }
396+ const pe = pushEvent ;
397+ if ( ! pe ) continue ; // Shell not mounted yet (between resize remounts)
390398 const t = msg . __wireType ?? "" ;
391399 if ( t === "TurnBegin" ) {
392400 const raw = msg . user_input ;
@@ -475,6 +483,18 @@ program
475483 process . exit ( 1 ) ;
476484 }
477485
486+ // ── Render loop: re-renders Ink on resize without recreating app ──
487+ // Inner loop handles resize remounts (UI-only). Outer loop handles
488+ // full reloads (session switch — app.shutdown + KimiCLI.create).
489+ let rootHubQueue : any = null ;
490+ let rootHubStarted = false ;
491+ let initialPromptFired = false ;
492+ // eslint-disable-next-line no-constant-condition
493+ while ( true ) {
494+ pushEvent = null ;
495+ pendingResizeRemount = false ;
496+ inkUnmountFn = null ;
497+
478498 const { waitUntilExit, unmount : inkUnmount } = render (
479499 React . createElement ( Shell , {
480500 modelName : app . soul . modelName ,
@@ -490,7 +510,7 @@ program
490510 currentCancelController = cancelController ;
491511 const wireFile = app . session . wireFile ? new WireFile ( app . session . wireFile ) : undefined ;
492512 try {
493- await runSoul ( app . soul , input , createShellUILoopFn ( pushEvent ! ) , cancelController , {
513+ await runSoul ( app . soul , input , createShellUILoopFn ( ) , cancelController , {
494514 wireFile,
495515 runtime : app . soul . runtime ,
496516 } ) ;
@@ -536,6 +556,10 @@ program
536556 onReload : ( newSessionId : string , prefill ?: string ) => {
537557 triggerReload ( newSessionId , prefill ) ;
538558 } ,
559+ onResizeRemount : ( ) => {
560+ pendingResizeRemount = true ;
561+ inkUnmountFn ?.( ) ;
562+ } ,
539563 extraSlashCommands : app . soul . availableSlashCommands ,
540564 } ) ,
541565 { exitOnCtrlC : false } ,
@@ -546,8 +570,11 @@ program
546570 // Start background task to forward RootWireHub events to UI.
547571 // ApprovalRequests flow through ApprovalRuntime → RootWireHub,
548572 // NOT through the soul's Wire, so we need a separate subscriber.
549- let rootHubQueue : any = null ;
550- if ( app . soul . runtime . rootWireHub ) {
573+ // Set up once before the inner render loop — survives resize remounts
574+ // because it reads the outer `pushEvent` variable which gets updated on
575+ // each Shell mount via onWireReady.
576+ if ( ! rootHubStarted && app . soul . runtime . rootWireHub ) {
577+ rootHubStarted = true ;
551578 rootHubQueue = app . soul . runtime . rootWireHub . subscribe ( ) ;
552579 ( async ( ) => {
553580 try {
@@ -583,12 +610,13 @@ program
583610 } ) ( ) ;
584611 }
585612
586- // Run initial prompt if provided (only on first iteration)
587- if ( currentPrompt ) {
613+ // Run initial prompt if provided (only on first render, not on resize remounts)
614+ if ( currentPrompt && ! initialPromptFired ) {
615+ initialPromptFired = true ;
588616 const cancelController = new AbortController ( ) ;
589617 currentCancelController = cancelController ;
590618 const wireFile = app . session . wireFile ? new WireFile ( app . session . wireFile ) : undefined ;
591- runSoul ( app . soul , currentPrompt , createShellUILoopFn ( pushEvent ! ) , cancelController , {
619+ runSoul ( app . soul , currentPrompt , createShellUILoopFn ( ) , cancelController , {
592620 wireFile,
593621 runtime : app . soul . runtime ,
594622 } ) . catch ( ( err ) => {
@@ -597,6 +625,18 @@ program
597625 }
598626
599627 await waitUntilExit ( ) ;
628+
629+ // Resize remount: only re-render Ink, keep app/soul/wire alive.
630+ // The inner render loop continues; soul streams reconnect via pushEvent.
631+ if ( pendingResizeRemount ) {
632+ currentPrefillText = undefined ; // Don't re-show prefill text
633+ continue ; // → inner render loop
634+ }
635+
636+ // Not a resize — break out to outer loop for reload or exit
637+ break ;
638+ } // end inner render loop
639+
600640 disableBracketedPaste ( ) ;
601641 if ( rootHubQueue && app . soul . runtime . rootWireHub ) {
602642 app . soul . runtime . rootWireHub . unsubscribe ( rootHubQueue ) ;
0 commit comments