- In buildTileHTML, make data-source-url attribute conditional: only add when
session.sourceUrl is non-empty (local sessions omit the attribute entirely)
- In renderGrid click/keydown handlers, pass sourceUrl with || '' fallback so
local sessions (where data-source-url is absent) pass empty string to openSession
- Add 2 tests: one verifying data-source-url present for remote sessions,
one verifying data-source-url absent for local sessions (empty sourceUrl)
Task: task-9-grid-click-routes-source-url
- Guard DELETE /api/sessions/current with if (!_viewingSourceUrl) check
- Reset _viewingSourceUrl to '' after the conditional DELETE
- For remote sessions, the remote instance doesn't need to know we
stopped watching - just disconnect WebSocket and update local UI
Tests added:
- closeSession does NOT fire DELETE for remote session (non-empty _viewingSourceUrl)
- closeSession still fires DELETE /api/sessions/current for local session
Task: task-8-close-session-remote-aware
Bug 1: closeSession() didn't call applyFitLayout when returning to
dashboard — tiles kept old heights from before the overview was hidden.
Fix: add rAF call after overview.style.display = '' so the layout is
remeasured once the overview becomes visible again.
Bug 2: Only 20 lines shown in tiles — not enough for tall fit-mode
tiles, and content sat at the top. Fix:
- buildTileHTML shows 80 lines in fit mode (vs 20 in auto)
- renderGrid rAF sets pre.scrollTop = pre.scrollHeight after
applyFitLayout so content anchors at the bottom (like a real
terminal). Hidden scrollbar via scrollbar-width:none + ::-webkit.
Tests: 3 new source-scan tests added and verified RED before GREEN.
- Add openLoginPopup(remoteUrl) function after buildOfflineTileHTML that strips
trailing slashes from remoteUrl before appending /login and opens a popup
window with width=500,height=600
- Wire up delegated click handler for .source-tile__login-btn buttons inside
bindStaticEventListeners, right after the existing delegated kill-session handler
- Export openLoginPopup from module.exports
Tests: 4 new tests covering openLoginPopup export, window.open call
with correct URL and dimensions, trailing slash handling (with and without)
Bug 1: applyFitLayout measured parentElement.clientHeight before the
browser reflowed the DOM — returned 0 on first render. Fix: wrap in
requestAnimationFrame so measurement happens after layout pass.
Applies to all three call sites: renderGrid(), applyDisplaySettings(),
and the window resize handler.
Bug 2: .tile-body pre had position:absolute bottom:0 but no top:0.
In fit mode (taller tiles), content sat at the bottom with a gap above.
Fix: .session-grid--fit .tile-body pre { top: 0; overflow-y: auto }
- Append auth tiles for auth_required sources after session tiles
- Append offline tiles for unreachable sources after session tiles
- _setSources test helper was already defined and exported (verified)
- All 197 tests pass (195 pre-existing + 2 new)
- Add formatLastSeen(ms) to format millisecond timestamps into relative
strings (Xs ago, Xm ago, Xh ago, Xd ago, Never for null)
- Add buildOfflineTileHTML(source) to render unreachable device tiles
with Offline badge and last-seen timestamp
- Both functions added after buildAuthTileHTML in app.js
- Both exported via module.exports
- 7 new tests covering export, CSS class, device name, badge, last-seen,
HTML escaping, and null handling
- Add buildAuthTileHTML(source) function to frontend/app.js after buildSidebarHTML
- Function renders an <article class='source-tile source-tile--auth'> with device name,
login button (with data-url), and hint text
- HTML-escapes both name and url to prevent XSS
- Export buildAuthTileHTML under '// Federation tiles' comment in module.exports
- Add 5 tests covering: export, article class, device name, login button data-url, HTML escaping
Task: task-2-build-auth-tile-html
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>