# Device Selector for New Session Dialog — Implementation Plan > **Execution:** Use the subagent-driven-development workflow to implement this plan. **Goal:** When multi-device is enabled with remote instances configured, the "new session" dialog shows a device dropdown so users can create sessions on any device — local or remote. **Architecture:** Add a `POST /api/federation/{remote_id}/sessions` proxy endpoint on the backend (same pattern as existing `federation_connect` and `federation_bell_clear`). On the frontend, inject a `` helper and wire into `showNewSessionInput()` **Files:** - Modify: `muxplex/frontend/app.js:1902-1945` (the `_createSessionInput` and `showNewSessionInput` functions) - Test: `muxplex/frontend/tests/test_app.mjs` (append) **Step 1: Write the failing tests** Append these tests to the end of `muxplex/frontend/tests/test_app.mjs`: ```javascript // --- Device selector in new session dialog --- test('_createDeviceSelect builds a element'); assert.ok(snippet.includes('remote_instances'), '_createDeviceSelect must read remote_instances from settings'); assert.ok(snippet.includes('device_name'), '_createDeviceSelect must use device_name for local option label'); }); test('showNewSessionInput creates device select when multi_device_enabled with remotes', () => { const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8'); const start = source.indexOf('function showNewSessionInput('); assert.ok(start !== -1, 'showNewSessionInput must exist'); const snippet = source.slice(start, start + 1200); assert.ok(snippet.includes('_createDeviceSelect'), 'showNewSessionInput must call _createDeviceSelect'); }); test('showNewSessionInput passes remoteId from device select to createNewSession', () => { const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8'); const start = source.indexOf('function showNewSessionInput('); const snippet = source.slice(start, start + 1200); // The Enter handler must read select.value and pass it to createNewSession assert.ok( snippet.includes('createNewSession(name,') || snippet.includes('createNewSession(name ,'), 'showNewSessionInput must pass a second argument (remoteId) to createNewSession' ); }); ``` **Step 2: Run tests to verify they fail** Run: `cd muxplex && node --test muxplex/frontend/tests/test_app.mjs 2>&1 | tail -20` Expected: The 3 new tests FAIL **Step 3: Implement the device select helper and update `showNewSessionInput`** In `muxplex/frontend/app.js`, insert a new `_createDeviceSelect()` function immediately **after** the `_createSessionInput()` function (after line 1910, before line 1912): ```javascript /** * Create a device ` to `showFabSessionInput()` (mobile) **Files:** - Modify: `muxplex/frontend/app.js:1953-1987` (the `showFabSessionInput` function) - Test: `muxplex/frontend/tests/test_app.mjs` (append) **Step 1: Write the failing test** Append to `muxplex/frontend/tests/test_app.mjs`: ```javascript test('showFabSessionInput creates device select when multi_device_enabled with remotes', () => { const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8'); const start = source.indexOf('function showFabSessionInput('); assert.ok(start !== -1, 'showFabSessionInput must exist'); const snippet = source.slice(start, start + 1200); assert.ok(snippet.includes('_createDeviceSelect'), 'showFabSessionInput must call _createDeviceSelect'); assert.ok( snippet.includes('createNewSession(name,') || snippet.includes('createNewSession(name ,'), 'showFabSessionInput must pass remoteId to createNewSession' ); }); ``` **Step 2: Run test to verify it fails** Run: `cd muxplex && node --test muxplex/frontend/tests/test_app.mjs 2>&1 | tail -10` Expected: New test FAILS **Step 3: Update `showFabSessionInput` to include device select** Replace the entire `showFabSessionInput` function (lines 1953–1987, note: line numbers may have shifted after Task 2 edits) with: ```javascript function showFabSessionInput() { if (document.querySelector('.fab-input-overlay')) return; var fab = $('new-session-fab'); var overlay = document.createElement('div'); overlay.className = 'fab-input-overlay'; var select = _createDeviceSelect(); var input = _createSessionInput(); if (select) overlay.appendChild(select); overlay.appendChild(input); function cleanup() { if (overlay.parentNode) overlay.parentNode.removeChild(overlay); if (fab) fab.style.display = ''; } input.addEventListener('keydown', function(e) { if (e.key === 'Enter') { var name = input.value.trim(); var remoteId = select ? select.value : ''; cleanup(); if (name) createNewSession(name, remoteId); } else if (e.key === 'Escape') { cleanup(); } }); input.addEventListener('blur', function() { setTimeout(cleanup, 150); }); if (fab) fab.style.display = 'none'; document.body.appendChild(overlay); input.focus(); } ``` **Step 4: Run tests to verify they pass** Run: `cd muxplex && node --test muxplex/frontend/tests/test_app.mjs 2>&1 | tail -10` Expected: New test PASSES, no regressions **Step 5: Commit** `cd muxplex && git add muxplex/frontend/app.js muxplex/frontend/tests/test_app.mjs && git commit -m "feat: add device select to mobile FAB session input"` --- ### Task 4: Frontend — Update `createNewSession()` to route through federation proxy **Files:** - Modify: `muxplex/frontend/app.js:1998-2054` (the `createNewSession` function) - Test: `muxplex/frontend/tests/test_app.mjs` (append) **Step 1: Write the failing tests** Append to `muxplex/frontend/tests/test_app.mjs`: ```javascript test('createNewSession accepts remoteId parameter and routes to federation endpoint', () => { const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8'); const start = source.indexOf('async function createNewSession('); assert.ok(start !== -1, 'createNewSession must exist'); const snippet = source.slice(start, start + 3000); // Function signature must accept remoteId assert.ok( snippet.startsWith("async function createNewSession(name, remoteId") || snippet.startsWith("async function createNewSession(name,remoteId"), 'createNewSession must accept a remoteId parameter' ); // Must route to federation endpoint when remoteId is set assert.ok( snippet.includes('/api/federation/'), 'createNewSession must POST to /api/federation/ endpoint when remoteId is set' ); // Must still support local endpoint assert.ok( snippet.includes("'/api/sessions'"), 'createNewSession must still POST to /api/sessions for local sessions' ); }); test('createNewSession passes remoteId through to openSession for auto-open', () => { const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8'); const start = source.indexOf('async function createNewSession('); const snippet = source.slice(start, start + 3000); // Must pass remoteId to openSession in the poll callback assert.ok( snippet.includes('openSession(sessionName, { remoteId') || snippet.includes('openSession(sessionName, {remoteId'), 'createNewSession must pass { remoteId } opts to openSession when auto-opening' ); }); test('createNewSession matches remote sessions by sessionKey in poll loop', () => { const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8'); const start = source.indexOf('async function createNewSession('); const snippet = source.slice(start, start + 3000); // When remoteId is set, must match by sessionKey (remoteId:name) not just name assert.ok( snippet.includes('sessionKey'), 'createNewSession must match remote sessions by sessionKey in the polling loop' ); }); ``` **Step 2: Run tests to verify they fail** Run: `cd muxplex && node --test muxplex/frontend/tests/test_app.mjs 2>&1 | tail -20` Expected: The 3 new tests FAIL **Step 3: Update `createNewSession` to accept and use `remoteId`** Replace the entire `createNewSession` function (from `async function createNewSession(name)` through its closing `}`) with: ```javascript async function createNewSession(name, remoteId) { remoteId = remoteId || ''; try { var endpoint = remoteId ? '/api/federation/' + encodeURIComponent(remoteId) + '/sessions' : '/api/sessions'; const res = await api('POST', endpoint, { name }); const data = await res.json(); const sessionName = data.name || name; showToast('Creating session \'' + sessionName + '\'\u2026'); // Inject a loading placeholder tile so the user sees feedback immediately var loadingTile = null; var grid = document.getElementById('session-grid'); if (grid) { loadingTile = document.createElement('div'); loadingTile.className = 'session-tile tile--loading'; loadingTile.id = 'loading-tile-' + sessionName; loadingTile.innerHTML = '
' + escapeHtml(sessionName) + '' + 'Creating...
' + '
'; grid.appendChild(loadingTile); } function removeLoadingTile() { var tile = document.getElementById('loading-tile-' + sessionName); if (tile) tile.remove(); } const ss = _serverSettings || {}; if (ss.auto_open_created === false) { // Auto-open disabled — just do one refresh await pollSessions(); removeLoadingTile(); return; } // For remote sessions, the sessionKey is "remoteId:name" var expectedKey = remoteId ? (remoteId + ':' + sessionName) : sessionName; // Poll until the session appears in _currentSessions (max 30s, every 2s) var attempts = 0; var maxAttempts = 15; var pollForSession = setInterval(async function() { attempts++; await pollSessions(); var found = _currentSessions && _currentSessions.find(function(s) { return (s.sessionKey || s.name) === expectedKey; }); if (found) { clearInterval(pollForSession); removeLoadingTile(); showToast('Session \'' + sessionName + '\' ready'); openSession(sessionName, { remoteId: remoteId }); } else if (attempts >= maxAttempts) { clearInterval(pollForSession); removeLoadingTile(); showToast('Session \'' + sessionName + '\' is taking longer than expected'); } }, 2000); } catch (err) { showToast(err.message || 'Failed to create session'); } } ``` **Step 4: Run tests to verify they pass** Run: `cd muxplex && node --test muxplex/frontend/tests/test_app.mjs 2>&1 | tail -20` Expected: All new tests PASS, no regressions **Step 5: Commit** `cd muxplex && git add muxplex/frontend/app.js muxplex/frontend/tests/test_app.mjs && git commit -m "feat: route createNewSession through federation proxy for remote devices"` --- ### Task 5: CSS — Style the device select dropdown **Files:** - Modify: `muxplex/frontend/style.css` (after `.new-session-input::placeholder` rule, ~line 1523) - Test: `muxplex/frontend/tests/test_app.mjs` (append) **Step 1: Write the failing test** Append to `muxplex/frontend/tests/test_app.mjs`: ```javascript test('CSS has new-session-device-select styling', () => { const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8'); assert.ok(source.includes('.new-session-device-select'), 'style.css must have .new-session-device-select rule'); }); ``` **Step 2: Run test to verify it fails** Run: `cd muxplex && node --test muxplex/frontend/tests/test_app.mjs 2>&1 | tail -5` Expected: FAIL — CSS rule doesn't exist yet **Step 3: Add the CSS rule** In `muxplex/frontend/style.css`, insert immediately after the `.new-session-input::placeholder` rule (after line 1523): ```css .new-session-device-select { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; font-size: 12px; font-family: var(--font-ui); padding: 4px 6px; color: var(--text); outline: none; margin-right: 6px; vertical-align: middle; } .fab-input-overlay .new-session-device-select { display: block; width: 100%; margin-bottom: 6px; margin-right: 0; } ``` **Step 4: Run tests to verify they pass** Run: `cd muxplex && node --test muxplex/frontend/tests/test_app.mjs 2>&1 | tail -5` Expected: PASS **Step 5: Commit** `cd muxplex && git add muxplex/frontend/style.css muxplex/frontend/tests/test_app.mjs && git commit -m "feat: style device select dropdown for new session dialog"` --- ### Task 6: Full test suite verification **Files:** - No new files — verification only **Step 1: Run the full backend test suite** Run: `cd muxplex && python -m pytest muxplex/tests/test_api.py -v --tb=short 2>&1 | tail -30` Expected: All tests PASS (including the 4 new federation_create_session tests) **Step 2: Run the full frontend test suite** Run: `cd muxplex && node --test muxplex/frontend/tests/test_app.mjs 2>&1 | tail -30` Expected: All tests PASS (including the 5 new device-selector tests) **Step 3: Run Python quality checks** Run: `cd muxplex && python -m ruff check muxplex/main.py muxplex/tests/test_api.py && python -m ruff format --check muxplex/main.py muxplex/tests/test_api.py` Expected: No issues **Step 4: Commit (if any lint/format fixes were needed)** `cd muxplex && git add -A && git commit -m "chore: lint/format fixes" --allow-empty`