- Extract inline ternary for device badge into `const badgeHtml` variable
above the return block, consistent with how `bellHtml` is handled
- Tighten data-session-key/data-source-url test assertions to check exact
attribute values rather than just attribute presence
- Add XSS test: verifies deviceName containing HTML is escaped in the badge
All 159 tests pass.
- Show <span class="device-badge"> after tile name when _sources.length > 1
and session.deviceName is present (multi-source federation context)
- Add data-session-key and data-source-url attributes to tile article
element for unique session identification across sources
- Tests: 3 new tests covering badge visibility, badge omission for single
source, and presence of new data attributes
- Replace getVisibleSessions filter logic: only hide sessions where
sourceUrl is empty/absent AND name matches hidden_sessions list
- Remote sessions with the same name as a hidden local session now
remain visible (task-7 spec requirement)
- Export getVisibleSessions in module.exports so tests can access it
Tests added:
- 'getVisibleSessions exported and filters hidden sessions': verifies
export and that local hidden sessions are filtered
- 'getVisibleSessions hides local sessions by name but not remote
sessions with same name': verifies remote sessions survive
All 155 tests pass.
After fetching /api/settings and caching in _serverSettings,
call _sources = buildSources(_serverSettings) so the polling
layer immediately knows which sources to query.
Implements task-6 of multi-device federation phase 1.
These two functions enable multi-source session federation:
- tagSessions(sessions, deviceName, sourceUrl): tags each session with
deviceName, sourceUrl, and sessionKey (format: sourceUrl::name),
returns new objects without mutating originals
- mergeSources(results): takes [{source, sessions}] objects, tags each
source's sessions and concatenates into a single flat array
Both functions are exported in module.exports.
6 new tests added covering all specified behaviors.
Auto: current behavior (auto-fill grid, fixed tile height, scrollable).
Fit: calculates cols × rows to fill viewport exactly, zero scroll.
Compact: 80px tiles, high density, auto-fill with 200px min-width.
Toggle button (▦) in overview header cycles modes. Fit recalculates
on window resize. View mode persisted in localStorage alongside other
display settings (viewMode: 'auto' default in DISPLAY_DEFAULTS).
- CSS: .session-grid--fit and .session-grid--compact modifiers
- JS: VIEW_MODES, cycleViewMode(), applyFitLayout() functions
- HTML: #view-mode-btn between + and ⚙ in overview header
- Tests: 8 new JS tests, 3 CSS tests, 1 HTML test all passing
Add 4th parameter `baseUrl` to `api()`. When provided, strips trailing
slash and prepends to path, and sets `credentials: 'include'` for
cross-origin cookie sending. Behavior is unchanged when no baseUrl is
given (backward compatible). Export `api` in module.exports.
All 135 tests pass.
🤖 Generated with Amplifier <https://github.com/microsoft/amplifier>
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Added 18px to font size select. Dialog click handler checks
e.target === settingsDialog (click landed on ::backdrop area,
not dialog content) and closes settings.
Implement new public endpoint that returns device name and version without authentication required. The auth exemption was already in place from task 4.
- Add 'remote_instances': [] to DEFAULT_SETTINGS for peer instance configs
- Add 'device_name': '' to DEFAULT_SETTINGS (empty for clean JSON serialization)
- Add import socket to settings.py
- In load_settings(), fill empty device_name with socket.gethostname() so the
hostname is always current even if the machine is renamed
- Add 7 new tests covering defaults, hostname fallback, round-trips, and
mutation isolation
- Update 2 existing tests that compared result == DEFAULT_SETTINGS to account
for the hostname fill-in behavior
Co-authored-by: Amplifier <amplifier@amplified.dev>
sendHeartbeat() always reported _viewingSession even when the user
switched to another browser tab. The server saw 'device is watching
session X' and cleared bells immediately. Fix: use Page Visibility API
(document.hidden) to send viewing_session=null when the tab is in the
background. Bells accumulate correctly, favicon badge shows activity,
and indicators appear in the dashboard/sidebar. When the user switches
back to the muxplex tab, the next heartbeat restores the real session.
launchd agents run with minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin).
Homebrew binaries (tmux, ttyd) in /opt/homebrew/bin and uv tool installs
in ~/.local/bin were not found, causing _check_dependencies() to exit(1).
Plist now includes EnvironmentVariables with a PATH that covers both
Apple Silicon and Intel Homebrew locations + ~/.local/bin.
Replace deprecated launchctl load/unload with bootstrap/bootout (modern
macOS API). Auto-bootout existing service before reinstall (fixes 'I/O
error' on reload). Add --host 0.0.0.0 to plist ProgramArguments so the
service is actually accessible from the network. install-service now
auto-starts the service and prints management commands. upgrade() also
uses modern API. doctor() checks if service is actually running.
Detect install source via PEP 610 direct_url.json:
- git+https: compare installed commit sha vs git ls-remote HEAD
- PyPI: compare installed version vs pypi.org/pypi/muxplex/json
- editable: skip (user manages source directly)
- unknown: upgrade to be safe
upgrade() skips reinstall if already up to date. --force overrides.
doctor() shows install source, commit hash, and update availability.
Platform-aware: launchctl on macOS, systemd on Linux/WSL.
Prefers uv tool install, falls back to pip.
Regenerates service file (picks up any plist/unit changes).
Runs doctor at the end for verification.
'update' is an alias for 'upgrade'.
- Inject .tile--loading skeleton tile into #session-grid immediately after
POST /api/sessions succeeds, giving instant visual feedback while the
backend creates the tmux session (which may take several seconds)
- Remove the placeholder when the poll finds the session or on timeout
- Add shimmer animation via @keyframes shimmer to style.css
- Tile placed after original .tile-body pre rule to avoid selector-substring
match in _extract_rule_block CSS tests
<dialog> is hidden by default via UA stylesheet (display:none). Our CSS
set display:flex unconditionally, overriding the UA rule and making the
dialog always visible. Fix: only apply display:flex when the dialog has
the [open] attribute (added by showModal()).
python-pam imports six but doesn't declare it. On Linux, six is usually
present system-wide. On macOS uv tool environments (clean/isolated),
import pam fails with 'No module named six'. Adding six explicitly to
our deps ensures PAM auth works on macOS.
python-multipart is required by FastAPI for form POST handling (login).
Was missing from pyproject.toml — crash on macOS first run.
launchd plist now uses the muxplex entry point script directly instead
of python -m muxplex, so macOS shows 'muxplex' in Activity Monitor,
launchctl list, and login items instead of 'python3'.
- 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