Skip to content

fix(terminal): converge resize/fit feedback loop, add WebGL oscillation fallback - #281

Closed
tstapler wants to merge 19 commits into
backup/pause-resume-fix-orig-basefrom
backlog/stapler-squad-terminal-fit-convergence
Closed

fix(terminal): converge resize/fit feedback loop, add WebGL oscillation fallback#281
tstapler wants to merge 19 commits into
backup/pause-resume-fix-orig-basefrom
backlog/stapler-squad-terminal-fit-convergence

Conversation

@tstapler

Copy link
Copy Markdown
Owner

Summary

Fixes an infinite fit()ResizeObserver feedback loop in the terminal UI that pegs CPU and freezes input. XtermTerminal.tsx's ResizeObserver gated fit() on a raw >1px pixel delta instead of whether FitAddon.proposeDimensions() actually proposes different integer cols/rows vs the terminal's live dimensions; useTerminalFlowControl.resize() only throttled by time, not value. A WebGL sub-pixel glyph-metric mismatch could keep both loops spinning indefinitely.

Closes backlog item fc63d55b-d6cf-4e11-af02-c76c86637c5e.

Note on base branch: this PR is opened against backup/pause-resume-fix-orig-base rather than main because this worktree's development lineage has diverged from main on this remote (a PR against main would show ~2,760 unrelated files). This is the closest live branch on origin sharing an exact, clean merge-base with this change (26 files / 4,578 insertions, matching the actual diff).

What Changed

  • New pure module web-app/src/lib/terminal/resizeConvergence.ts: shouldFit, shouldSendResize, shouldAbandonWebgl + TerminalSize/ResizeEvent types.
  • XtermTerminal.tsx: gates fit() on live terminal.cols/rows at both the ResizeObserver debounce path and the imperative fit() handle used by tab-visibility/viewport-change handlers (the latter closes the tab-background/resume repro path, caught during pre-mortem review). Adds an oscillation-burst detector hooked at terminal.onResize that falls back from WebGL to xterm's default DOM renderer after 3 recurrences within 2s, with an async-load race guard (cancelled flag) and a webglFallbackTrippedRef distinguishing "never loaded" from "already fell back."
  • useTerminalFlowControl.ts: resize() gains value-dedup (lastSentSizeRef) independent of the existing 200ms time-throttle, plus a force bypass param for its 2 legitimate non-deduped callers (reconnect resync, manual debug resize).
  • TerminalOutput.tsx / useTerminalStream.ts: thread the force param through both call sites and the intermediate hook signature.
  • docs/adr/018-webgl-oscillation-fallback-to-default-renderer.md: documents the fallback decision and corrects the original ticket's "canvas renderer" wording — @xterm/addon-canvas is deprecated/incompatible with the pinned @xterm/xterm ^6.0.0; disposing WebGL falls back to xterm's default DOM renderer instead, which fully achieves the same functional goal.
  • Full SDD process artifacts (requirements, research, plan, 2 review rounds, validation, pre-mortem, verification report) under project_plans/terminal-resize-fit-loop/.

Test plan

  • cd web-app && npx jest --no-coverage --testPathPatterns="resizeConvergence|useTerminalFlowControl|XtermTerminalResize|TerminalOutputBug" → 65/65 passing across 4 suites, including a XtermTerminalResize.test.tsx component test exercising the real ResizeObserver/imperative-fit() wiring (not just the extracted pure functions), a StrictMode double-mount regression test, and 3 AC4 oscillation/backstop cases.
  • Mutation-tested: temporarily reverted the AC2 gate, the cancelled guard, and the backstop branch logic in turn and confirmed each removal causes the corresponding test to fail.
  • npx tsc --noEmit clean on all touched files; next lint shows no new warnings.
  • sdd:6-verify (idiom + architecture + refactor review): 0 BLOCKER / 0 MUST FIX. Fixed 3 review-consensus items inline, including one genuine latent bug (webglFallbackTrippedRef not reset on cleanup) found only by the idiom reviewer, now regression-tested.
  • Live browser verification: built and ran the real binary in an isolated instance, drove 3 real terminal sessions with headless Playwright. Observed bounded convergence (8 total resize-triggering log lines across 3 tabs in a 10s post-resize window, zero in the final 3s) and 0 console errors through a simulated tab-background/resume cycle — while this environment's software GPU reproduced a genuine live WebGL glyph-metric mismatch (8.59/8.41px, 8.27/8.00px, 8.59/8.41px), demonstrating the fix tolerates the mismatch rather than merely not encountering it. Full timeline in project_plans/terminal-resize-fit-loop/implementation/manual-verification-report.md.
  • Still recommended before merge: a human pass with Chrome DevTools' Performance panel (real CPU%, real OS-level tab switch) on hardware closer to the original bug report — flagged explicitly as the fully authoritative check for this class of bug; the evidence above is strong supporting automation, not a full substitute.

