diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js
index 0e21afe..4e65092 100644
--- a/muxplex/frontend/app.js
+++ b/muxplex/frontend/app.js
@@ -898,187 +898,14 @@ function _setViewingSession(name) {
_viewingSession = name;
}
-// ─── Command palette state ────────────────────────────────────────────────────
-const PALETTE_MAX_ITEMS = 9;
-let _paletteSelectedIndex = 0;
-let _paletteFilteredSessions = [];
-let _paletteOpen = false;
-let _paletteInputListener = null;
-
-// ─── Command palette functions ────────────────────────────────────────────────
-
-/**
- * Render the filtered session list inside #palette-list.
- * Shows up to 9 items. Each item is a
with index number,
- * session name, optional bell emoji, and timestamp.
- */
-function renderPaletteList() {
- const list = $('palette-list');
- if (!list) return;
-
- const items = _paletteFilteredSessions.slice(0, PALETTE_MAX_ITEMS);
- list.innerHTML = items
- .map((session, i) => {
- const isBell = sessionPriority(session) === 'bell';
- const bell = isBell ? ' 🔔' : '';
- const time = formatTimestamp(session.last_activity_at || null);
- const name = escapeHtml(session.name || '');
- return `${i + 1} ${name}${bell} ${escapeHtml(time)}`;
- })
- .join('');
-
- // Bind click handlers on each item
- list.querySelectorAll('.palette-item').forEach((item) => {
- on(item, 'click', () => {
- const idx = parseInt(item.dataset.index, 10);
- const session = _paletteFilteredSessions[idx];
- if (session) {
- closePalette();
- openSession(session.name).catch((err) => console.error('[renderPaletteList]', err));
- }
- });
- });
-
- highlightPaletteItem(_paletteSelectedIndex);
-}
-
-/**
- * Toggle the palette-item--selected class on the item at `index`.
- * @param {number} index
- */
-function highlightPaletteItem(index) {
- const list = $('palette-list');
- if (!list) return;
- list.querySelectorAll('.palette-item').forEach((item, i) => {
- if (i === index) {
- item.classList.add('palette-item--selected');
- } else {
- item.classList.remove('palette-item--selected');
- }
- });
-}
-
-/**
- * Open the command palette.
- * Shows #command-palette, copies _currentSessions to _paletteFilteredSessions,
- * renders the list, resets selection index, focuses #palette-input, and binds
- * the input event listener.
- */
-function openPalette() {
- _paletteOpen = true;
- _paletteFilteredSessions = _currentSessions.slice();
- _paletteSelectedIndex = 0;
-
- const palette = $('command-palette');
- if (palette) palette.classList.remove('hidden'); // palette starts with hidden class
-
- renderPaletteList();
-
- const input = $('palette-input');
- if (input) {
- input.value = '';
- input.focus();
- if (_paletteInputListener) {
- input.removeEventListener('input', _paletteInputListener);
- }
- _paletteInputListener = onPaletteInput;
- input.addEventListener('input', _paletteInputListener);
- }
-}
-
-/**
- * Close the command palette.
- * Hides #command-palette and removes the input event listener.
- */
-function closePalette() {
- _paletteOpen = false;
-
- const palette = $('command-palette');
- if (palette) palette.classList.add('hidden');
-
- const input = $('palette-input');
- if (input && _paletteInputListener) {
- input.removeEventListener('input', _paletteInputListener);
- _paletteInputListener = null;
- }
-}
-
-/**
- * Handle input events on #palette-input.
- * Filters sessions by the current query, re-renders the list, resets selection.
- * @param {Event} e
- */
-function onPaletteInput(e) {
- const query = e && e.target ? e.target.value : '';
- _paletteFilteredSessions = filterByQuery(_currentSessions, query);
- _paletteSelectedIndex = 0;
- renderPaletteList();
-}
-
-/**
- * Handle keydown events inside the command palette.
- * ArrowDown/Up moves selection, Enter opens selected session,
- * Escape closes palette, G closes palette + returns to grid,
- * number keys 1-9 jump directly to that item.
- * @param {KeyboardEvent} e
- * @returns {Promise}
- */
-async function handlePaletteKeydown(e) {
- const visibleCount = Math.min(_paletteFilteredSessions.length, PALETTE_MAX_ITEMS);
-
- if (e.key === 'Escape') {
- e.preventDefault();
- closePalette();
- } else if (e.key === 'g' || e.key === 'G') {
- e.preventDefault();
- closePalette();
- await closeSession();
- } else if (visibleCount > 0) {
- if (e.key === 'ArrowDown') {
- e.preventDefault();
- _paletteSelectedIndex = (_paletteSelectedIndex + 1) % visibleCount;
- highlightPaletteItem(_paletteSelectedIndex);
- } else if (e.key === 'ArrowUp') {
- e.preventDefault();
- _paletteSelectedIndex = (_paletteSelectedIndex - 1 + visibleCount) % visibleCount;
- highlightPaletteItem(_paletteSelectedIndex);
- } else if (e.key === 'Enter') {
- e.preventDefault();
- const session = _paletteFilteredSessions[_paletteSelectedIndex];
- if (session) {
- closePalette();
- await openSession(session.name);
- }
- } else if (e.key >= '1' && e.key <= '9') {
- const idx = parseInt(e.key, 10) - 1;
- if (idx < visibleCount) {
- e.preventDefault();
- const session = _paletteFilteredSessions[idx];
- closePalette();
- await openSession(session.name);
- }
- }
- }
-}
-
/**
* Global keydown handler.
- * When palette is open: delegates to handlePaletteKeydown.
- * When in fullscreen with palette closed: backtick or Ctrl+K opens palette,
- * Escape returns to grid.
+ * When in fullscreen: Escape returns to grid.
* @param {KeyboardEvent} e
*/
function handleGlobalKeydown(e) {
- if (_paletteOpen) {
- handlePaletteKeydown(e).catch((err) => console.error('[handleGlobalKeydown]', err));
- return;
- }
-
if (_viewMode === 'fullscreen') {
- if (e.key === '`' || (e.ctrlKey && e.key === 'k')) {
- e.preventDefault();
- openPalette();
- } else if (e.key === 'Escape') {
+ if (e.key === 'Escape') {
e.preventDefault();
closeSession();
}
@@ -1166,8 +993,6 @@ function bindStaticEventListeners() {
on($('sidebar-toggle-btn'), 'click', toggleSidebar);
on($('sidebar-collapse-btn'), 'click', toggleSidebar);
bindSidebarClickAway();
- on($('palette-trigger'), 'click', openPalette);
- on($('palette-backdrop'), 'click', closePalette);
document.addEventListener('keydown', handleGlobalKeydown);
on($('session-pill'), 'click', openBottomSheet);
on($('sheet-backdrop'), 'click', closeBottomSheet);
@@ -1216,36 +1041,6 @@ function _setCurrentSessions(sessions) {
_currentSessions = sessions;
}
-/** Test-only: set _paletteFilteredSessions directly. */
-function _setPaletteFilteredSessions(sessions) {
- _paletteFilteredSessions = sessions;
-}
-
-/** Test-only: get _paletteFilteredSessions. */
-function _getPaletteFilteredSessions() {
- return _paletteFilteredSessions;
-}
-
-/** Test-only: set _paletteSelectedIndex directly. */
-function _setPaletteSelectedIndex(index) {
- _paletteSelectedIndex = index;
-}
-
-/** Test-only: get _paletteSelectedIndex. */
-function _getPaletteSelectedIndex() {
- return _paletteSelectedIndex;
-}
-
-/** Test-only: set _paletteOpen directly. */
-function _setPaletteOpen(val) {
- _paletteOpen = val;
-}
-
-/** Test-only: get _paletteOpen. */
-function _isPaletteOpen() {
- return _paletteOpen;
-}
-
/** Test-only: set _viewMode directly. */
function _setViewMode(mode) {
_viewMode = mode;
@@ -1301,13 +1096,6 @@ if (typeof module !== 'undefined' && module.exports) {
openSession,
closeSession,
_setViewingSession,
- // Command palette
- renderPaletteList,
- highlightPaletteItem,
- openPalette,
- closePalette,
- onPaletteInput,
- handlePaletteKeydown,
handleGlobalKeydown,
bindStaticEventListeners,
openBottomSheet,
@@ -1323,12 +1111,6 @@ if (typeof module !== 'undefined' && module.exports) {
hidePreview,
// Test-only helpers
_setCurrentSessions,
- _setPaletteFilteredSessions,
- _getPaletteFilteredSessions,
- _setPaletteSelectedIndex,
- _getPaletteSelectedIndex,
- _setPaletteOpen,
- _isPaletteOpen,
_setViewMode,
};
}
diff --git a/muxplex/tests/__pycache__/test_frontend_js.cpython-312-pytest-9.0.2.pyc b/muxplex/tests/__pycache__/test_frontend_js.cpython-312-pytest-9.0.2.pyc
new file mode 100644
index 0000000..9468866
Binary files /dev/null and b/muxplex/tests/__pycache__/test_frontend_js.cpython-312-pytest-9.0.2.pyc differ
diff --git a/muxplex/tests/test_frontend_js.py b/muxplex/tests/test_frontend_js.py
new file mode 100644
index 0000000..6137fad
--- /dev/null
+++ b/muxplex/tests/test_frontend_js.py
@@ -0,0 +1,330 @@
+"""Tests for frontend/app.js — verifies palette code removal and handleGlobalKeydown simplification."""
+
+import pathlib
+import re
+
+JS_PATH = pathlib.Path(__file__).parent.parent / "frontend" / "app.js"
+
+# Read once per module — tests are read-only so sharing is safe.
+_JS: str = JS_PATH.read_text()
+
+
+# ── Palette state variables must be removed ──────────────────────────────────
+
+def test_no_palette_max_items_constant() -> None:
+ """PALETTE_MAX_ITEMS constant must be removed."""
+ assert "PALETTE_MAX_ITEMS" not in _JS, (
+ "PALETTE_MAX_ITEMS must be removed from app.js"
+ )
+
+
+def test_no_palette_selected_index_variable() -> None:
+ """_paletteSelectedIndex variable must be removed."""
+ assert "_paletteSelectedIndex" not in _JS, (
+ "_paletteSelectedIndex must be removed from app.js"
+ )
+
+
+def test_no_palette_filtered_sessions_variable() -> None:
+ """_paletteFilteredSessions variable must be removed."""
+ assert "_paletteFilteredSessions" not in _JS, (
+ "_paletteFilteredSessions must be removed from app.js"
+ )
+
+
+def test_no_palette_open_variable() -> None:
+ """_paletteOpen variable must be removed."""
+ assert "_paletteOpen" not in _JS, (
+ "_paletteOpen must be removed from app.js"
+ )
+
+
+def test_no_palette_input_listener_variable() -> None:
+ """_paletteInputListener variable must be removed."""
+ assert "_paletteInputListener" not in _JS, (
+ "_paletteInputListener must be removed from app.js"
+ )
+
+
+# ── Palette functions must be removed ────────────────────────────────────────
+
+def test_no_render_palette_list_function() -> None:
+ """renderPaletteList function must be removed."""
+ assert "renderPaletteList" not in _JS, (
+ "renderPaletteList must be removed from app.js"
+ )
+
+
+def test_no_highlight_palette_item_function() -> None:
+ """highlightPaletteItem function must be removed."""
+ assert "highlightPaletteItem" not in _JS, (
+ "highlightPaletteItem must be removed from app.js"
+ )
+
+
+def test_no_open_palette_function() -> None:
+ """openPalette function must be removed."""
+ assert "openPalette" not in _JS, (
+ "openPalette must be removed from app.js"
+ )
+
+
+def test_no_close_palette_function() -> None:
+ """closePalette function must be removed."""
+ assert "closePalette" not in _JS, (
+ "closePalette must be removed from app.js"
+ )
+
+
+def test_no_on_palette_input_function() -> None:
+ """onPaletteInput function must be removed."""
+ assert "onPaletteInput" not in _JS, (
+ "onPaletteInput must be removed from app.js"
+ )
+
+
+def test_no_handle_palette_keydown_function() -> None:
+ """handlePaletteKeydown function must be removed."""
+ assert "handlePaletteKeydown" not in _JS, (
+ "handlePaletteKeydown must be removed from app.js"
+ )
+
+
+# ── handleGlobalKeydown must be simplified ───────────────────────────────────
+
+def test_handle_global_keydown_exists() -> None:
+ """handleGlobalKeydown function must exist."""
+ assert "function handleGlobalKeydown" in _JS, (
+ "handleGlobalKeydown must still exist in app.js"
+ )
+
+
+def test_handle_global_keydown_no_palette_open_check() -> None:
+ """handleGlobalKeydown must not check _paletteOpen."""
+ # Extract the function body
+ match = re.search(
+ r"function handleGlobalKeydown\s*\(e\)\s*\{(.*?)\n\}",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "handleGlobalKeydown function not found"
+ body = match.group(1)
+ assert "_paletteOpen" not in body, (
+ "handleGlobalKeydown must not reference _paletteOpen"
+ )
+
+
+def test_handle_global_keydown_no_open_palette_call() -> None:
+ """handleGlobalKeydown must not call openPalette."""
+ match = re.search(
+ r"function handleGlobalKeydown\s*\(e\)\s*\{(.*?)\n\}",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "handleGlobalKeydown function not found"
+ body = match.group(1)
+ assert "openPalette" not in body, (
+ "handleGlobalKeydown must not call openPalette"
+ )
+
+
+def test_handle_global_keydown_handles_escape_in_fullscreen() -> None:
+ """handleGlobalKeydown must call closeSession() on Escape in fullscreen mode."""
+ match = re.search(
+ r"function handleGlobalKeydown\s*\(e\)\s*\{(.*?)\n\}",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "handleGlobalKeydown function not found"
+ body = match.group(1)
+ assert "fullscreen" in body, "handleGlobalKeydown must check for fullscreen mode"
+ assert "Escape" in body, "handleGlobalKeydown must handle Escape key"
+ assert "closeSession" in body, "handleGlobalKeydown must call closeSession"
+
+
+# ── bindStaticEventListeners must have no palette references ─────────────────
+
+def test_bind_static_event_listeners_no_palette_trigger() -> None:
+ """bindStaticEventListeners must not bind palette-trigger click."""
+ match = re.search(
+ r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "bindStaticEventListeners function not found"
+ body = match.group(1)
+ assert "palette-trigger" not in body, (
+ "bindStaticEventListeners must not bind palette-trigger click"
+ )
+
+
+def test_bind_static_event_listeners_no_palette_backdrop() -> None:
+ """bindStaticEventListeners must not bind palette-backdrop click."""
+ match = re.search(
+ r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "bindStaticEventListeners function not found"
+ body = match.group(1)
+ assert "palette-backdrop" not in body, (
+ "bindStaticEventListeners must not bind palette-backdrop click"
+ )
+
+
+# ── Palette test-only helpers must be removed ─────────────────────────────────
+
+def test_no_set_palette_filtered_sessions_helper() -> None:
+ """_setPaletteFilteredSessions test helper must be removed."""
+ assert "_setPaletteFilteredSessions" not in _JS, (
+ "_setPaletteFilteredSessions must be removed from app.js"
+ )
+
+
+def test_no_get_palette_filtered_sessions_helper() -> None:
+ """_getPaletteFilteredSessions test helper must be removed."""
+ assert "_getPaletteFilteredSessions" not in _JS, (
+ "_getPaletteFilteredSessions must be removed from app.js"
+ )
+
+
+def test_no_set_palette_selected_index_helper() -> None:
+ """_setPaletteSelectedIndex test helper must be removed."""
+ assert "_setPaletteSelectedIndex" not in _JS, (
+ "_setPaletteSelectedIndex must be removed from app.js"
+ )
+
+
+def test_no_get_palette_selected_index_helper() -> None:
+ """_getPaletteSelectedIndex test helper must be removed."""
+ assert "_getPaletteSelectedIndex" not in _JS, (
+ "_getPaletteSelectedIndex must be removed from app.js"
+ )
+
+
+def test_no_set_palette_open_helper() -> None:
+ """_setPaletteOpen test helper must be removed."""
+ assert "_setPaletteOpen" not in _JS, (
+ "_setPaletteOpen must be removed from app.js"
+ )
+
+
+def test_no_is_palette_open_helper() -> None:
+ """_isPaletteOpen test helper must be removed."""
+ assert "_isPaletteOpen" not in _JS, (
+ "_isPaletteOpen must be removed from app.js"
+ )
+
+
+# ── module.exports must not include palette exports ───────────────────────────
+
+def test_exports_no_render_palette_list() -> None:
+ """module.exports must not export renderPaletteList."""
+ match = re.search(
+ r"module\.exports\s*=\s*\{(.*?)\};",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "module.exports block not found"
+ exports = match.group(1)
+ assert "renderPaletteList" not in exports, (
+ "module.exports must not export renderPaletteList"
+ )
+
+
+def test_exports_no_highlight_palette_item() -> None:
+ """module.exports must not export highlightPaletteItem."""
+ match = re.search(
+ r"module\.exports\s*=\s*\{(.*?)\};",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "module.exports block not found"
+ exports = match.group(1)
+ assert "highlightPaletteItem" not in exports, (
+ "module.exports must not export highlightPaletteItem"
+ )
+
+
+def test_exports_no_open_palette() -> None:
+ """module.exports must not export openPalette."""
+ match = re.search(
+ r"module\.exports\s*=\s*\{(.*?)\};",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "module.exports block not found"
+ exports = match.group(1)
+ assert "openPalette" not in exports, (
+ "module.exports must not export openPalette"
+ )
+
+
+def test_exports_no_close_palette() -> None:
+ """module.exports must not export closePalette."""
+ match = re.search(
+ r"module\.exports\s*=\s*\{(.*?)\};",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "module.exports block not found"
+ exports = match.group(1)
+ assert "closePalette" not in exports, (
+ "module.exports must not export closePalette"
+ )
+
+
+def test_exports_no_on_palette_input() -> None:
+ """module.exports must not export onPaletteInput."""
+ match = re.search(
+ r"module\.exports\s*=\s*\{(.*?)\};",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "module.exports block not found"
+ exports = match.group(1)
+ assert "onPaletteInput" not in exports, (
+ "module.exports must not export onPaletteInput"
+ )
+
+
+def test_exports_no_handle_palette_keydown() -> None:
+ """module.exports must not export handlePaletteKeydown."""
+ match = re.search(
+ r"module\.exports\s*=\s*\{(.*?)\};",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "module.exports block not found"
+ exports = match.group(1)
+ assert "handlePaletteKeydown" not in exports, (
+ "module.exports must not export handlePaletteKeydown"
+ )
+
+
+def test_exports_still_has_handle_global_keydown() -> None:
+ """module.exports must still export handleGlobalKeydown."""
+ match = re.search(
+ r"module\.exports\s*=\s*\{(.*?)\};",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "module.exports block not found"
+ exports = match.group(1)
+ assert "handleGlobalKeydown" in exports, (
+ "module.exports must still export handleGlobalKeydown"
+ )
+
+
+def test_exports_still_has_bind_static_event_listeners() -> None:
+ """module.exports must still export bindStaticEventListeners."""
+ match = re.search(
+ r"module\.exports\s*=\s*\{(.*?)\};",
+ _JS,
+ re.DOTALL,
+ )
+ assert match, "module.exports block not found"
+ exports = match.group(1)
+ assert "bindStaticEventListeners" in exports, (
+ "module.exports must still export bindStaticEventListeners"
+ )