Skip to content

fix(tui): stop registering one resize listener per transcript row - #43562

Open
kitlangton wants to merge 2 commits into
v2from
session-resize-listeners
Open

fix(tui): stop registering one resize listener per transcript row#43562
kitlangton wants to merge 2 commits into
v2from
session-resize-listeners

Conversation

@kitlangton

Copy link
Copy Markdown
Contributor

What

Listener count on the renderer grew linearly with transcript length, tripping Bun's EventTarget warning ("11 resize listeners added to [CliRenderer]. MaxListeners is undefined...") within about four prompts of a live session and climbing into the dozens over a long one.

Before / After

Before: every AssistantFooter (one per assistant message) and every SessionImages (one per user message, per tool part, and per grouped tool section — even when it renders nothing, since the hook ran before the empty-images guard) called useTerminalDimensions(), which registers a renderer resize listener for the lifetime of the mounted row. Transcript rows stay mounted for the life of the session view, so a 10-prompt drive session logged the leak warning with zero actual resizes.

After: the session route's single existing useTerminalDimensions() subscription is exposed as a reactive terminal size on the session context, and the row components read that instead. The same 10-prompt session registers no extra listeners and logs no warning.

How

packages/tui/src/routes/session/index.tsx:

  • session context gains terminal: { width, height } backed by reactive getters over the route-level dimensions() memo, with a doc comment steering row components away from the hook.
  • AssistantFooter reads ctx.terminal.width; SessionImages reads ctx.terminal.height. Reactivity is preserved — the getters track the same signal the hook wrapped.

Scope

Only the two per-row subscribers. App-chrome useTerminalDimensions() call sites (prompt, tabs, dialogs, toasts) are bounded and untouched. No setMaxListeners suppression — the growth is fixed rather than silenced.

Testing

  • bun run --cwd packages/tui typecheck
  • End-to-end with opencode-drive: a scripted 10-prompt session (5 streams + 5 mid-stream steers) against this branch produced zero resize listeners warnings in TUI stderr; the same script against the v2 baseline warns at 11 listeners. Footer model/duration hiding still responds to width (getters remain reactive).

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.
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.
@kitlangton

Copy link
Copy Markdown
Contributor Author

Follow-ups from review passes on this diff, plus a demo recording.

Applied in 7784767:

  • The context's terminal getters are now backed by per-axis memos, so width readers (the footer breakpoints) no longer re-run on height-only resizes and vice versa. dimensions() is a single object signal with identity equality, so without the memos every resize event re-evaluated every row's conditions on both axes.
  • Documented that the context's bare width is content width (terminal minus tabs/sidebar/padding), since it now sits next to terminal.width and the two are easy to confuse.

Checked and deliberately not changed: no per-row useTerminalDimensions() callers remain in the transcript tree (permission.tsx/form.tsx/composer are singletons, bounded at O(1)); no existing shared dimensions helper exists in packages/tui/src to reuse. Possible follow-up outside this PR: memoize the signal per renderer inside useTerminalDimensions itself (or an app-level context), which would make all ~29 call sites cheap and this context field unnecessary.

Demo: drive run against this branch. Four replies with assistant footers, then a width sweep 100 → 80 → 60 → 44 → 34 → 27 → 24 → 30 → 40 → 70 → 100 crossing the 28/36-column footer breakpoints in both directions, then a prompt submitted at 44 columns. Footer adapts at every width; zero listener warnings in the TUI logs (the pre-fix baseline warned at 11 mounted footers).

recording-320b724a-718c-4bc4-9bb4-e9b50df733d3.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant