- Expand Prerequisites section with bullet format (was a sparse table)
- Add tmux and ttyd per-platform install commands (macOS, Ubuntu/WSL)
- Note that muxplex prints install instructions if either dependency is missing
- Update 'Install as a Service' section with macOS launchd instructions
- Update Usage table to reflect platform-aware install-service behavior
- Add _check_dependencies() that checks shutil.which() for tmux and ttyd
- Print clear error with per-platform install hints when either is missing
- Call _check_dependencies() in main() before serve — not for install-service,
show-password, or reset-secret (those don't need the binary dependencies)
- Tests: exits on missing ttyd, exits on missing tmux, passes when both present,
called for serve but not for install-service
- Add _install_launchd() that writes ~/Library/LaunchAgents/com.muxplex.plist
- Refactor existing systemd code into _install_systemd()
- install_service() now detects sys.platform == 'darwin' and delegates accordingly
- Update install-service help text to mention both platforms
- Tests: launchd plist on macOS, systemd on Linux, no cross-contamination
Hidden sessions were still appearing in the mobile bottom-sheet session
switcher because renderSheetList() read _currentSessions directly,
bypassing the getVisibleSessions() helper used by renderGrid() and
renderSidebar().
- app.js: sortByPriority now receives getVisibleSessions(_currentSessions)
instead of _currentSessions directly in renderSheetList()
- test_frontend_js.py: add test_render_sheet_list_filters_hidden_sessions
to assert getVisibleSessions is called in renderSheetList body
Completes consistent hidden-sessions filtering across all three render
paths (grid, sidebar, mobile bottom sheet).
Co-authored-by: Amplifier <amplifier@amplified.dev>
- Extract getVisibleSessions() helper to consolidate hidden-session
filter used by renderGrid() and renderSidebar() (DRY reduction)
- Add comment to terminal.js magic literal 600 noting it matches
MOBILE_THRESHOLD in app.js
- Move TERMINAL_JS_PATH and _TERMINAL_JS module fixtures to the top
of the test file alongside JS_PATH and _JS for consistency
- Strengthen test_render_grid_uses_visible_for_empty_state_check to
assert visible.length specifically, distinguishing it from
test_render_grid_creates_visible_array
- Update renderGrid/renderSidebar filter tests to assert getVisibleSessions()
call; add test_get_visible_sessions_filters_hidden_sessions to cover
the extracted helper directly
- terminal.js createTerminal(): read fontSize from localStorage 'muxplex.display',
default 14, apply Math.min(storedFontSize, 12) mobile cap
- app.js renderGrid(): filter hidden sessions via _serverSettings.hidden_sessions,
create visible array, apply alphabetical sort via localeCompare when sort_order is
'alphabetical'; 'recent'/'manual' use server-provided order
- app.js renderSidebar(): apply same hidden sessions filter, use visible array
- app.js DOMContentLoaded: call loadServerSettings() after startPolling() in restoreState().then()
- tests: 13 new tests in test_frontend_js.py covering all spec requirements
Co-authored-by: Amplifier <amplifier@example.com>
Three suggestions from the code review applied:
1. Extract _createSessionInput() factory to eliminate five-line
duplication between showNewSessionInput and showFabSessionInput.
Both functions now call the shared factory. Updated 4 existing
tests to check _createSessionInput's body; added 7 new tests
covering factory behavior and factory usage in each consumer.
2. Fix .new-session-fab:focus-visible outline color from var(--accent)
to var(--bg) — the FAB background is already var(--accent), so
using the same color for the outline created zero contrast.
Added 1 new test to guard this going forward.
3. Add early-return guard in showFabSessionInput against duplicate
overlays: if (document.querySelector('.fab-input-overlay')) return;
Prevents a second overlay being created if called programmatically
while one is already open. Added 1 new test.
532 tests pass (9 new tests added, 523 unchanged).
- Add div.sidebar-footer with button#sidebar-new-session-btn.sidebar-new-btn
('+ New') to #session-sidebar in index.html, positioned after #sidebar-list
- Add CSS for .sidebar-footer (padding: 8px, border-top, flex-shrink: 0)
- Add CSS for .sidebar-new-btn (width: 100%, dashed border, dim text, hover
effects with muted text and border color)
- Bind #sidebar-new-session-btn click to showNewSessionInput() in
bindStaticEventListeners()
- Add 15 tests covering HTML structure, CSS rules, and JS binding
🤖 Co-authored-by: Amplifier <amplifier@example.com>
- Extract shared _updateNotificationUI(statusEl, reqBtn, permission) helper
to remove duplicated notification status UI update logic from openSettings
and the requestPermission click handler
- Replace var with const for local bindings in new Notifications handlers
- Use this.checked in bell sound change handler instead of re-querying
the DOM element that was already looked up for addEventListener
switchSettingsTab() in app.js reads panel.dataset.tab (data-tab attribute).
The four .settings-panel elements in index.html were using data-panel instead,
causing all panels to get hidden on every tab click — the settings dialog rendered
as a blank content area after any tab interaction.
Fix: Change data-panel="..." to data-tab="..." on all four panel divs.
Tests added:
- test_html_settings_panels_use_data_tab: verifies each .settings-panel uses
data-tab and not data-panel
- test_html_settings_tab_panel_data_tab_alignment: cross-checks that every tab
button data-tab value has a matching .settings-panel data-tab value, preventing
this class of HTML/JS attribute mismatch from recurring
- Replace DEFAULT_SETTINGS.copy() with copy.deepcopy(DEFAULT_SETTINGS) in
load_settings() and save_settings() to prevent mutation of module-level
mutable defaults (hidden_sessions list was shared via shallow copy)
- Replace except (json.JSONDecodeError, Exception) with
except (FileNotFoundError, json.JSONDecodeError) to only suppress expected
errors and let unexpected exceptions (PermissionError etc.) propagate
- Add two regression tests:
test_load_does_not_mutate_default_settings: verifies deepcopy isolation
test_load_propagates_non_json_errors: verifies narrow exception handling
Remove tile-body::before and sidebar-item-body::before gradient overlays
that obscured ANSI colors at the bottom of previews. Replace box-shadow
inset with border-left on sidebar-item--active so the cyan accent stripe
spans the full item height (was only showing on the header).
ANSI_COLORS: replaced with xterm.js default GTK/Tango palette.
Background: #000000 on tile-body, sidebar-item-body, preview-popover.
Font: 'SF Mono', 'Fira Code', Consolas, monospace (same as terminal).
Line-height: 1.0 across all preview surfaces (xterm.js default).
Gradient overlays: fade to #000 instead of theme bg.
Result: previews are visually indistinguishable from the terminal.
ANSI colors make the overlay content readable without dimming. Removed
preview-dimmer, liftHoveredTile(), tile--previewing/item--previewing —
all dead code now. Added var(--accent) 2px border to .preview-popover
for visual separation.
Server: add -e flag to tmux capture-pane to preserve escape sequences.
Frontend: ansiToHtml() parser converts SGR codes (bold, italic, dim,
16-color, 256-color) to <span> tags with inline styles. Applied to
dashboard tiles, sidebar items, and hover overlay. No external deps.
Replace side-positioned popover with a viewport-covering overlay (5vh/5vw
margins). pre-wrap + word-break wraps terminal text to fit any viewport.
pointer-events:none on overlay lets mouseleave fire on the original tile.
Click anywhere while preview is active navigates to that session.
Removes repositionPreview() — no JS positioning needed.
Previous approach guarded renderGrid/renderSidebar from running while
preview active, but catch-up renders, stale DOM references, and timer
races caused flicker, wrong positioning, and stale content.
New approach: track _previewSessionName (string, not DOM node). Renders
run normally. After each innerHTML rebuild, liftHoveredTile() re-applies
z-index to the new DOM element. repositionPreview() re-queries DOM each
call. Timer always cleared when entering a new tile. No render guards.
Poll cycle rebuilds innerHTML every ~2s, destroying the hovered tile's
DOM element and tile--previewing class. Fix: renderGrid() and
renderSidebar() early-return when _previewPopover is non-null. On
hidePreview(), a catch-up render fires so no data is stale.
1.5s hover delay (up from 350ms) for deliberate preview. When popover
appears, a semi-transparent dimmer covers the page (z-index 499). The
hovered tile/sidebar-item gets z-index 500 (same as popover) so it
appears above the dimmer alongside the preview. Both cleaned up on
mouseleave via hidePreview().
- Auto-scroll to bottom (prompt area is the valuable part)
- Dynamic width based on available space (fixes 3-column layout)
- 350ms delay (prevents brush-over flicker)
- Extend hover preview to sidebar items (popover to the right)
- Remove fixed min/max-width from CSS, calculate in JS
Desktop only (guarded by ontouchstart check). On mouseenter of a
session tile, shows a floating popover with the full tmux snapshot
(all lines, not the cropped 20-line preview). Positioned right of
tile if space, else left. 100ms delay prevents flicker on brush-over.
Data from _currentSessions (already in memory, zero API calls).
Also calls hidePreview() in openSession() so the popover is cleaned
up when switching to fullscreen view.
_openTerminal(name) was inside a 260ms setTimeout that fired before the
/connect POST completed. The WebSocket connected to the OLD ttyd (still
serving the previous session). Fix: animation runs concurrently with
/connect as before, but _openTerminal only fires after BOTH resolve.
Sequence: start animation + POST concurrently → await POST → await
animation → mount terminal. The new ttyd is guaranteed to be serving
the correct session before the WebSocket connects.
Added timerId null-check so animDone resolves immediately in test envs
where setTimeout is stubbed, preventing test hangs.