31 KiB
Settings Panel — Phase 2: Remaining Tabs + New Session UI
Execution: Use the subagent-driven-development workflow to implement this plan.
Phase 2 of 2 — complete Phase 1 before starting this phase.
Design doc: docs/plans/2026-03-29-settings-design.md
Goal: Complete all four settings tabs and build the new session creation UI — + button, inline name input, mobile FAB, and end-to-end creation flow.
Architecture: Sessions/New Session tabs use the GET/PATCH /api/settings and POST /api/sessions endpoints from Phase 1. The + button uses inline name input (header, sidebar, FAB) that calls the create endpoint. Display/Notifications tabs use localStorage. All controls apply immediately — no save button.
Tech Stack: Vanilla JS, CSS custom properties, FastAPI backend (already deployed from Phase 1).
Prereqs from Phase 1:
muxplex/settings.py—load_settings(),save_settings(),patch_settings()GET/PATCH /api/settings,POST /api/sessionsendpoints inmain.py- Settings dialog HTML skeleton with tab navigation in
index.html openSettings(),closeSettings(),switchSettingsTab(),loadDisplaySettings(),saveDisplaySettings()inapp.js.settings-*CSS classes instyle.css
Task 1: Sessions tab — server settings UI
Files:
- Modify:
muxplex/frontend/index.html - Modify:
muxplex/frontend/app.js
Step 1: Add Sessions tab HTML
In muxplex/frontend/index.html, replace the empty #settings-panel-sessions div:
<!-- Sessions tab -->
<div id="settings-panel-sessions" class="settings-panel hidden" role="tabpanel" data-tab="sessions">
<h3 class="settings-panel-title">Sessions</h3>
<label class="settings-field">
<span class="settings-label">Default session</span>
<select id="setting-default-session" class="settings-select">
<option value="">None</option>
</select>
</label>
<label class="settings-field">
<span class="settings-label">Sort order</span>
<select id="setting-sort-order" class="settings-select">
<option value="manual">Manual</option>
<option value="alphabetical">Alphabetical</option>
<option value="recent">Recent activity</option>
</select>
</label>
<div class="settings-field settings-field--column">
<span class="settings-label">Hidden sessions</span>
<div id="setting-hidden-sessions" class="settings-checkbox-list"></div>
</div>
<label class="settings-field">
<span class="settings-label">Auto-set window-size largest</span>
<input type="checkbox" id="setting-window-size-largest" class="settings-checkbox">
</label>
<label class="settings-field">
<span class="settings-label">Auto-open created sessions</span>
<input type="checkbox" id="setting-auto-open" class="settings-checkbox" checked>
</label>
</div>
Step 2: Add server settings load/save functions to app.js
In muxplex/frontend/app.js, add after the onDisplaySettingChange function:
// ─── Server settings ─────────────────────────────────────────────────────────
let _serverSettings = null;
/**
* Load server settings via GET /api/settings.
* Caches the result in _serverSettings for local reads.
* @returns {Promise<object>}
*/
async function loadServerSettings() {
try {
var res = await api('GET', '/api/settings');
_serverSettings = await res.json();
} catch (err) {
console.warn('[loadServerSettings]', err);
_serverSettings = _serverSettings || {};
}
return _serverSettings;
}
/**
* Patch a single server setting field.
* @param {string} key
* @param {*} value
* @returns {Promise<void>}
*/
async function patchServerSetting(key, value) {
var patch = {};
patch[key] = value;
try {
var res = await api('PATCH', '/api/settings', patch);
_serverSettings = await res.json();
showToast('Setting saved');
} catch (err) {
console.warn('[patchServerSetting]', err);
showToast('Failed to save setting');
}
}
Step 3: Populate Sessions tab when settings opens
Update the openSettings function. After the display settings load, add server settings loading:
// Load server settings and populate Sessions tab
loadServerSettings().then(function(ss) {
// Default session dropdown: populate with current session list
var defaultSel = $('setting-default-session');
if (defaultSel) {
var current = ss.default_session || '';
defaultSel.innerHTML = '<option value="">None</option>' +
_currentSessions.map(function(s) {
var name = escapeHtml(s.name);
var selected = s.name === current ? ' selected' : '';
return '<option value="' + name + '"' + selected + '>' + name + '</option>';
}).join('');
}
// Sort order
var sortSel = $('setting-sort-order');
if (sortSel) sortSel.value = ss.sort_order || 'manual';
// Hidden sessions checkboxes
var hiddenDiv = $('setting-hidden-sessions');
if (hiddenDiv) {
var hiddenList = ss.hidden_sessions || [];
hiddenDiv.innerHTML = _currentSessions.map(function(s) {
var name = escapeHtml(s.name);
var checked = hiddenList.indexOf(s.name) !== -1 ? ' checked' : '';
return '<label class="settings-checkbox-item">' +
'<input type="checkbox" data-session="' + name + '"' + checked + '> ' + name +
'</label>';
}).join('');
}
// Checkboxes
var wslEl = $('setting-window-size-largest');
if (wslEl) wslEl.checked = !!ss.window_size_largest;
var aoEl = $('setting-auto-open');
if (aoEl) aoEl.checked = ss.auto_open_created !== false;
});
Step 4: Bind change handlers for Sessions tab in bindStaticEventListeners
Add to bindStaticEventListeners:
// Sessions tab change handlers
on($('setting-default-session'), 'change', function() {
patchServerSetting('default_session', this.value || null);
});
on($('setting-sort-order'), 'change', function() {
patchServerSetting('sort_order', this.value);
});
on($('setting-window-size-largest'), 'change', function() {
patchServerSetting('window_size_largest', this.checked);
});
on($('setting-auto-open'), 'change', function() {
patchServerSetting('auto_open_created', this.checked);
});
// Hidden sessions: delegated handler on container (checkboxes are dynamic)
var hiddenContainer = $('setting-hidden-sessions');
if (hiddenContainer) {
hiddenContainer.addEventListener('change', function() {
var checkboxes = hiddenContainer.querySelectorAll('input[type="checkbox"]');
var hidden = [];
checkboxes.forEach(function(cb) {
if (cb.checked) hidden.push(cb.dataset.session);
});
patchServerSetting('hidden_sessions', hidden);
});
}
Step 5: Add CSS for column-layout fields and checkbox list
Append to muxplex/frontend/style.css:
.settings-field--column {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.settings-checkbox-list {
display: flex;
flex-direction: column;
gap: 6px;
padding-left: 4px;
}
.settings-checkbox-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text);
cursor: pointer;
}
.settings-checkbox {
accent-color: var(--accent);
cursor: pointer;
}
Step 6: Add exports
Add to module.exports:
loadServerSettings,
patchServerSetting,
Step 7: Run tests
cd muxplex && python -m pytest muxplex/tests/ -v
Expected: All tests pass.
Step 8: Commit
git add -A && git commit -m "feat: add Sessions settings tab with server-side persistence"
Task 2: Notifications tab
Files:
- Modify:
muxplex/frontend/index.html - Modify:
muxplex/frontend/app.js - Modify:
muxplex/frontend/style.css
Step 1: Add Notifications tab HTML
In muxplex/frontend/index.html, replace the empty #settings-panel-notifications div:
<!-- Notifications tab -->
<div id="settings-panel-notifications" class="settings-panel hidden" role="tabpanel" data-tab="notifications">
<h3 class="settings-panel-title">Notifications</h3>
<label class="settings-field">
<span class="settings-label">Bell sound</span>
<input type="checkbox" id="setting-bell-sound" class="settings-checkbox">
</label>
<div class="settings-field">
<span class="settings-label">Desktop notifications</span>
<div class="settings-notification-status">
<span id="notification-status-text" class="settings-status-text">Not requested</span>
<button id="notification-request-btn" class="settings-action-btn">Request permission</button>
</div>
</div>
</div>
Step 2: Add CSS for notification status and action button
Append to muxplex/frontend/style.css:
.settings-notification-status {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6px;
}
.settings-status-text {
font-size: 12px;
color: var(--text-muted);
}
.settings-action-btn {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 12px;
font-family: var(--font-ui);
padding: 6px 12px;
cursor: pointer;
transition: border-color var(--t-fast);
}
.settings-action-btn:hover {
border-color: var(--accent);
}
.settings-action-btn:disabled {
opacity: 0.5;
cursor: default;
}
Step 3: Add notification handlers to app.js
Update openSettings to populate notification controls (add inside the function):
// Notifications tab
var ds2 = loadDisplaySettings();
var bellSoundEl = $('setting-bell-sound');
if (bellSoundEl) bellSoundEl.checked = !!ds2.bellSound;
var notifText = $('notification-status-text');
var notifBtn = $('notification-request-btn');
if (notifText && notifBtn) {
var perm = typeof Notification !== 'undefined' ? Notification.permission : 'unavailable';
if (perm === 'granted') {
notifText.textContent = 'Granted';
notifBtn.textContent = 'Granted';
notifBtn.disabled = true;
} else if (perm === 'denied') {
notifText.textContent = 'Denied (change in browser settings)';
notifBtn.textContent = 'Denied';
notifBtn.disabled = true;
} else if (perm === 'unavailable') {
notifText.textContent = 'Not supported';
notifBtn.disabled = true;
} else {
notifText.textContent = 'Not requested';
notifBtn.textContent = 'Request permission';
notifBtn.disabled = false;
}
}
Step 4: Bind notification change handlers in bindStaticEventListeners
Add to bindStaticEventListeners:
// Notifications tab
on($('setting-bell-sound'), 'change', function() {
var ds = loadDisplaySettings();
ds.bellSound = this.checked;
saveDisplaySettings(ds);
});
on($('notification-request-btn'), 'click', function() {
if (typeof Notification === 'undefined') return;
Notification.requestPermission().then(function(perm) {
_notificationPermission = perm;
var ds = loadDisplaySettings();
ds.notificationPermission = perm;
saveDisplaySettings(ds);
// Update UI
var notifText = $('notification-status-text');
var notifBtn = $('notification-request-btn');
if (perm === 'granted') {
if (notifText) notifText.textContent = 'Granted';
if (notifBtn) { notifBtn.textContent = 'Granted'; notifBtn.disabled = true; }
} else if (perm === 'denied') {
if (notifText) notifText.textContent = 'Denied (change in browser settings)';
if (notifBtn) { notifBtn.textContent = 'Denied'; notifBtn.disabled = true; }
}
});
});
Step 5: Run tests
cd muxplex && python -m pytest muxplex/tests/ -v
Expected: All tests pass.
Step 6: Commit
git add -A && git commit -m "feat: add Notifications settings tab"
Task 3: New Session tab — template textarea
Files:
- Modify:
muxplex/frontend/index.html - Modify:
muxplex/frontend/app.js - Modify:
muxplex/frontend/style.css
Step 1: Add New Session tab HTML
In muxplex/frontend/index.html, replace the empty #settings-panel-new-session div:
<!-- New Session tab -->
<div id="settings-panel-new-session" class="settings-panel hidden" role="tabpanel" data-tab="new-session">
<h3 class="settings-panel-title">New Session</h3>
<div class="settings-field settings-field--column">
<span class="settings-label">Command template</span>
<textarea id="setting-template" class="settings-textarea" rows="3"
placeholder="tmux new-session -d -s {name}"></textarea>
<span class="settings-helper">{name} is replaced with the session name</span>
</div>
<button id="setting-template-reset" class="settings-action-btn">Reset to default</button>
</div>
Step 2: Add CSS for textarea and helper text
Append to muxplex/frontend/style.css:
.settings-textarea {
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 13px;
font-family: var(--font-mono);
padding: 10px;
resize: vertical;
min-height: 60px;
}
.settings-textarea:focus {
outline: 1px solid var(--accent);
border-color: var(--accent);
}
.settings-helper {
font-size: 12px;
color: var(--text-muted);
font-style: italic;
}
Step 3: Populate template on settings open
Inside openSettings, in the loadServerSettings().then(...) callback, add:
// New Session tab
var templateEl = $('setting-template');
if (templateEl) templateEl.value = ss.new_session_template || 'tmux new-session -d -s {name}';
Step 4: Bind template change handlers in bindStaticEventListeners
Add to bindStaticEventListeners:
// New Session tab: save template on blur (not every keystroke)
var templateEl = $('setting-template');
if (templateEl) {
var _templateDebounce = null;
templateEl.addEventListener('input', function() {
clearTimeout(_templateDebounce);
_templateDebounce = setTimeout(function() {
patchServerSetting('new_session_template', templateEl.value);
}, 500);
});
}
on($('setting-template-reset'), 'click', function() {
var templateEl = $('setting-template');
if (templateEl) templateEl.value = 'tmux new-session -d -s {name}';
patchServerSetting('new_session_template', 'tmux new-session -d -s {name}');
});
Step 5: Run tests
cd muxplex && python -m pytest muxplex/tests/ -v
Expected: All tests pass.
Step 6: Commit
git add -A && git commit -m "feat: add New Session settings tab with template textarea"
Task 4: Header + button with inline name input
Files:
- Modify:
muxplex/frontend/app.js - Modify:
muxplex/frontend/style.css
Step 1: Add new session creation function
In muxplex/frontend/app.js, add after the server settings functions:
// ─── New session creation ────────────────────────────────────────────────────
/**
* Show an inline name input replacing the given button element.
* On Enter: call createNewSession(). On Escape: cancel and restore button.
* @param {HTMLElement} btn - the button to replace with input
*/
function showNewSessionInput(btn) {
if (!btn) return;
var container = btn.parentElement;
if (!container) return;
// Create inline input
var input = document.createElement('input');
input.type = 'text';
input.className = 'new-session-input';
input.placeholder = 'Session name\u2026';
input.setAttribute('autocomplete', 'off');
input.setAttribute('spellcheck', 'false');
// Replace button with input
btn.style.display = 'none';
container.insertBefore(input, btn);
input.focus();
function cleanup() {
input.remove();
btn.style.display = '';
}
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
var name = input.value.trim();
if (name) {
cleanup();
createNewSession(name);
}
} else if (e.key === 'Escape') {
e.preventDefault();
cleanup();
}
});
// Cancel on blur (click away)
input.addEventListener('blur', function() {
// Delay to allow click handlers to fire first
setTimeout(cleanup, 150);
});
}
/**
* Create a new session by POSTing to /api/sessions.
* Shows toast, triggers poll refresh, auto-opens if setting enabled.
* @param {string} name
* @returns {Promise<void>}
*/
async function createNewSession(name) {
try {
var res = await api('POST', '/api/sessions', { name: name });
var data = await res.json();
showToast("Session '" + data.name + "' created");
// Trigger immediate poll refresh to pick up the new session
await pollSessions();
// Auto-open if setting enabled
var ss = _serverSettings || await loadServerSettings();
if (ss.auto_open_created !== false) {
// Small delay to let tmux create the session
setTimeout(function() {
openSession(data.name);
}, 500);
}
} catch (err) {
showToast(err.message || 'Failed to create session');
}
}
Step 2: Add CSS for inline input
Append to muxplex/frontend/style.css:
/* ============================================================
New session inline input
============================================================ */
.new-session-input {
background: var(--bg);
border: 1px solid var(--accent);
border-radius: 4px;
color: var(--text);
font-size: 13px;
font-family: var(--font-ui);
padding: 4px 10px;
width: 180px;
outline: none;
}
.new-session-input::placeholder {
color: var(--text-dim);
}
Step 3: Bind the header + button
Add to bindStaticEventListeners:
// New session: header + button
on($('new-session-btn'), 'click', function() {
showNewSessionInput($('new-session-btn'));
});
Step 4: Add exports
Add to module.exports:
showNewSessionInput,
createNewSession,
Step 5: Run tests
cd muxplex && python -m pytest muxplex/tests/ -v
Expected: All tests pass.
Step 6: Commit
git add -A && git commit -m "feat: add header + button with inline name input for session creation"
Task 5: Sidebar + New sticky footer
Files:
- Modify:
muxplex/frontend/index.html - Modify:
muxplex/frontend/app.js - Modify:
muxplex/frontend/style.css
Step 1: Add sidebar footer HTML
In muxplex/frontend/index.html, add a footer inside the #session-sidebar div, after #sidebar-list:
<div id="session-sidebar" class="session-sidebar">
<div class="sidebar-header">
<span class="sidebar-title">Sessions</span>
<button id="sidebar-collapse-btn" class="sidebar-collapse-btn" aria-label="Collapse session list">‹</button>
</div>
<div id="sidebar-list" class="sidebar-list"></div>
<div class="sidebar-footer">
<button id="sidebar-new-session-btn" class="sidebar-new-btn">+ New</button>
</div>
</div>
Step 2: Add sidebar footer CSS
Append to muxplex/frontend/style.css:
.sidebar-footer {
padding: 8px;
border-top: 1px solid var(--border-subtle);
flex-shrink: 0;
}
.sidebar-new-btn {
width: 100%;
background: none;
border: 1px dashed var(--border);
border-radius: 4px;
color: var(--text-dim);
font-size: 12px;
font-family: var(--font-ui);
padding: 8px;
cursor: pointer;
transition: color var(--t-fast), border-color var(--t-fast);
}
.sidebar-new-btn:hover {
color: var(--text);
border-color: var(--text-muted);
}
Step 3: Bind sidebar button
Add to bindStaticEventListeners:
// New session: sidebar footer button
on($('sidebar-new-session-btn'), 'click', function() {
showNewSessionInput($('sidebar-new-session-btn'));
});
Step 4: Run tests
cd muxplex && python -m pytest muxplex/tests/ -v
Expected: All tests pass.
Step 5: Commit
git add -A && git commit -m "feat: add sidebar + New sticky footer with inline input"
Task 6: Mobile FAB
Files:
- Modify:
muxplex/frontend/index.html - Modify:
muxplex/frontend/app.js - Modify:
muxplex/frontend/style.css
Step 1: Add FAB HTML
In muxplex/frontend/index.html, add before the toast element:
<!-- ── Mobile FAB (new session) ─────────────────────────────── -->
<button id="new-session-fab" class="new-session-fab" aria-label="New session">+</button>
Step 2: Add FAB CSS
Append to muxplex/frontend/style.css:
/* ============================================================
Mobile FAB (new session)
============================================================ */
.new-session-fab {
display: none; /* hidden by default, shown on mobile */
position: fixed;
bottom: 16px;
right: 16px;
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--accent);
color: var(--bg);
border: none;
font-size: 28px;
font-weight: 300;
line-height: 1;
cursor: pointer;
z-index: 100;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
transition: transform var(--t-fast), box-shadow var(--t-fast);
}
.new-session-fab:active {
transform: scale(0.95);
}
@media (max-width: 959px) {
.new-session-fab {
display: flex;
align-items: center;
justify-content: center;
}
/* Hide the header + button on mobile since FAB replaces it */
#new-session-btn {
display: none;
}
}
Step 3: Bind FAB click
Add to bindStaticEventListeners:
// New session: mobile FAB
on($('new-session-fab'), 'click', function() {
showNewSessionInput($('new-session-fab'));
});
Step 4: Hide FAB when in fullscreen view
In openSession, add after switching views:
var fab = $('new-session-fab');
if (fab) fab.classList.add('hidden');
In closeSession, restore the FAB:
var fab = $('new-session-fab');
if (fab) fab.classList.remove('hidden');
Step 5: Run tests
cd muxplex && python -m pytest muxplex/tests/ -v
Expected: All tests pass.
Step 6: Commit
git add -A && git commit -m "feat: add mobile FAB for new session creation"
Task 7: Apply settings effects — font size, grid columns, hover delay, hidden sessions, sort order
Files:
- Modify:
muxplex/frontend/app.js - Modify:
muxplex/frontend/terminal.js
Step 1: Font size affects terminal.js
In muxplex/frontend/terminal.js, update the createTerminal function to read font size from localStorage:
Replace the hardcoded fontSize in createTerminal():
// Read font size from display settings (localStorage)
var storedFontSize = 14;
try {
var raw = localStorage.getItem('muxplex.display');
if (raw) {
var parsed = JSON.parse(raw);
if (parsed.fontSize) storedFontSize = parsed.fontSize;
}
} catch (_) {}
const mobile = window.innerWidth < 600;
_term = new window.Terminal({
cursorBlink: true,
fontSize: mobile ? Math.min(storedFontSize, 12) : storedFontSize,
fontFamily: "'SF Mono', 'Fira Code', Consolas, monospace",
// ... rest unchanged
});
Step 2: Hidden sessions filter in renderGrid
In muxplex/frontend/app.js, update renderGrid to filter hidden sessions. At the top of the function, before computing ordered:
// Filter hidden sessions (server settings)
var hidden = (_serverSettings && _serverSettings.hidden_sessions) || [];
var visible = sessions;
if (hidden.length > 0) {
visible = sessions.filter(function(s) {
return hidden.indexOf(s.name) === -1;
});
}
Then use visible instead of sessions for the rest of the function. Update the empty-state check and the ordered computation:
if (!visible || visible.length === 0) {
if (grid) grid.innerHTML = '';
if (emptyState) emptyState.classList.remove('hidden');
return;
}
if (emptyState) emptyState.classList.add('hidden');
const mobile = isMobile();
const ordered = mobile ? sortByPriority(visible) : visible;
Step 3: Sort order support in renderGrid
Update renderGrid to apply sort order from server settings. After the hidden filter, before the mobile check:
// Apply sort order from server settings
var sortOrder = (_serverSettings && _serverSettings.sort_order) || 'manual';
if (sortOrder === 'alphabetical') {
visible = visible.slice().sort(function(a, b) {
return (a.name || '').localeCompare(b.name || '');
});
}
// 'recent' and 'manual' use the server-provided order (which is already the default)
Step 4: Hidden sessions filter in renderSidebar
In renderSidebar, apply the same hidden filter:
var hidden = (_serverSettings && _serverSettings.hidden_sessions) || [];
var visible = sessions;
if (hidden.length > 0) {
visible = sessions.filter(function(s) {
return hidden.indexOf(s.name) === -1;
});
}
Use visible instead of sessions for the sidebar rendering.
Step 5: Load server settings on page init
In the DOMContentLoaded handler, after startPolling(), add a call to load server settings:
restoreState()
.then(() => {
startPolling();
startHeartbeat();
requestNotificationPermission();
loadServerSettings(); // <-- add this line
bindStaticEventListeners();
})
Step 6: Run tests
cd muxplex && python -m pytest muxplex/tests/ -v
Expected: All tests pass.
Step 7: Commit
git add -A && git commit -m "feat: apply settings effects — font size, grid columns, hidden sessions, sort order"
Task 8: Frontend tests for settings
Files:
- Modify:
muxplex/tests/test_frontend_html.py - Modify:
muxplex/tests/test_frontend_css.py
Step 1: Add HTML tests for settings elements
Add to muxplex/tests/test_frontend_html.py:
def test_html_settings_dialog() -> None:
"""id=settings-dialog, settings-backdrop, settings-btn."""
soup = _SOUP
for id_ in ("settings-dialog", "settings-backdrop", "settings-btn"):
assert soup.find(id=id_), f"Missing element with id='{id_}'"
def test_html_settings_tabs() -> None:
"""Settings dialog contains four tab buttons: display, sessions, notifications, new-session."""
soup = _SOUP
tabs = soup.select(".settings-tab")
tab_names = [t.get("data-tab") for t in tabs]
assert "display" in tab_names
assert "sessions" in tab_names
assert "notifications" in tab_names
assert "new-session" in tab_names
def test_html_display_tab_controls() -> None:
"""Display tab has font-size, hover-delay, and grid-columns selects."""
soup = _SOUP
for id_ in ("setting-font-size", "setting-hover-delay", "setting-grid-columns"):
assert soup.find(id=id_), f"Missing element with id='{id_}'"
def test_html_new_session_btn() -> None:
"""id=new-session-btn, new-session-fab exist."""
soup = _SOUP
assert soup.find(id="new-session-btn"), "Missing element with id='new-session-btn'"
assert soup.find(id="new-session-fab"), "Missing element with id='new-session-fab'"
def test_html_sidebar_new_session_btn() -> None:
"""id=sidebar-new-session-btn exists inside the sidebar."""
soup = _SOUP
assert soup.find(id="sidebar-new-session-btn"), "Missing element with id='sidebar-new-session-btn'"
def test_html_sessions_tab_controls() -> None:
"""Sessions tab has default-session, sort-order selects and checkboxes."""
soup = _SOUP
for id_ in ("setting-default-session", "setting-sort-order", "setting-window-size-largest", "setting-auto-open"):
assert soup.find(id=id_), f"Missing element with id='{id_}'"
def test_html_notifications_tab_controls() -> None:
"""Notifications tab has bell-sound checkbox and permission button."""
soup = _SOUP
assert soup.find(id="setting-bell-sound"), "Missing id='setting-bell-sound'"
assert soup.find(id="notification-request-btn"), "Missing id='notification-request-btn'"
def test_html_new_session_tab_controls() -> None:
"""New Session tab has template textarea and reset button."""
soup = _SOUP
assert soup.find(id="setting-template"), "Missing id='setting-template'"
assert soup.find(id="setting-template-reset"), "Missing id='setting-template-reset'"
Step 2: Add CSS tests for settings styles
Add to muxplex/tests/test_frontend_css.py:
def test_css_settings_dialog():
css = read_css()
assert ".settings-dialog" in css
assert ".settings-tabs" in css
assert ".settings-tab--active" in css
assert ".settings-content" in css
assert ".settings-field" in css
assert ".settings-select" in css
def test_css_header_btn():
css = read_css()
assert ".header-btn" in css
assert ".header-actions" in css
def test_css_new_session_fab():
css = read_css()
assert ".new-session-fab" in css
def test_css_new_session_input():
css = read_css()
assert ".new-session-input" in css
def test_css_settings_textarea():
css = read_css()
assert ".settings-textarea" in css
def test_css_sidebar_footer():
css = read_css()
assert ".sidebar-footer" in css
assert ".sidebar-new-btn" in css
Step 3: Run all tests
cd muxplex && python -m pytest muxplex/tests/ -v
Expected: All tests pass (both old and new).
Step 4: Commit
git add -A && git commit -m "test: add frontend tests for settings dialog and new session UI"
Phase 2 Complete
After completing all 8 tasks, the full settings feature is done:
- Sessions tab — default session, sort order, hidden sessions, window-size largest, auto-open
- Notifications tab — bell sound toggle, desktop notification permission button
- New Session tab — command template textarea with reset button
- Header
+button — inline name input, creates session via API - Sidebar
+ New— sticky footer with same inline input - Mobile FAB — 56px floating action button, <960px only
- Settings effects applied — font size, grid columns, hover delay, hidden sessions, sort order
- Frontend tests — HTML structure and CSS coverage for all new elements
Open questions (from design doc — decide during implementation):
- Hidden sessions: completely removed from grid/sidebar (current implementation)
- Session name validation: accepts anything the user types (tmux will reject invalid names)
- Settings pre-auth: not accessible from login page (v1)