Skip to content

Commit a120ae6

Browse files
docs: document split-pane resize bug and layout redesign solution
Three-layer CSS/flex failure + SIGWINCH architectural incompatibility. Includes root cause analysis, prevention strategies, test infrastructure patterns, and cross-references to related docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9ca4291 commit a120ae6

1 file changed

Lines changed: 156 additions & 0 deletions

File tree

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
---
2+
title: "Web UI Split-Pane Resize Bug — Multi-Layer Layout Failure"
3+
date: 2026-04-08
4+
category: ui-bugs
5+
tags:
6+
- css
7+
- layout
8+
- split-pane
9+
- flexbox
10+
- fastapi
11+
- alpine-js
12+
- htmx
13+
- sse
14+
- split-js
15+
- tmux
16+
- sigwinch
17+
- playwright
18+
- test-isolation
19+
severity: high
20+
component: studyctl web UI (FastAPI + Alpine.js PWA)
21+
symptoms:
22+
- Dragging the split-pane divider resized the terminal panel but not the dashboard panel
23+
- Visual gaps appeared between dashboard content and the drag gutter after resize
24+
- Tests passed in headless and headed Playwright but the bug was visible to the user
25+
- Dashboard content height remained at intrinsic content height regardless of panel size
26+
root_cause: >
27+
Three compounding layout failures plus an architectural incompatibility.
28+
(1) calc(100vh - 60px) hardcoded header height at 60px when actual was ~63.5px.
29+
(2) Inner session-dashboard content did not flex-fill the Split.js panel.
30+
(3) Activity feed was wrapped in a classless <div hx-ext="sse"> — CSS targeted
31+
the nested .activity-feed instead of the wrapper flex child.
32+
(4) Split-pane resize sent SIGWINCH to tmux, corrupting the Claude Code agent session.
33+
Resolved by replacing split-pane with sidebar activity feed + full terminal + status bar.
34+
---
35+
36+
# Split-Pane Resize Bug — Multi-Layer CSS/Flex Failure
37+
38+
## Problem
39+
40+
The web UI study session view used a Split.js split-pane layout with dashboard (top) and terminal (bottom) panels. Dragging the divider resized the terminal but the dashboard panel appeared not to resize — a visible gap appeared between the dashboard content and the divider.
41+
42+
Tests passed in all environments (headless Chromium, headed Chromium) because they measured `offsetHeight` of the panel element, which Split.js updated correctly. The bug was in the visual rendering of content *inside* the panel.
43+
44+
## Investigation Steps
45+
46+
1. Built data-driven test infrastructure with `_measure_all()` JS function capturing all DOM metrics via Playwright. Tests measured `offsetHeight`, `getBoundingClientRect()`, and computed styles for every element in the split layout.
47+
48+
2. Discovered tests produced **false positives** without real ttyd — the terminal iframe was absent, and the layout engine computed simpler dimensions that satisfied assertions. Switched all layout tests (R01-R12) to use real tmux + ttyd fixtures.
49+
50+
3. Added `dashboard_fill_ratio` measurement (content height / panel height). Even with this, the ratio was 1.0 because `session-dashboard` was correctly filling the panel — the gap was *inside* the session-dashboard, between the last content element and the bottom of the flex container.
51+
52+
4. Added diagnostic background colours (red=panel, blue=session-dashboard, green=activity-feed) and measured every child element. Found the activity feed was stuck at exactly 200px (`min-height`) despite having `flex: 1` applied.
53+
54+
5. Discovered the third child of session-dashboard had `class: ""` — it was the `<div hx-ext="sse">` wrapper, not `.activity-feed`. CSS `flex: 1` was applied to `.activity-feed` (nested inside the wrapper), not the wrapper itself.
55+
56+
6. After fixing all CSS issues, discovered that drag-resizing the terminal iframe sent SIGWINCH to the tmux session, corrupting the Claude Code agent running in the adjacent pane.
57+
58+
## Root Cause
59+
60+
### RC-1: Hardcoded Header Height Mismatch
61+
62+
```css
63+
/* Broke layout when header rendered at 63.5px, not 60px */
64+
.split-container {
65+
height: calc(100vh - 60px);
66+
}
67+
```
68+
69+
Fix: `position: absolute; inset: 0` inside a `position: relative` content-area — fills exactly regardless of header height.
70+
71+
### RC-2: Inner Content Not Stretching
72+
73+
Split.js correctly set panel `offsetHeight`, but `.session-dashboard` was a flex column that sized to content rather than filling its parent. Fix: `flex: 1; min-height: 0` on the session-dashboard.
74+
75+
### RC-3: Classless SSE Wrapper as Unintended Flex Child
76+
77+
```html
78+
<div hx-ext="sse"> <!-- no class — actual flex child -->
79+
<div class="activity-feed"> <!-- CSS targeted this, not the wrapper -->
80+
```
81+
82+
Fix: target the wrapper directly:
83+
84+
```css
85+
.split-dashboard .session-dashboard > [hx-ext="sse"] {
86+
flex: 1 1 0px;
87+
min-height: 0;
88+
display: flex;
89+
flex-direction: column;
90+
}
91+
```
92+
93+
### RC-4: SIGWINCH Corruption (Architectural)
94+
95+
Resizing the terminal iframe triggered SIGWINCH in the tmux session, corrupting the Claude Code agent. This is a fundamental incompatibility between dynamically resized iframes and interactive agent sessions.
96+
97+
## Solution
98+
99+
Replaced the split-pane architecture entirely:
100+
101+
```
102+
┌─────────────────────────────────────┐
103+
│ Header / Nav │
104+
├──────────────┬──────────────────────┤
105+
│ Sidebar │ Full-screen │
106+
│ Nav buttons │ Terminal (ttyd) │
107+
│ Activity │ │
108+
│ Feed (SSE) │ │
109+
├──────────────┴──────────────────────┤
110+
│ Status Bar: timer ⏸↺■ topic ⚡7 ✓○▲│
111+
└─────────────────────────────────────┘
112+
```
113+
114+
- **Sidebar**: fixed width, nav buttons + scrollable activity feed (SSE-driven)
115+
- **Content area**: 100% terminal iframe — no resize, no SIGWINCH after initial load
116+
- **Bottom status bar**: timer + pause/reset/stop + topic + energy + counters
117+
118+
Split.js and `splitLayout()` Alpine component remain for the body-double view (where the timer iframe doesn't interact with tmux).
119+
120+
### Test Infrastructure Built
121+
122+
- **STUDYCTL_SESSION_DIR** env var for test isolation — tests never touch `~/.config/studyctl/`
123+
- **18 Playwright E2E tests** with real tmux + ttyd
124+
- **`_measure_all()`** JS function captures all DOM metrics in one call
125+
- **`results.json`** written after every run for agent loop consumption
126+
- **16 pure verifier functions** (`verify_R01` through `verify_R16`)
127+
128+
## Key Learnings
129+
130+
1. **Test what users see, not what the DOM reports.** `offsetHeight` was correct throughout — the bug was that content didn't fill the allocated space. The right metric was fill ratio, not panel height.
131+
132+
2. **`min-height: 0` is mandatory for scrollable flex children.** Without it, the browser's minimum content size constraint overrides `flex: 1`.
133+
134+
3. **Avoid `calc(100vh - Npx)` magic numbers.** `position: absolute; inset: 0` inside a positioned parent is robust and measurement-free.
135+
136+
4. **Target the actual flex child, not its descendant.** HTMX/Alpine inject wrapper elements that become unintended flex children. Always verify the DOM hierarchy in DevTools before writing flex CSS.
137+
138+
5. **SIGWINCH and agent sessions are incompatible.** Any UI widget that resizes a terminal iframe sends SIGWINCH to every process in the underlying tmux session. Keep terminal iframes at a stable size after initial load.
139+
140+
6. **False positives emerge without real dependencies.** Layout tests without a live ttyd produced false positives because the iframe was absent. Always test against the full dependency stack.
141+
142+
## Prevention Strategies
143+
144+
- **Real dependencies in tests**: Use real tmux + ttyd in layout tests. If a dependency can't start in CI, skip rather than fake.
145+
- **Name every flex child**: If a component is rendered inside a flex container, its outermost element must have an explicit class.
146+
- **SIGWINCH complexity threshold**: If adding a feature requires cross-process signal propagation, evaluate whether the feature justifies the coupling.
147+
- **Machine-readable results**: Emit `results.json` for any test measuring quantitative layout values.
148+
- **Isolation fixtures**: Every test touching session state must use `STUDYCTL_SESSION_DIR` via a session-scoped fixture.
149+
150+
## Related Documentation
151+
152+
- [Four-bug-fix solutions](../integration-issues/web-ui-four-bug-fix-pomodoro-auth-ttyd-cleanup.md) — predecessor session fixes (pomodoro, auth, ttyd cleanup)
153+
- [Data-driven test design spec](../../superpowers/specs/2026-04-07-data-driven-split-pane-tests-design.md) — autoagent-inspired test infrastructure design
154+
- [C4 Architecture](../../architecture-c4.md) — updated to reflect sidebar+terminal+status bar layout
155+
- [tmux session management](../../internal/solutions/tmux-session-management-and-ci-issues.md) — SIGWINCH context and tmux test isolation patterns
156+
- [CI workflow failures](../test-failures/ci-workflow-failures-schema-init-pytest-markers.md) — false positive patterns and marker discipline

0 commit comments

Comments
 (0)