Without flex-shrink:0, flex compresses all sidebar cards proportionally
when the container is shorter than their total height. Cards visually
fit the screen with no overflow — scrollTop stays 0, touch handler
has nothing to scroll. One property restores the intended overflow.
position:fixed + overflow:hidden on .session-sidebar in mobile overlay
mode prevents Android Chrome from routing native touch-scroll to the
child .sidebar-list (overflow-y:auto). Direct scrollTop manipulation
always works. Mirrors the terminal WheelEvent approach: take JS control
rather than relying on native CSS touch-scroll routing.
scrollLines() only moves xterm.js's local scrollback buffer (outside).
WheelEvent dispatch goes through xterm.js's mouse reporting path —
if tmux mouse mode is on, escape sequences reach the PTY so tmux and
applications (vim, less, etc.) handle the scroll (inside).
Same behavior as mouse wheel on desktop.
Terminal: add touchstart/touchmove/touchend IIFE on #terminal-container.
Vertical swipe delta translated to _term.scrollLines(n). touchmove is
non-passive (required for e.preventDefault to block page scroll while
swiping inside terminal). mouse wheel scroll unaffected (separate wheel
event path in xterm.js).
Sidebar: add touch-action:pan-y + overscroll-behavior:contain to
.sidebar-list. Tells browser explicitly that vertical panning is allowed
and prevents scroll chaining when reaching list ends.
- Mobile idle tiles: display:none → height:36px so session content is
visible (all sessions are 'idle' with zero bells — was blank on iPhone)
- .tile-pre dead CSS: merge font/color rules into .tile-body pre (the
actual HTML selector) so snapshot text renders with monospace + muted
color; update mobile active-tier selector to match
- systemd PATH: strip /mnt/ entries from PATH before writing service
unit file — WSL paths with spaces caused systemd to reject the line
terminal.js sends the ttyd auth token as a TEXT WebSocket frame. The proxy's
client_to_ttyd() called receive_bytes() which fails silently on text frames,
swallows the exception, and exits — auth never reached ttyd, ttyd never
started streaming, resulting in permanent black screen and reconnect loop.
Fix: use receive() and dispatch on message type, handling both bytes and text.
Required fixes:
- main.py: update module docstring, startup comment, and FastAPI title from
'tmux-web coordinator' / 'coordinator service' → 'muxplex' (4 locations)
- test_api.py: replace hasattr/BaseRoute pattern with isinstance(r, (APIRoute,
APIWebSocketRoute)) to satisfy pyright (union-attr error eliminated)
Recommendations applied:
- test_api.py + test_cli.py: auto-formatted with ruff (blank-line normalisation)
- test_cli.py: replace exec(Path('muxplex/__main__.py').read_text()) with
importlib.util.find_spec() to obtain the absolute path — no longer assumes
pytest is invoked from a specific working directory
- README.md: fix architecture table — WebSocket proxy moved from ttyd.py
description to main.py; ttyd.py now correctly described as lifecycle only
All 178 tests pass; python_check clean (0 errors, 0 warnings).
Addresses code quality suggestion from review: requirements.txt was a
redundant secondary source of truth. Dependencies are now exclusively
managed in pyproject.toml with proper runtime/dev separation.
- requirements.txt: replaced with a comment redirecting to pip install -e '[dev]'
- README.md: updated install command from pip install -r requirements.txt
to pip install -e '[dev]'
Before: openTerminal() never closed the previous WebSocket. After A→B→A,
two WS connections to session A both wrote to _term → double keystroke echoes.
Rapid switches triggered a CONNECTING-state send error and reconnect loop.
After: openTerminal() nulls _currentSession, clears reconnect timer, and
closes _ws before opening a new connection. connect() event handlers now
capture the WS instance in a local const and check 'ws !== _ws' on entry
so stale connections silently ignore all events.
Tests: three new regression tests verify:
- openTerminal() calls close() on the previous WS (Bug 1)
- stale open handler does not send on new WS after switch (Bug 2)
- stale close handler does not schedule reconnect after switch (Bug 2)
Three-part consistency defect identified in code review:
1. HTML: Four sidebar elements missing CSS class attributes so all CSS
rules targeting .session-sidebar, .sidebar-toggle-btn,
.sidebar-collapse-btn, and .sidebar-list applied to zero elements.
Added class= attributes to all four.
2. JS: initSidebar(), toggleSidebar(), and bindSidebarClickAway() called
$('sidebar') and $('collapse-btn') — neither ID exists in the DOM.
Corrected to $('session-sidebar') and $('sidebar-collapse-btn').
3. Tests: initSidebar/toggleSidebar mocks matched the wrong IDs, masking
the defect. Updated all six mock conditions to match corrected IDs.
4. Coverage gap: test_html_element_classes did not verify new sidebar
elements carry CSS classes. Added four entries for session-sidebar,
sidebar-toggle-btn, sidebar-collapse-btn, and sidebar-list — these
entries would have caught Defect 1 on Task 1's test run.
All 136 JS + 167 Python tests pass. Zero regressions.
Added renderSidebar(sessions, _viewingSession) call in pollSessions function after renderGrid(sessions) and before handleBellTransitions(prev, sessions), so sidebar updates on every poll tick alongside dashboard grid. Added test verifying sidebar-list innerHTML is set during pollSessions in fullscreen mode.
🤖 Generated with Amplifier
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
- .tile-bell: inline-flex badge (min-width:16px height:16px) so it's a
circle for single digits, pill for '9+'; was display:inline so
width/height were ignored → oval shape
- .tile-bell color: #0D1117 (dark on amber) — was inheriting muted gray
from .tile-meta, making count illegible
- formatTimestamp(null): return '' instead of em-dash '—' which rendered
as a confusing glyph in the tile meta area
Previously the <pre> snapshot floated to the top of .tile-body — when
content overflowed, the oldest lines showed and newest were clipped.
Now .tile-body pre is position:absolute;bottom:0 so overflow clips
from the top and the lower-left corner (newest content) is always shown.