Compare commits

...

2 Commits

Author SHA1 Message Date
Kit Langton 7784767d91 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.
2026-08-20 00:42:40 -04:00
Kit Langton 3a7eb80b9d 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.
2026-08-19 23:21:14 -04:00
+23 -5
View File
@@ -121,7 +121,14 @@ 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
* 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
@@ -1124,12 +1131,25 @@ 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 (
<context.Provider
value={{
get width() {
return contentWidth()
},
terminal: {
get width() {
return terminalWidth()
},
get height() {
return terminalHeight()
},
},
sessionID: route.sessionID,
thinkingMode,
showThinking,
@@ -1805,7 +1825,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 +1848,10 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) {
<span style={{ fg: props.message.error ? theme.text.subdued : local.agent.color(props.message.agent) }}>
{Locale.titlecase(props.message.agent)}
</span>
<Show when={dimensions().width >= 28}>
<Show when={ctx.terminal.width >= 28}>
<span style={{ fg: theme.text.subdued }}> · {model()}</span>
</Show>
<Show when={duration() && (dimensions().width < 28 || dimensions().width >= 36)}>
<Show when={duration() && (ctx.terminal.width < 28 || ctx.terminal.width >= 36)}>
<span style={{ fg: theme.text.subdued }}> · {Locale.duration(duration())}</span>
</Show>
<Show when={interrupted()}>
@@ -2521,9 +2540,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 (