From 3a7eb80b9d7f9dd4b03e69681a727ee35cd884a2 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 19 Aug 2026 23:21:14 -0400 Subject: [PATCH 1/2] fix(tui): stop registering one resize listener per transcript row AssistantFooter and SessionImages each called useTerminalDimensions(), which subscribes a renderer resize listener per mounted component. SessionImages did so even when it renders nothing, and it mounts per user message, per tool part, and per grouped tool section, so listener count grew linearly with transcript length and tripped Bun's EventTarget warning ("11 resize listeners added to [CliRenderer]") within a few prompts. The session route already subscribes once; expose that as a reactive terminal size on the session context and read it from the row components. --- packages/tui/src/routes/session/index.tsx | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 357df25780d6..ca63b956b32c 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -122,6 +122,12 @@ type PendingAction = "steer" | "queue" | "cancel" const context = createContext<{ width: number + /** + * Shared reactive terminal size. Transcript-row components must read this + * instead of calling useTerminalDimensions(), which registers one renderer + * resize listener per mounted component and grows with transcript length. + */ + terminal: { width: number; height: number } sessionID: string thinkingMode: () => ThinkingMode showThinking: () => boolean @@ -1130,6 +1136,14 @@ export function Session(props: { verticalTabsWidth: number }) { get width() { return contentWidth() }, + terminal: { + get width() { + return dimensions().width + }, + get height() { + return dimensions().height + }, + }, sessionID: route.sessionID, thinkingMode, showThinking, @@ -1805,7 +1819,6 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) { const ctx = use() const data = useData() const local = useLocal() - const dimensions = useTerminalDimensions() const theme = useTheme("elevated") const model = createMemo( () => @@ -1829,10 +1842,10 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) { {Locale.titlecase(props.message.agent)} - = 28}> + = 28}> · {model()} - = 36)}> + = 36)}> · {Locale.duration(duration())} @@ -2521,9 +2534,8 @@ function ToolImages(props: { parts: readonly SessionMessageAssistantTool[] }) { function SessionImages(props: { images: readonly { uri: string }[]; paddingLeft?: number }) { const ctx = use() const dialog = useDialog() - const dimensions = useTerminalDimensions() const images = createMemo(() => (ctx.config.session?.image_preview ? props.images : [])) - const height = createMemo(() => Math.max(4, Math.min(8, Math.floor(dimensions().height / 4)))) + const height = createMemo(() => Math.max(4, Math.min(8, Math.floor(ctx.terminal.height / 4)))) const visible = createMemo(() => images().slice(0, 3)) return ( From 7784767d9198d2507cbbd89a642546efab7157ae Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 20 Aug 2026 00:42:40 -0400 Subject: [PATCH 2/2] refactor: memoize shared terminal axes and label content width Review follow-ups: back the context's terminal getters with per-axis memos so width readers do not re-run on height-only resizes (and vice versa), and document that the context's bare width is content width. --- packages/tui/src/routes/session/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index ca63b956b32c..2cfc352fd6d5 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -121,6 +121,7 @@ const TRANSCRIPT_BACKFILL_CHUNK = 60 type PendingAction = "steer" | "queue" | "cancel" const context = createContext<{ + /** Content width: terminal width minus vertical tabs, sidebar, and padding. */ width: number /** * Shared reactive terminal size. Transcript-row components must read this @@ -1130,6 +1131,11 @@ export function Session(props: { verticalTabsWidth: number }) { ), ) + // Memoized per axis so width readers do not re-run on height-only resizes + // (dimensions() is one object signal with identity equality) and vice versa. + const terminalWidth = createMemo(() => dimensions().width) + const terminalHeight = createMemo(() => dimensions().height) + return (