🤖 Generated via the SDD pipeline (requirements → research → plan w/ 2 adversarial review rounds → validate/pre-mortem/triad review → implement → sdd:6-verify) in Claude Code.

tstapler and others added 17 commits July 27, 2026 21:30
…rgence fix

Derives requirements from backlog item fc63d55b-d6cf-4e11-af02-c76c86637c5e's
description and 7 acceptance criteria (unattended run, no ideation interview).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
…, architecture, pitfalls, ux, build-vs-buy)

6 parallel research agents. Key findings: FitAddon.proposeDimensions() is
already the correct integer-cols/rows signal for AC2; resize() has 3 callers
and 2 deliberately need to bypass value-dedup; StateApplicator mutates
terminal.cols/rows independent of fit() so the AC2 baseline must be live
terminal state; AC4's "canvas renderer" is unavailable on xterm.js v6
(addon-canvas deprecated) — disposing WebGL falls back to the default DOM
renderer instead, which the ADR needs to document explicitly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
… plan reviews

Phase 3 planning: 5 phases / 11 epics / 17 stories / 25 tasks covering AC1-7.
Architecture review found one BLOCKER (async WebGL addon load races the
persistent webglAddonRef across StrictMode/scrollback-remount effect
re-runs) and 4 CONCERNS; adversarial review found 3 CONCERNS. All resolved
in-plan: a cancelled-liveness-flag guard closes the blocker, a TerminalSize
object type replaces loose positional (cols,rows) params to remove a
transposition hazard, a webglFallbackTrippedRef distinguishes "WebGL never
loaded" from "already fell back" so the AC4 console.error backstop doesn't
misfire on repeat bursts, plus a terminalRef liveness check before dispose
and expanded ADR consequences (false-positive discriminator, imperative
fit() path scope note). Re-review confirms CLEAN.

ADR-018 documents the AC4 "canvas renderer" -> "default DOM renderer"
terminology correction (@xterm/addon-canvas is deprecated/incompatible with
the pinned @xterm/xterm ^6.0.0).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
…lve 2 P1s

Adds validation.md (7/7 ACs mapped to concrete tests) and pre-mortem.md
(2 P1, 2 P2, 1 P3 failure modes). Resolves both P1s in plan.md/ADR-018:

- P1 #1: the imperative fit() entry point (XtermTerminal.tsx, exposed via
  useImperativeHandle, called by TerminalOutput.tsx's tab-visibility and
  visualViewport.resize handlers) was left ungated in the original plan,
  but AC1/AC7's repro explicitly includes tab background/resume, which
  routes through this path, not the ResizeObserver. Adds Epic 2.4 to gate
  it with the same shouldFit predicate.
- P1 #4: upgrades Epic 5.2's manual verification from a PR-description
  nice-to-have to a hard pre-merge gate, split into independently-verified
  AC1 (window-resize) and AC7 (tab-background/resume) sub-cases, with the
  actual pixels-per-column baseline and shortcut-registry churn observation
  recorded as evidence.

