Skip to content

Commit 9f9b2dc

Browse files
feat(ts): use resizeKey to rebuild Shell JSX subtree on terminal resize
1 parent 0ad8bc0 commit 9f9b2dc

2 files changed

Lines changed: 25 additions & 6 deletions

File tree

src/kimi_cli_ts/ui/shell/Shell.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,12 @@ export function Shell({
193193
});
194194

195195
// ── Layout ──
196-
const { staticItems } = useShellLayout(wire.messages, wire.isStreaming);
196+
const { staticItems, resizeKey } = useShellLayout(wire.messages, wire.isStreaming);
197197
const mode = inputState.mode;
198198
const promptSymbol = getPromptSymbol(mode, inputState.shellMode, thinking, wire.status?.plan_mode ?? false);
199199

200200
return (
201-
<Box flexDirection="column">
201+
<Box key={resizeKey} flexDirection="column">
202202
<Static items={staticItems}>
203203
{(item: any) =>
204204
item._isWelcome ? (

src/kimi_cli_ts/ui/shell/useShellLayout.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
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";
66
import { useStdout } from "ink";
77
import 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

Comments
 (0)