22 * useShellLayout.ts — Terminal height tracking and static items computation.
33 */
44
5- import React , { useEffect , useState } from "react" ;
5+ import React , { useEffect , useRef , useState } from "react" ;
66import { useStdout } from "ink" ;
77import type { UIMessage } from "./events.ts" ;
88
@@ -21,16 +21,35 @@ export function useShellLayout(messages: UIMessage[], isStreaming: boolean) {
2121 const { stdout } = useStdout ( ) ;
2222 const [ termHeight , setTermHeight ] = useState ( stdout ?. rows || 24 ) ;
2323
24+ // Monotonic counter incremented after resize settles (debounced).
25+ // Used as React key on Shell's root Box to force a full subtree rebuild,
26+ // which makes <Static> re-output all items at the new terminal width.
27+ // Shell's own hooks (useWire, useShellInput, etc.) are unaffected because
28+ // Shell itself does not unmount — only its returned JSX tree is rebuilt.
29+ const [ resizeKey , setResizeKey ] = useState ( 0 ) ;
30+ const debounceRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
31+
2432 useEffect ( ( ) => {
25- const onResize = ( ) => setTermHeight ( stdout ?. rows || 24 ) ;
33+ const onResize = ( ) => {
34+ setTermHeight ( stdout ?. rows || 24 ) ;
35+
36+ if ( debounceRef . current ) clearTimeout ( debounceRef . current ) ;
37+ debounceRef . current = setTimeout ( ( ) => {
38+ debounceRef . current = null ;
39+ setResizeKey ( ( k ) => k + 1 ) ;
40+ } , 300 ) ;
41+ } ;
2642 stdout ?. on ( "resize" , onResize ) ;
27- return ( ) => { stdout ?. off ( "resize" , onResize ) ; } ;
43+ return ( ) => {
44+ stdout ?. off ( "resize" , onResize ) ;
45+ if ( debounceRef . current ) clearTimeout ( debounceRef . current ) ;
46+ } ;
2847 } , [ stdout ] ) ;
2948
3049 const staticItems = React . useMemo (
3150 ( ) => buildStaticItems ( messages , isStreaming ) ,
3251 [ messages , isStreaming ] ,
3352 ) ;
3453
35- return { termHeight, staticItems } ;
54+ return { termHeight, staticItems, resizeKey } ;
3655}
0 commit comments