You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(tui): cap session tab pulse frame rate - #39681
Cap animated session tabs at one shared 60 Hz clock instead of marking every TabPulseRenderable as OpenTUI live.
The visual animation is unchanged, but a busy tab no longer drives the renderer thousands of times per second.
Before / After
Before: the first busy tab set live: true. Although OpenCode creates one global renderer with targetFps: 60 and OpenTUI reports maxFps: 60, a pulse-only benchmark measured 1,000–3,500 renderer-reported FPS (typically 1,600–1,900). The request stayed busy for 20 seconds without emitting text or tool deltas, so this isolates the animation loop from session traffic.
After: active pulses join one module-global 60 Hz clock. The clock advances every active pulse, requests rendering through the existing global renderer, and stops when the last pulse finishes. The same benchmark measures 51–55 frames/sec with OpenTUI reporting a stable 55–56 FPS.
How
packages/tui/src/component/tab-pulse.tsx:
Keep TabPulseRenderable.live disabled, avoiding the uncapped OpenTUI live loop.
Maintain one Set of active pulse renderables and one 60 Hz timer for the process.
Advance all active envelopes from the same measured delta.
Remove stopped/destroyed pulses and clear the timer when none remain.
sequenceDiagram
participant S as Session status
participant P as Active pulses
participant C as Shared 60 Hz clock
participant R as Global renderer
S->>P: tab becomes busy
P->>C: join clock
loop while any pulse is active
C->>P: advance(delta)
P->>R: requestRender()
end
S->>P: pulse/fade completes
P->>C: leave clock
Loading
Scope
This is a localized workaround for the OpenTUI live scheduling path. It does not change the global renderer configuration.
No animation timing, easing, color, glow, or completion behavior changes.
CPU improves only modestly because the excess frames were cheap: pulse-only steady-state samples moved from roughly 20–32% to 18–22%. The important invariant is that renderer work is now bounded.
Testing
bun typecheck in packages/tui
bun run test in packages/tui: 566 pass, 5 skip, 0 fail
bun run test test/component/tab-pulse.test.ts: 5 pass, 0 fail
Repo-pinned Prettier and oxlint clean on the changed file
Isolated opencode-drive pulse benchmark, 160×45 viewport, busy background session with zero streamed deltas:
before: typically 1,600–1,900 FPS, renderer-reported peaks above 3,000
Holding this as a draft while tracing the root scheduler interaction. A generic OpenTUI live renderable honors the global 60 FPS cap; the runaway requires the full combination of a live tab pulse and high-frequency streaming updates. If the core scheduler can coalesce that path correctly, this localized clock workaround will be replaced or the PR closed.
Closing in favor of the architectural fix in OpenTUI core. The root cause is a leaked scheduled frame timer: CliRenderer.loop() nulled renderTimeout before trying to clear it, so an intermediate frame during a live loop orphaned the pending timer and streaming multiplied concurrent timer chains. The core fix cancels the pending timer before clearing its handle; the full OpenCode repro drops from ~1,625 FPS to ~52 FPS without a TabPulse-specific clock.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Cap animated session tabs at one shared 60 Hz clock instead of marking every
TabPulseRenderableas OpenTUIlive.The visual animation is unchanged, but a busy tab no longer drives the renderer thousands of times per second.
Before / After
Before: the first busy tab set
live: true. Although OpenCode creates one global renderer withtargetFps: 60and OpenTUI reportsmaxFps: 60, a pulse-only benchmark measured 1,000–3,500 renderer-reported FPS (typically 1,600–1,900). The request stayed busy for 20 seconds without emitting text or tool deltas, so this isolates the animation loop from session traffic.After: active pulses join one module-global 60 Hz clock. The clock advances every active pulse, requests rendering through the existing global renderer, and stops when the last pulse finishes. The same benchmark measures 51–55 frames/sec with OpenTUI reporting a stable 55–56 FPS.
How
packages/tui/src/component/tab-pulse.tsx:TabPulseRenderable.livedisabled, avoiding the uncapped OpenTUI live loop.Setof active pulse renderables and one 60 Hz timer for the process.sequenceDiagram participant S as Session status participant P as Active pulses participant C as Shared 60 Hz clock participant R as Global renderer S->>P: tab becomes busy P->>C: join clock loop while any pulse is active C->>P: advance(delta) P->>R: requestRender() end S->>P: pulse/fade completes P->>C: leave clockScope
livescheduling path. It does not change the global renderer configuration.Testing
bun typecheckinpackages/tuibun run testinpackages/tui: 566 pass, 5 skip, 0 failbun run test test/component/tab-pulse.test.ts: 5 pass, 0 fail