Replace s.name with s.sessionKey || s.name as the map key in
detectBellTransitions so two sessions with the same name on
different devices are tracked independently.
- prevMap construction uses s.sessionKey || s.name as key
- next filter lookup uses the same s.sessionKey || s.name key
- Falls back to s.name when no sessionKey is present (backward compatible)
Co-authored-by: Amplifier <amplifier@example.com>
When opts.sourceUrl is non-empty (remote session), skip the POST
/api/sessions/{name}/connect call and skip window._openTerminal(name).
Remote terminal connections are deferred to Phase 3.
Local sessions (empty sourceUrl) continue to work as before.
Co-authored-by: Amplifier <amplifier@amplified.dev>
- Replace renderSidebar to group sessions by deviceName when _sources.length > 1
- Render sidebar-device-header h4 elements before each device's sessions
- When single source configured, render flat list with no headers
- Click handlers now pass sourceUrl to openSession
- Add tests: groups with header when multiple sources, flat when single source
🤖 Generated with Amplifier
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
- Deduplicate CSS: merge shared properties of .settings-remote-url and
.settings-remote-name into a combined selector, keeping only flex
values in individual rules. Reduces 18 lines to 10.
- Fix remove button hover color: .settings-remote-remove:hover now uses
#ef4444 (red) and rgba(239, 68, 68, 0.1) background, consistent with
.tile-delete:hover and .sidebar-delete:hover for destructive actions.
- Add aria-label attributes to urlInput and nameInput in
_buildRemoteInstanceRow for screen-reader accessibility:
'Remote instance URL' and 'Remote instance display name'.
- Add 2 tests to verify aria-label attributes are set on both inputs
(test_build_remote_instance_row_url_input_aria_label,
test_build_remote_instance_row_name_input_aria_label).
All 615 tests pass.
Co-authored-by: Amplifier <amplifier@sourcegraph.com>
- Add loadGridViewMode() that reads grid view mode preference based on
viewPreferenceScope in display settings: returns gridViewMode from
localStorage display settings when scope is 'local' (default), or
grid_view_mode from _serverSettings when scope is 'server'. Returns
'flat' as default when no preference is set.
- Add saveGridViewMode(mode) that saves to appropriate scope (localStorage
via saveDisplaySettings when local, patchServerSetting when server)
and updates _gridViewMode module variable.
- Wire loadGridViewMode() into DOMContentLoaded startup after applyDisplaySettings
so _gridViewMode is initialized from stored preference on page load.
- Export both functions from module.exports for testing.
Closes task-13
- Fixed imprecise pill count assertion in test_app.mjs by changing /filter-pill/g regex to /<button/g to accurately count button elements without double-counting when active pill has both classes
- Removed redundant OR fallback in active pill test, now relying only on specific regex for Laptop pill
- Added defensive null guard 'allSessions = allSessions || []' at entry of renderFilterBar in app.js to prevent TypeError
🤖 Generated with Amplifier
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
- Add <div id="filter-bar" class="filter-bar"> to index.html before session-grid div
- Implement renderFilterBar(container, allSessions) that renders pill buttons
for each unique device name plus an 'All' button
- Active device pill gets filter-pill--active class
- Add _setActiveFilterDevice(device) test helper
- Bind delegated click handler on filter-bar in bindStaticEventListeners
that sets _activeFilterDevice and re-renders grid
- Export renderFilterBar and _setActiveFilterDevice
Co-authored-by: Amplifier <amplifier@amplified.dev>
- Add renderGroupedGrid(sessions, mobile) that groups sessions by deviceName
and creates <h3 class="device-group-header"> headers for each group
- Rewrite renderGrid to: apply device filter in 'filtered' mode, apply sort
order, call renderGroupedGrid when _gridViewMode === 'grouped', render
filter bar in 'filtered' mode, bind click handlers with sourceUrl
- Add _setGridViewMode(mode) test-only helper
- Export renderGroupedGrid and _setGridViewMode from module.exports
- 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()).