Also resolves the cross-artifact-consistency BLOCKER (requirements.md's
AC4 still said "canvas renderer" after plan.md/ADR-018 corrected it to
xterm's default DOM renderer) by amending requirements.md itself, plus
3 cheap CONCERNs: shouldFallbackToCanvas -> shouldAbandonWebgl rename
noted in requirements.md, backstop console.error/console.log carve-out
clarified, and a missing regression test added (Task 3.1.2c) verifying
TerminalOutput.tsx's two force=true callers actually pass it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
…or Epic 2.4, mark resolved items

Engineering-lens triad review flagged Epic 2.4's imperative fit() gate
(added to close pre-mortem P1 #1) as having zero automated test coverage,
only the manual QA checklist. Adds Story 4.2.4 / Task 4.2.4a covering both
the no-op and genuine-change cases via the same test harness, plus updates
the stale Dependency Visualization diagram (missing Epic 2.4 and Task
3.1.2c) and validation.md's AC1/AC7 test mapping.

Re-checked with a fresh engineering-lens agent: ready, 0 blockers. All
three triad legs (Product, UX, Engineering) are now ready. Marks the
already-resolved items in adversarial-review.md and pre-mortem.md as
checked, since both still showed unchecked boxes despite the fixes already
being present in plan.md/ADR-018.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
…ctions

Adds shouldFit (AC2 fit() gate), shouldSendResize (AC3 RPC dedup), and
shouldAbandonWebgl (AC4 oscillation/burst detector) as pure, independently
unit-tested functions per Phase 1 (Epic 1.1 + 1.2) of the
terminal-resize-fit-loop implementation plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
…n XtermTerminal

Implements Phase 2 (Epics 2.1-2.4) of the terminal-resize-fit-loop plan:
gate the ResizeObserver debounced fit() and the imperative fit() handle
on shouldFit() (AC2/AC1/AC7), and detect resize oscillation at
terminal.onResize via shouldAbandonWebgl(), disposing the WebGL addon
and falling back to xterm's default renderer after 3 repeats within
2000ms (AC4). Adds webglAddonRef/webglFallbackTrippedRef plus a
cancelled liveness guard around the async WebGL addon load to close
the StrictMode/scrollback-remount race identified in architecture
review.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
…nnect/debug callers

Adds lastSentSizeRef + shouldSendResize-gated dedup to
useTerminalFlowControl's resize() (AC3), independent of the existing
200ms time-throttle. A new optional force parameter lets the
reconnect-resync and manual force-resize call sites in
TerminalOutput.tsx bypass the dedup so they keep sending
unconditionally. useTerminalStream's TerminalStreamResult.resize type
is also widened to accept the optional force param, since
TerminalOutput consumes resize through that wrapper hook rather than
useTerminalFlowControl directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
Adds 3 test cases to useTerminalFlowControl.test.ts's resize describe
block per plan.md Epic 4.1 Tasks 4.1.1a-c: same-value dedup after the
200ms throttle elapses, genuinely-new-value is never deduped (AC6
RPC-level no-regression), and force=true bypasses only the value-dedup
while correctly re-establishing lastSentSizeRef for subsequent calls.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
…C5/AC6/AC7

Covers plan.md Phase 4 Epic 4.2 (harness + StrictMode webglAddonRef regression
+ AC5/AC6/AC1/AC7) and Epic 4.3 (AC4 oscillation-burst WebGL fallback, backstop,
and no-repeat-error on a second burst) for XtermTerminal.tsx's resize-convergence
behavior. Mutation-tested against the real component: reverting the AC2 gate,
the cancelled-guard, or the webglFallbackTrippedRef branch each independently
fails the corresponding test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
… to human review

Attempts to build/run the app and drive live browser verification of AC1/AC7
did not complete cleanly, and this worktree's host is running multiple other
live stapler-squad instances doing unrelated work, so a second live-server
attempt was not made to avoid resource/port contention risk.

Documents the strong automated evidence in its place: 64/64 tests passing,
mutation-tested component coverage exercising the real ResizeObserver and
imperative-fit() wiring, and a spec-compliance sweep confirming AC2-AC6 in
the diff. Flags AC7's live CPU/input-responsiveness observation as a known
gap for human review during PR review, per the Product-lens triad review's
own prediction that this step needs a human in an agent-driven pipeline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
…fix fallback-tripped-ref leak across remounts

Three cheap review-consensus fixes from sdd:6-verify's Layer 1/2 passes on the
terminal-resize-fit-loop work:

- Extract attemptFit() in XtermTerminal.tsx so the ResizeObserver debounce path
  and the imperative fit() handle share one shouldFit gate-and-fit implementation
  instead of two drifting copies (only one logged on skip).
- Export OSCILLATION_WINDOW_MS/OSCILLATION_THRESHOLD from resizeConvergence.ts
  as shouldAbandonWebgl's default params, and import them in XtermTerminal.tsx
  instead of re-declaring the same literals locally.
- Reset webglFallbackTrippedRef.current alongside webglAddonRef.current in the
  mount effect's cleanup, so a [scrollback]-triggered remount whose new instance
  never loads WebGL doesn't inherit a stale "already tripped" flag and misreport
  a fresh oscillation burst via the wrong console branch. Added a regression test
  (XtermTerminalResize.test.tsx) verified to fail without the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
3 parallel review layers (React/TS idioms, architecture, refactor
candidates) found 0 BLOCKER / 0 MUST FIX. Applied 3 cheap, review-consensus
fixes inline (dedupe fit-gate logic, dedupe oscillation-window constants,
fix a webglFallbackTrippedRef cleanup leak that would misfire a diagnostic
log after a scrollback-triggered remount -- caught only by the idiom
review, with a new regression test proving it was a real bug). Layer 3
correctness: 65/65 tests passing, no security-sensitive surface, error
handling/observability match the plan. Layer 4 UX skipped (no user-facing
surface). Verdict: PASS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
…1/AC7 convergence

A retried verification pass succeeded where the first attempt stopped
short: built the real binary (go build + make build), ran it under an
isolated STAPLER_SQUAD_INSTANCE to avoid colliding with other live
instances on this shared host, and drove 3 real terminal sessions with
headless Playwright.

Results: AC1 -- 8 total resize-triggering log lines across 3 tabs in a 10s
post-resize window, zero in the final 3 seconds (full timestamped
timeline in the report) -- bounded convergence, not a runaway loop. AC7 --
zero console errors/pageerrors across a simulated tab-background/resume
cycle (with the honest caveat that Playwright can't drive a true OS-level
tab switch). Notably, this environment's software GPU reproduced a live
WebGL glyph-metric mismatch (8.59/8.41px, 8.27/8.00px, 8.59/8.41px across
the 3 tabs) -- the same failure class as the original bug report -- and
convergence still held despite it, directly demonstrating the fix
tolerates the mismatch rather than merely not encountering it. Zero
"Duplicate shortcut id" churn observed.

Still flags a human Chrome DevTools Performance-panel pass (real CPU %,
real OS tab switch, hardware closer to the original report) as the fully
authoritative check for Story 5.2.1 -- this is materially stronger
supporting evidence, not a full substitute.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
…mise

A PR review verdict (FAIL) correctly identified that research/features.md's
claim "PaneSplitRenderer.tsx does not render TerminalOutput/XtermTerminal at
all" was wrong -- it was based on a shallow grep that missed a real 3-hop
import chain (PaneSplitRenderer -> SessionDetail -> SessionDetailView ->
dynamically-imported TerminalOutput -> XtermTerminal). A same-page tiled
layout with multiple concurrently-mounted, independent XtermTerminal
instances genuinely exists, and it is the original ticket's actual repro
topology ("3 terminals open in a split/tiled layout... panes resize in
lockstep"), not "3 separate browser tabs" as this pipeline had reframed it.

Corrects requirements.md's Problem Statement, Success Criteria, and Out of
Scope section; adds an ERRATA note to research/features.md preserving the
original (wrong) analysis for transparency rather than silently rewriting
it; updates plan.md's Domain Glossary and Story 5.2.1's manual verification
checklist to require the real tiled-panes scenario as the primary check
(separate tabs remains a valid secondary check, since that topology is also
real).

Also corrects a second, independently-found CRITICAL bug from the same PR
review cycle: useTerminalFlowControl.resize()'s force parameter only
bypassed the value-dedup check, not the pre-existing 200ms time-throttle,
so a forced call landing within 200ms of a prior send was silently
dropped -- defeating the guarantee both real force=true callers
(reconnect-resync, manual force-resize) need. Plan text updated for Task
3.1.1b/4.1.1c-d; actual code/test fix is being applied in a follow-up
commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
…, not just dedup

The 200ms time-throttle check in resize() ran unconditionally before the
force-aware value-dedup check, so a force=true call landing within 200ms of a
prior send hit the throttle's early return before force was ever consulted —
silently dropping resizes at both real force call sites (post-reconnect
resync, manual force-resize button), exactly when a recent resize is likely.

Add !force to the time-throttle condition and document the guarantee, plus a
regression test proving force=true bypasses the throttle window (not just
the dedup) — verified to fail without the fix and pass with it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
@TylerStaplerAtFanatics
TylerStaplerAtFanatics marked this pull request as ready for review July 28, 2026 07:34
tstapler and others added 2 commits July 28, 2026 00:38
All existing resize-convergence tests instantiated exactly one
XtermTerminal at a time, so none proved the fix holds for the original
bug's actual topology: multiple sibling panes rendered side by side in
PaneSplitRenderer's tiled layout, where a resize can cascade into every
sibling's own ResizeObserver simultaneously ("panes resize in lockstep").

Extends the mock xterm.js/FitAddon/ResizeObserver harness to track
per-instance state (fitCalledCount, onResizeCb, proposedDimensions) via
a new harness.instances[] array, wired through MockTerminal.loadAddon()
similarly to how real xterm.js activates an addon against its terminal.
The existing singleton harness fields are left fully intact so all
pre-existing single-instance tests are unaffected.

Adds a new describe block that mounts 2-3 real XtermTerminal instances
in sibling flex containers and asserts: each instance converges
independently to its own genuine resize, a simultaneous sub-pixel
wobble across all siblings causes no instance to churn, one sibling's
resize does not perturb unaffected siblings, repeated identical
resizes across all siblings don't grow fitCalledCount unboundedly, and
two siblings resizing to identical pixel dimensions in the same tick
both still converge independently.

Mutation-tested by temporarily hoisting XtermTerminal.tsx's effect-
scoped `lastContainerSize` to module scope (simulating a cross-instance
state leak) and confirming the new "identical simultaneous dimensions"
test fails as expected, then reverted with no diff remaining in
XtermTerminal.tsx.
… attempt

A dispatched agent to redo the live verification pass using the real
split-pane UI (correcting the prior pass's wrong 3-separate-tabs topology)
left a dangling forward-reference to a "Redone verification" section that
was never actually appended before the session moved on -- its server
process was observed alive and consuming CPU across multiple checks,
suggesting genuine work exploring how to trigger a pane split via the UI,
but it did not land a commit.

Rather than leave the dangling reference or fabricate a result, this
records what's actually true: the live human-observable verification of
the tiled-panes topology (Chrome DevTools Performance panel, real pane
splits, real CPU%) remains an open gap, explicitly flagged as the primary
remaining item for human follow-up. In its place, a new mutation-tested
Jest test (XtermTerminalResize.test.tsx's multi-instance describe block,
added in 063fd93) proves sibling XtermTerminal instances converge
independently at the component level, including the discriminating case
of simultaneous identical-dimension resizes -- verified to actually catch
a simulated cross-instance state leak, not just pass vacuously.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASBehcKnb5xh5SqRja7XEN
@tstapler

Copy link
Copy Markdown
Owner Author

Update: addressed FAIL-verdict findings

The backlog system's automated review found 2 real issues after the initial commits, both now fixed and pushed:

1. False premise (AC1/AC6): research/features.md had incorrectly concluded no same-page tiled layout exists in this codebase. It does — PaneSplitRenderer.tsxSessionDetailSessionDetailView → dynamically-imported TerminalOutputXtermTerminal, tiled as sibling flex panes. This is the actual original repro topology ("3 terminals open in a split/tiled layout... panes resize in lockstep"), not "3 separate browser tabs" as this pipeline had reframed it. Corrected requirements.md/research/features.md/plan.md with a visible ERRATA (not silently rewritten), and added a mutation-tested 5-case Jest test (XtermTerminalResize.test.tsx) proving sibling XtermTerminal instances converge independently in a tiled layout, including the discriminating case of simultaneous identical-dimension resizes.

Honest open gap: a live human/browser verification pass using the real split-pane UI (Chrome DevTools CPU trace, actual pane splits) was attempted twice by dispatched agents but did not land verifiable, substantiated results in this pipeline run. This remains the single most important follow-up before considering the fix fully verified end-to-end — see project_plans/terminal-resize-fit-loop/implementation/manual-verification-report.md's "CORRECTION" and "Updated Verdict" sections for the full honest account.

2. CRITICAL bug (found during this PR's own Gate 2 code review, not by the backlog reviewer): useTerminalFlowControl.resize()'s force=true only bypassed the value-dedup check, not the pre-existing 200ms time-throttle — so a forced resize landing within 200ms of a prior send was silently dropped, defeating the guarantee both real force callers need (reconnect-resync, manual force-resize). Fixed so force bypasses both gates; mutation-tested (regression test fails without the fix, passes with it).

Current state: 71/71 tests passing, tsc/lint clean, 0 BLOCKER/CRITICAL across 4 parallel code-review dimensions (testing, code quality, architecture, security — architecture's 1 CRITICAL finding above is now fixed). PR is MERGEABLE/CLEAN. No CI runs on this PR since all workflows in .github/workflows/ are scoped to branches: [main] and this PR targets backup/pause-resume-fix-orig-base (see PR description for why).

Recommend a human complete the live tiled-pane browser verification before merge.

@tstapler

Copy link
Copy Markdown
Owner Author

Update: live tiled-pane verification completed successfully

Correcting my own prior comment — the live human/browser verification I flagged as incomplete has now landed. A verification pass drove the app's real split-pane UI (each session row's "..." → "Open in new pane" action, which dispatches SPLIT_AND_ASSIGN_SESSION) to tile 3 real terminal sessions as sibling XtermTerminal panes on one page — confirmed via .xterm count = 3 and screenshots. This is the literal repro topology from the original ticket, not an approximation.

Key findings:

  • Directly observed the "panes resize in lockstep" behavior the ticket describes — during pane tiling, two different panes logged Container resized at the identical millisecond, with paired Sending resize to server/Skipping fit() lines following. This same-timestamp cross-pane correlation is structurally only observable in this topology (not the separate-tabs approximation used in the first pass).
  • Confirmed convergence despite it: after an explicit window resize, only 3 resize-triggering log lines fired (landing within 325ms), then complete silence for the remaining ~9.7s of the 10s observation window.
  • New signal, only visible in this topology: 27 occurrences of Duplicate shortcut id churn, architecturally explained (sibling panes share one global ShortcutRegistry singleton, unlike isolated per-tab JS realms) — 0 console errors accompanied it, consistent with requirements.md's existing carve-out for this line.
  • A genuine WebGL glyph-metric mismatch was reproduced live again (8.18–8.56px actual vs 8.00–8.40px expected), and convergence held despite it.

Full details in project_plans/terminal-resize-fit-loop/implementation/manual-verification-report.md's "Redone verification: same-page tiled panes" and "Overall Verdict" sections (both the tiled-panes primary topology and the separate-tabs secondary topology now show PASS).

Remaining gap, unchanged: no CPU%/DevTools-Performance-panel measurement was taken (headless environment, no DevTools UI to script against) — a human spot-check with that tooling on hardware closer to the original report remains the fully authoritative check, but is no longer blocking given the strength of the automated evidence now assembled (live tiled-pane pass + live tabs pass + mutation-tested component test, three independent lines of evidence all showing convergence).

@tstapler

Copy link
Copy Markdown
Owner Author

Closing — this bug is already fixed on main by #272 ("fix(web): dead-band terminal resize loop, RPC dedup, and WebGL fallback"), merged independently while this branch was in progress (its base branch turned out to be a stale snapshot of main from before that merge, which is why this PR's diff doesn't include it).

#272 covers the same ground this PR does — dead-band ResizeObserver, RPC value-dedup with force bypass + regression tests, and a WebGL mismatch tracker with renderer fallback — via an independently-derived approach (a two-consecutive-sample dead-band, its own ADR-002).

No further action needed here; the backlog item this branch addresses is resolved by #272.

@tstapler tstapler closed this Jul 29, 2026
@tstapler
tstapler deleted the backlog/stapler-squad-terminal-fit-convergence branch July 29, 2026 01:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant