From e12a965cdf9f5796d6d9afd4abbbfb38bdd57318 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Sat, 4 Apr 2026 07:51:21 -0700 Subject: [PATCH] feat: add device select dropdown to showNewSessionInput - Add _createDeviceSelect() helper function after _createSessionInput() - Returns null when multi_device_enabled is false or remote_instances is empty - Creates for multi-device session creation. + * Returns null when multi_device_enabled is false or remote_instances is empty. + * @returns {HTMLSelectElement|null} + */ +function _createDeviceSelect() { + const ss = _serverSettings || {}; + const remotes = ss.remote_instances; + if (!ss.multi_device_enabled || !remotes || remotes.length === 0) { + return null; + } + + const select = document.createElement('select'); + select.className = 'new-session-device-select'; + + // Local device option + const localOpt = document.createElement('option'); + localOpt.value = ''; + localOpt.textContent = ss.device_name || 'Local'; + select.appendChild(localOpt); + + // Remote instance options + for (var i = 0; i < remotes.length; i++) { + var opt = document.createElement('option'); + opt.value = String(i); + opt.textContent = remotes[i].name || remotes[i].url || 'Remote ' + i; + if (_activeFilterDevice === remotes[i].name || _activeFilterDevice === remotes[i].url) { + opt.selected = true; + select.value = String(i); + } + select.appendChild(opt); + } + + return select; +} + +/** + * Replace the header + button with an inline text input (and optional device + * select) for session naming. Hides the button, inserts controls before it, + * and focuses the input. + * On Enter: if name is non-empty after trim, calls createNewSession(name, remoteId). * On Escape: restores the button (cleanup only). * On blur: delayed cleanup (150ms) to allow click handlers. * @param {HTMLElement} btn - The button element to replace temporarily. */ function showNewSessionInput(btn) { + const select = _createDeviceSelect(); const input = _createSessionInput(); function cleanup() { + if (select && select.parentNode) select.parentNode.removeChild(select); if (input.parentNode) input.parentNode.removeChild(input); btn.style.display = ''; } @@ -1928,8 +1967,9 @@ function showNewSessionInput(btn) { input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { const name = input.value.trim(); + const remoteId = select ? select.value : ''; cleanup(); - if (name) createNewSession(name); + if (name) createNewSession(name, remoteId); } else if (e.key === 'Escape') { cleanup(); } @@ -1940,6 +1980,7 @@ function showNewSessionInput(btn) { }); btn.style.display = 'none'; + if (select) btn.parentNode.insertBefore(select, btn); btn.parentNode.insertBefore(input, btn); input.focus(); } @@ -2483,6 +2524,7 @@ if (typeof module !== 'undefined' && module.exports) { // Fetch wrapper api, // Header + button with inline name input + _createDeviceSelect, showNewSessionInput, showFabSessionInput, createNewSession, diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index 8213b36..c06f6ee 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -4103,3 +4103,111 @@ test('updatePageTitle is called from pollSessions', () => { const pollFn = source.substring(source.indexOf('async function pollSessions'), source.indexOf('async function pollSessions') + 700); assert.ok(pollFn.includes('updatePageTitle'), 'pollSessions must call updatePageTitle'); }); + +// --- _createDeviceSelect and showNewSessionInput multi-device tests --- + +test('_createDeviceSelect builds a element when multi_device_enabled'); +}); + +test('showNewSessionInput passes remoteId from device select to createNewSession', () => { + const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8'); + // Extract showNewSessionInput function body + const fnStart = source.indexOf('function showNewSessionInput('); + assert.ok(fnStart !== -1, 'showNewSessionInput function must exist'); + const fnBody = source.substring(fnStart, fnStart + 1200); + // Must call _createDeviceSelect + assert.ok(fnBody.includes('_createDeviceSelect'), 'showNewSessionInput must call _createDeviceSelect'); + // Must read remoteId from select.value (or equivalent) + assert.ok( + fnBody.includes('remoteId') && (fnBody.includes('select.value') || fnBody.includes('sel.value')), + 'showNewSessionInput Enter handler must read remoteId from select element value', + ); + // Must call createNewSession with two arguments (name and remoteId) + assert.ok( + fnBody.includes('createNewSession(name') && fnBody.includes('remoteId'), + 'showNewSessionInput must call createNewSession with name and remoteId arguments', + ); +});