Commit Graph

536 Commits

Author SHA1 Message Date
Brian Krabach 03bf24e8d4 refactor(views): introduce operation layer with pure and user-intent ops (Phase 2)
Backend (muxplex/views.py):
- Added five pure data operations that mutate settings dict in place:
  add_membership, remove_membership, remove_from_all_views, hide, unhide.
  Each operation is isolated and affects only its named field.
- Added 16 new tests under 'Pure data operations (Phase 2)' in test_views.py.
  Isolation verified: each operation only modifies its target field.

Frontend (muxplex/frontend/app.js):
- Added five pure ops (_opAddMembership, _opRemoveMembership,
  _opRemoveFromAllViews, _opHide, _opUnhide) and _cloneOpState helper.
- Added four user-intent operations with intentional asymmetry:
  * hideSessionOp = hide + removeFromAllViews (federation-safe;
    matches current UX; see design doc for rationale)
  * unhideSessionOp = unhide (orthogonal; does NOT change view membership)
  * addSessionToViewOp = unhide + addMembership (auto-unhide on add;
    expressed as explicit composition)
  * removeSessionFromViewOp = removeMembership (orthogonal; does NOT hide)
- Refactored 7 call sites to use user-intent ops: _doHideSession,
  _doUnhideSession, _doRemoveFromView, flyout submenu toggle, 'New View'
  flow, mobile picker toggle, Manage View checkbox.
- All new operations exported in module.exports.

Frontend tests (muxplex/frontend/tests/test_app.mjs):
- Added 30 new tests under 'Phase 2 operation layer'.
- Cover pure-op isolation, idempotency, no-op behavior, user-intent op
  composition, and guarantee that _serverSettings is never mutated.

Verification:
- muxplex/tests/: 1239 passed (1223 prior + 16 new pure-op tests)
- test_app.mjs: 372 passed, 10 pre-existing failures (Phase 1 had 21;
  Phase 2 reduces to 10; none Phase-2-related; all test unrelated details
  of createNewSession, renderFilterBar, loadGridViewMode)

Intentional inline-PATCH constructions (NOT regressions):
- Creating empty views (~1263, 1347, 1595)
- Moving views up/down in Settings tab (~2520, 2577)
- _saveViewsAndRerender (full views array persistence)
- createNewSession auto-add-to-active-view (session-creation side effect)
These are view-management operations, not session-in-view management.

Builds on Phase 0+1 (commit 0f9623d).
No behavior change — purely structural refactor.

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-05-17 10:10:05 -07:00
Brian Krabach 0f9623d2c2 refactor(views): introduce visibility helper and schema version (Phases 0+1)
## Phase 0 — Schema Version Field

- Added SCHEMA_VERSION = 2 constant and _schema_version field to DEFAULT_SETTINGS
  in muxplex/settings.py to support versioned federation.
- Added _schema_version to SYNCABLE_KEYS so peers see the version but
  apply_synced_settings never accepts an incoming version (one-way: send only).
- save_settings() always clamps _schema_version to current SCHEMA_VERSION.
- Added peer_supports_v2() helper for federation handshake.
- 6 new tests under "Schema version (Phase 0)" in test_settings.py.

## Phase 1 — Backend & Frontend Visibility Helpers

Backend (muxplex/views.py):
- Added is_hidden(key, settings), filter_visible(sessions, settings, view,
  *, include_hidden=False), visible_count(...), and normalize_session_keys()
  to provide read-time visibility filtering.
- Updated module docstring to describe v2 semantics (hidden is a property,
  not a placement; legacy enforce_mutual_exclusion retained as v1 backstop).
- 22 new tests covering the full filter matrix and normalization edge cases.

Frontend (muxplex/frontend/app.js):
- Added isHidden, filterVisible, visibleCount to app.js (pre-ES6 idioms).
- Replaced getVisibleSessions body with thin wrapper around filterVisible.
- Replaced 8 raw .length count sites in dropdown, settings, and Manage View
  to route through visibleCount.
- Settings panel shows "N sessions (M hidden)" when M > 0.
- Manage View "in this view" count uses includeHidden: true.
- 17 new tests in test_app.mjs covering the same matrix as backend.
- Updated 5 stale tests in test_frontend_js.py.

## Additional Updates

- Updated test_readme.py to exempt internal underscore-prefixed setting keys
  from README documentation requirement.
- Updated docs/plans/2026-05-17-hidden-state-redesign-design.md with COE
  corrections and design notes (federation truth, Phase 3 deferral, schema
  version semantics, local-only pruning state, federation tests).

All 1223 tests in muxplex/tests/ pass. All 17 new JS tests pass.
No raw .length count sites remain in counting code paths.

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-05-17 09:33:46 -07:00
Brian Krabach 64915777be chore: bump version to 0.5.0 2026-05-06 12:37:34 -07:00
Brian Krabach 33ea016f7e feat(tls): add --method ca for persistent local CA-signed leaves
Adds a new TLS certificate method ('ca') that generates a persistent local
Certificate Authority and signs leaf certificates against it. This allows
users to install the CA once on client devices and have browser-trusted
HTTPS for plain LAN names (my-host, 192.168.1.5, my-host.local) without
requiring external services like Tailscale.

Key improvements over existing --method selfsigned:
- Persistent CA: the root cert is stored separately and never rotates,
  so leaf certificate renewal doesn't require re-trusting on clients
- Auto-detected SANs: includes LAN IP, tailnet name (if applicable),
  hostname, and localhost variants
- Per-platform install guide: comprehensive docs/TRUSTING_THE_LOCAL_CA.md
  with Windows PowerShell, macOS/Linux, iOS, and Android install steps

Solves PWA installation issues on Windows machines with corporate IT
policy blocking Tailscale: PWAs now persist in standalone mode when the
local CA is trusted in the Windows user cert store.

Changes:
- muxplex/tls.py: added _default_lan_ip(), _default_tailnet_name(),
  generate_local_ca(), generate_leaf_signed_by_ca()
- muxplex/cli.py: added 'ca' to --method choices, wired setup_tls()
  to generate CA + leaf with auto-detected SAN
- docs/TRUSTING_THE_LOCAL_CA.md: comprehensive per-platform install guide
- README.md: added --method ca documentation and cross-links
- CHANGELOG.md: documented v0.5.0 features

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-05-06 12:37:34 -07:00
Brian Krabach abe6a97241 fix: use ResizeObserver for terminal refit after sidebar toggle
The previous transitionend approach (v0.4.5) had a { once: true } bug:
the sidebar transitions two CSS properties (width and min-width), and if
min-width fired first, the listener self-destructed before the width event
arrived — so _refitTerminal() was never called.

Replace the fragile event-based approach with a ResizeObserver on
#terminal-container. This automatically calls _fitAddon.fit() (debounced
50ms) whenever the container dimensions change, handling sidebar toggle,
browser resize, and any future layout change. Remove the now-unnecessary
window._refitTerminal global and the broken transitionend handler.

Bumps version to 0.4.6.
2026-05-02 07:45:11 -07:00
Brian Krabach 538d11165c fix: refit terminal after sidebar toggle animation completes
toggleSidebar() flipped the sidebar--collapsed CSS class but never told
xterm.js to recalculate its dimensions. The terminal canvas kept its old
column/row count until a new session was opened (which creates a fresh
terminal that measures correctly on first fit()).

Fix: expose window._refitTerminal() from terminal.js, then call it via
a transitionend listener in toggleSidebar() so the terminal refits once
the 250ms CSS width/transform transition finishes.

Bumps version to 0.4.5.
2026-05-02 07:08:18 -07:00
Brian Krabach 493eed5a60 test: update renderSidebar regex to match 3-parameter signature
The test regexes matched `renderSidebar(\w+,\s*\w+)` (exactly 2 params)
but the fix in 821c595 added a third `currentRemoteId` parameter.
Updated to use `\(.*?\)` which matches any parameter count.

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-05-02 06:57:20 -07:00
Brian Krabach 821c595abe fix: resolve session switching failure when same-named sessions exist on different devices
The sidebar click guard, active highlight, mobile bottom sheet, and kill-session
auto-close all compared sessions by name only, ignoring remoteId. This caused
same-named sessions on different devices to be treated as identical — clicking
one while viewing the other was silently discarded as a no-op.

Now all 6 comparison sites check both name AND remoteId, using the _viewingRemoteId
that was already being tracked but never used in guards.

Bumps version to 0.4.4.
2026-05-02 06:55:39 -07:00
Brian Krabach 3538ecf48d chore: bump version to 0.4.3 2026-04-16 12:12:00 -07:00
Brian Krabach 2aa483554f fix: update stale test assertions — version check, remoteId routing 2026-04-16 12:11:56 -07:00
Brian Krabach b9144d9ae5 chore: bump version to 0.4.2 2026-04-16 08:58:31 -07:00
Brian Krabach 769621a009 feat: add flyout menu to sidebar session items
- Add data-session-key attribute to buildSidebarHTML article element so
  openFlyoutMenu() can read session info via closest('[data-session-key]')
- Add tile-options-btn button (with aria-label and aria-haspopup) inside
  sidebar-item-header — reuses the same class as tile version so the
  existing delegated document click handler picks it up automatically
- Guard renderSidebar click handler against .tile-options-btn clicks so
  clicking ⋮ doesn't also trigger openSession()
- Add .sidebar-item-header .tile-options-btn CSS: compact 20×20px, subtle
  opacity-based visibility to fit the narrower sidebar layout
- Tests: buildSidebarHTML data-session-key, tile-options-btn presence/aria,
  renderSidebar click guard, CSS sidebar btn styling
2026-04-16 08:58:05 -07:00
Brian Krabach 600f34ebe0 chore: bump version to 0.4.1 2026-04-16 08:34:52 -07:00
Brian Krabach a4271bb89c fix: local sessions 404 — use remoteId (not deviceId) for routing attribute
buildTileHTML and buildSidebarHTML were using session.deviceId for the
data-remote-id attribute. Since deviceId is now non-null for ALL sessions
(including local), openSession() routed local sessions through federation
endpoints which returned 404. Fixed to use session.remoteId which is null
for local sessions, preserving the local vs federation routing distinction.
2026-04-16 08:34:46 -07:00
Brian Krabach d90f9cca30 chore: bump version to 0.4.0 2026-04-16 08:22:08 -07:00
Brian Krabach c910eca6a6 feat: manage view rename/delete, unified views submenu, stable checkbox list, dead code cleanup
Fix 1: Wire rename click handler in Manage View panel
- openManageViewPanel() now adds click handler on #manage-view-name
- Replaces h2 with inline manage-view-panel__name-input on click
- On Enter: validates (non-empty, max 30, not reserved, not duplicate)
  then PATCHes /api/settings, updates _activeView, re-renders panel
- On Escape / blur: reverts to text display
- Add CSS for .manage-view-panel__delete-btn and .manage-view-panel__confirm-text

Fix 2: Add delete button in Manage View panel header
- Adds trash button (id=manage-view-delete-btn) next to view name
- Clicking shows inline 'Delete this view? [Yes] [No]' confirmation
- On confirm: PATCH /api/settings to remove view, close panel,
  switch to 'all' view, show toast confirming deletion

Fix 3: Remove rename affordance from settings tab
- Remove cursor:pointer from .views-settings-name
- Remove .views-settings-name:hover hover/underline styles
- Remove .views-settings-rename-input CSS class (dead code)

Fix 4: Merge Add to View + Remove from View into unified Views submenu
- Remove 'remove-from-view' item from FLYOUT_MENU_MAP 'user' entry
- _openFlyoutSubmenu now shows ALL user views (no longer skips
  the current active view) — unified toggle-checkmark submenu
- Submenu already stays open during toggling (existing behavior)
- Update old test to match new correct behavior

Fix 5: Fix checkbox re-render layout thrash in Manage View panel
- renderManageViewList onChange .then() no longer calls renderManageViewList()
- Instead updates summaryEl.textContent in-place with current counts
- Prevents position jumps when toggling checkboxes in long lists

Fix 6: Clean up dead CSS
- Remove .add-sessions-tile + __icon + __label (old affordance tile)
- Remove .manage-view-panel__close (HTML uses __close-btn)
- Remove .manage-view-panel__title (HTML uses __name)
- Update CSS comment 'Add Sessions Panel' -> 'Manage View Panel'
- Keep .manage-view-panel__name-input (now used by Fix 1 rename input)
2026-04-16 08:11:48 -07:00
Brian Krabach 29b180a713 feat: UX overhaul — Manage View panel, dropdown refinements, label fixes, badge sizing 2026-04-16 06:56:59 -07:00
Brian Krabach 95371712f2 fix: UX refinements from live testing — sidebar new view, session counts, empty view flow, header add button, tile layout
Issue 1: Add '+ New View' to sidebar dropdown
- Add '+ New View' action button (data-action='new-view') to renderSidebarViewDropdown()
- Add showSidebarNewViewInput() function targeting #sidebar-view-dropdown-menu
- Wire sidebar dropdown click handler to call showSidebarNewViewInput()
- Also wire 'Manage Views' action in sidebar dropdown

Issue 2: Remove keyboard shortcut numbers, add session counts
- Remove <span class='view-dropdown__shortcut'> elements from both render functions
- Show count of non-hidden sessions next to 'All Sessions'
- Show each user view's session count (view.sessions.length) in parentheses
- Keep keyboard shortcuts functional (backtick, 1-9) — just remove visual display

Issue 3: Empty new view opens Add Sessions panel
- Call openAddSessionsPanel() after switchView() in showNewViewInput() Enter handler
- Same in showSidebarNewViewInput() — user immediately sees panel to populate new view

Issue 4: Add '+ New View' option to tile flyout submenu
- Add separator + '+ New View' button at bottom of _openFlyoutSubmenu() view list
- Click handler: close flyout, prompt for name, validate, create view with session, switchView
- Remove early exit when views.length === 0 (show New View option even with no views)

Issue 5: Fix openAddSessionsPanel entry point + move to header
- Remove broken add-sessions-tile with onclick='window.MuxplexApp.openAddSessionsPanel()'
- Add <button id='add-sessions-btn'> to header-actions in index.html (hidden by default)
- Add updateAddSessionsButton() to show/hide button based on active view
- Wire button click to openAddSessionsPanel() in bindStaticEventListeners()
- Call updateAddSessionsButton() from switchView() and DOMContentLoaded

Issue 6: Fix device badge overlapping flyout icon
- Move tile-options-btn inside tile-header as last flex item (was absolute sibling of article)
- Move device badge out of tile-meta, now a direct flex sibling between tile-name and tile-meta
- Remove tile-meta-sep separator (badge no longer in meta)
- Add gap: 4px to .tile-header CSS
- Remove position:absolute from .tile-options-btn CSS — flex-shrink:0 instead

Tests: Add 17 new static analysis tests in test_frontend_js.py for all 6 issues.
Update test_app.mjs tile tests to match new tile-header layout.
All 318 frontend tests + 224 backend tests pass.
2026-04-16 03:53:15 -07:00
Brian Krabach f9b96ec077 fix: prevent click-outside from closing dropdown during new-view input creation
Race condition: showNewViewInput() calls replaceChild() to swap the
'+ New View' button for an <input>. The click event then bubbles to the
document-level click-outside handler where e.target is the removed button,
no longer in the DOM. dropdown.contains(e.target) returns false, so
closeViewDropdown() fires immediately and the input disappears.

Fix: before closing, check whether the dropdown now contains a
.view-dropdown__new-input element. If it does, showNewViewInput() just
ran and we must not close the dropdown.

Test: added test_click_outside_view_dropdown_guards_against_new_view_input
to verify the guard is present in the handler.
2026-04-16 03:17:20 -07:00
Brian Krabach e2c27cfe99 fix: update test_app.mjs assertions for tile-options-btn and sidebar-delete removal 2026-04-15 22:37:37 -07:00
Brian Krabach 3200896e6c fix: flyout sheet title CSS, ARIA role correction, sidebar-delete cleanup
Fix A: Add .flyout-sheet__title CSS rule in style.css flyout-sheet section.
  - Kill confirm sheet renders a title div with class flyout-sheet__title
    but no CSS rule existed; title displayed as raw unstyled text.
  - Added padding/font/color/alignment rule after .flyout-sheet__handle.

Fix B: Change role='menuitem' to role='button' on Kill and Cancel buttons
  in _openMobileKillConfirm() (app.js).
  - Buttons inside a role='alertdialog' must use role='button' per ARIA spec;
    role='menuitem' is only valid inside role='menu'.

Fix C: Remove 3 orphaned .sidebar-delete CSS rule blocks from style.css.
  - .sidebar-delete, .sidebar-item:hover .sidebar-delete, .sidebar-delete:hover
    were dead code after the delete button was removed from buildSidebarHTML().

Tests added:
  - test_frontend_css.py: test_flyout_sheet_title_css_exists (Fix A)
  - test_frontend_js.py:  test_kill_confirm_buttons_use_role_button (Fix B)
  - test_frontend_css.py: test_no_sidebar_delete_css (Fix C)
2026-04-15 22:30:40 -07:00
Brian Krabach 8e53dbe1cf fix: address Phase 3 COE verification findings — tile click guard, disclosure visibility, mobile confirm, dead code cleanup 2026-04-15 21:57:38 -07:00
Brian Krabach 2d136f1ff7 feat: close flyout when targeted session disappears 2026-04-15 21:25:13 -07:00
Brian Krabach 95bfc36fd8 feat: add 'Add Sessions' affordance tile in user views grid 2026-04-15 21:24:26 -07:00
Brian Krabach 3fe6cf2de6 feat: add mobile bottom action sheet for flyout menu 2026-04-15 21:23:08 -07:00
Brian Krabach f9c7cc9c6e feat: add Add Sessions panel JS logic with immediate-commit checkboxes 2026-04-15 21:18:50 -07:00
Brian Krabach 1167c8df7a feat: add Add Sessions panel HTML and CSS 2026-04-15 21:13:25 -07:00
Brian Krabach e18f08a320 feat: add inline kill confirmation in flyout, remove old confirm() dialog 2026-04-15 21:07:08 -07:00
Brian Krabach 51770b961e feat: add hide, unhide, and remove-from-view flyout actions 2026-04-15 21:05:15 -07:00
Brian Krabach 1d5773ba22 feat: add flyout click handler, Add to View submenu with toggle 2026-04-15 21:03:47 -07:00
Brian Krabach e0700ef04d feat: add FLYOUT_MENU_MAP data map and _buildFlyoutMenuItems builder 2026-04-15 21:02:05 -07:00
Brian Krabach 5c836c6108 feat: add flyout menu open/close/position base, replace old tile-delete handler 2026-04-15 20:56:13 -07:00
Brian Krabach 66f62321f5 feat: replace tile-delete button with ⋮ options button on session tiles 2026-04-15 20:53:48 -07:00
Brian Krabach bbe4f2d3f7 feat: add flyout menu, submenu, and mobile bottom sheet CSS 2026-04-15 20:52:49 -07:00
Brian Krabach 30f61d159d fix: address Phase 2 COE verification findings — dropdown styling, ARIA, sidebar clipping, manage-views wiring, persistence
Fix 1: BEM class names — renderViewDropdown/renderSidebarViewDropdown now emit
view-dropdown__item, __separator, __shortcut, __action, __count matching CSS

Fix 2: role=menuitem — all buttons in both dropdown renderers now carry
role="menuitem" so handleGlobalKeydown arrow-key navigation works

Fix 3: Sidebar dropdown clipping — #sidebar-view-dropdown-menu changed to
position:fixed; toggleSidebarViewDropdown uses getBoundingClientRect() to
escape .session-sidebar { overflow:hidden } clipping

Fix 4: manage-views wiring — bindStaticEventListeners now calls
openSettings() + switchSettingsTab('views') instead of falling through to
closeViewDropdown only

Fix 5: active_view persistence — delete and rename paths in
renderViewsSettingsTab now PATCH /api/state when _activeView changes

Fix 6: Sidebar dropdown click-outside — second document click listener
closes sidebar-view-dropdown-menu on outside clicks

Fix 7: Case-insensitive reserved names — showNewViewInput and commitRename
use name.toLowerCase() for 'all'/'hidden' check

Fix 8: Rename input maxLength — views-settings-rename-input now has
maxLength=30 matching the creation input

Fix 9: Local device_id — added _localDeviceId variable, fetched from
/api/instance-info at startup; createNewSession uses _localDeviceId instead
of _serverSettings.device_id (which is not in /api/settings response)

Fix 10: Dead code — renderFilterBar body emptied; dead filter-bar click
handler removed from bindStaticEventListeners

Tests: 24 new tests added covering all 10 findings; 1070 total tests pass
2026-04-15 20:36:16 -07:00
Brian Krabach 3b8543ff44 feat: remove hidden sessions section from settings panel (task-12)
Remove the Hidden Sessions checkbox list from the Sessions settings tab.
The feature is superseded by the Hidden view + tile flyout (Phase 3),
which is accessible via the sidebar view dropdown.

Changes:
- muxplex/frontend/index.html: Remove #setting-hidden-sessions div block
- muxplex/frontend/app.js: Remove hidden sessions checkbox population
  block from openSettings() and delegated change handler from
  bindStaticEventListeners()
- muxplex/tests/test_frontend_html.py: Remove old
  test_html_sessions_panel_has_hidden_sessions_container (for removed
  element); add test_no_hidden_sessions_checkbox_list_in_settings
  asserting element no longer exists
- muxplex/tests/test_frontend_js.py: Remove
  test_bind_static_event_listeners_uses_delegated_handler_for_hidden_sessions
  and test_settings_hidden_sessions_uses_session_key (for removed JS code)
2026-04-15 19:19:09 -07:00
Brian Krabach d34509681e feat: remove filtered gridViewMode rendering code and option (task-11)
- Remove 'Filtered' option from HTML view mode select in index.html
- Add test_no_active_filter_device_in_render_grid to test_frontend_js.py
- Add test_no_filtered_option_in_view_mode_select to test_frontend_html.py
- All 3 acceptance criteria tests pass

The renderGrid() function was already clean (no filtered mode checks,
no _activeFilterDevice references) from prior cleanup work. The
loadGridViewMode() and _setGridViewMode() guards were also already in
place. The remaining task was to remove the HTML <option value="filtered">
and add the missing tests.
2026-04-15 19:03:47 -07:00
Brian Krabach 9c2abe73a1 feat: add sidebar view switcher dropdown in fullscreen mode (task-10)
- HTML: Replace .sidebar-title in sidebar-header with a view dropdown
  containing #sidebar-view-dropdown-trigger and #sidebar-view-dropdown-menu
- JS: Add renderSidebarViewDropdown() for populating sidebar menu (no action buttons)
- JS: Add toggleSidebarViewDropdown() for open/close with aria-expanded
- JS: Update switchView() to also update #sidebar-view-label in sidebar
- JS: Bind sidebar trigger click and delegated menu click in bindStaticEventListeners()
- CSS: Add .sidebar-view-dropdown, .sidebar-view-trigger, .sidebar-view-trigger:hover,
  and #sidebar-view-dropdown-menu position overrides
- Tests: Add test_sidebar_view_dropdown_exists and test_sidebar_view_dropdown_menu_exists
- Tests: Update test_html_session_sidebar_structure to reflect new dropdown structure
2026-04-15 18:43:50 -07:00
Brian Krabach 11326764e7 fix: remove dead sessions variable and add committed guard to prevent double-commit in commitRename
- Remove unused 'sessions' variable in renderViewsSettingsTab() forEach loop
  (was declared but never used; sessionCount already derived from view.sessions directly)
- Add 'committed' flag to commitRename() closure to prevent duplicate PATCH
  requests when Enter-triggered DOM teardown fires blur on the still-live input
  (pattern mirrors the 150ms guard already used in showNewViewInput())
2026-04-15 18:33:43 -07:00
Brian Krabach 2c04c2600a feat: add Views settings tab with inline rename, reorder, and delete
Implements the "Manage Views" Settings Tab with full UI and functionality:

HTML: Added Views tab button and panel with empty state message.
JS: Added renderViewsSettingsTab() function that renders the view list with:
- Clickable name for inline rename (validates reserved/duplicate names)
- Session count display
- Up/down arrow buttons for reordering (disabled at boundaries)
- Delete button with inline 'Sure? [Yes] [No]' confirmation
- Delegated event handlers for all interactions
Added _saveViewsAndRerender() helper that PATCHes /api/settings,
updates _serverSettings, and re-renders both tab and dropdown.
Updated openSettings() to call renderViewsSettingsTab() after loading.
CSS: Added styling for views-settings-list, views-settings-row,
views-settings-name, views-settings-count, views-settings-actions,
views-settings-btn, views-settings-confirm, and rename input.
Tests: Added 3 new frontend tests and updated existing panel count test.

All 491 frontend tests pass with no regressions.

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-04-15 18:23:56 -07:00
Brian Krabach 5732436f88 feat: add showNewViewInput inline creation flow for New View dropdown
- Add showNewViewInput() function after closeViewDropdown() in app.js
  - Creates inline text input replacing '+ New View' button in dropdown
  - Input: type=text, class=view-dropdown__new-input, placeholder='View name',
    maxLength=30, aria-label='New view name'
  - Re-focuses existing input if already present (prevents duplicates)
  - On Enter: validates name (non-empty, non-reserved, non-duplicate),
    PATCHes /api/settings with updated views, switches to new view on success
  - On Escape: closes the dropdown
  - On blur: closes dropdown after 150ms delay if input not focused
- Update bindStaticEventListeners to call showNewViewInput() for data-action='new-view'
  instead of just closeViewDropdown()
- Export showNewViewInput in module.exports block

Tests added:
- test_show_new_view_input_function_exists: verifies function exists in app.js
- test_show_new_view_input_patches_settings: verifies PATCH /api/settings with views

Task: task-8
2026-04-15 18:08:43 -07:00
Brian Krabach a891fada46 feat: keyboard shortcuts — backtick, number keys, arrow navigation in handleGlobalKeydown
- Backtick (Backquote) toggles view dropdown in grid mode only (not fullscreen)
- Number keys 1-9 directly switch views (1=all, 9=hidden, 2-8=user views)
- Arrow keys navigate within open dropdown using [role="menuitem"] elements
- Enter activates the focused dropdown item
- Escape closes open dropdown when one is visible
- Shortcuts disabled when typing in INPUT/TEXTAREA/SELECT
- Input guard applied globally (not just for comma shortcut)

Tests added:
- test_handle_global_keydown_has_backtick_handler
- test_handle_global_keydown_has_number_key_shortcuts
- test_backtick_only_on_grid_not_fullscreen

All 229 tests pass (226 existing + 3 new)
2026-04-15 18:00:53 -07:00
Brian Krabach 9180fbbfa9 fix: remove escapeHtml() from textContent assignment on view label
textContent is inherently XSS-safe and does not interpret HTML entities.
Wrapping with escapeHtml() caused literal entity strings (e.g., &amp;)
to display instead of the intended characters (e.g., &) for view names
containing special characters like &, <, or >.

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-04-15 17:54:39 -07:00
Brian Krabach 9afa4b2347 feat: task-6 header dropdown view switching (renderViewDropdown, toggleViewDropdown, closeViewDropdown, switchView)
- Add renderViewDropdown() populating #view-dropdown-menu with All Sessions,
  user views, Hidden (count), + New View, Manage Views; updates label
- Add toggleViewDropdown() toggling hidden class and aria-expanded
- Add closeViewDropdown() hiding menu and cleaning up inline inputs
- Add switchView(viewName) setting _activeView, re-rendering, persisting
  active_view via PATCH /api/state (fire-and-forget)
- Restore _activeView from server state in restoreState()
- Bind view dropdown event listeners in bindStaticEventListeners():
  trigger click, delegated menu item clicks, click-outside dismiss
- Export all 4 new functions in module.exports
- Add 5 new tests: render_view_dropdown_function_exists,
  render_view_dropdown_exported, toggle_view_dropdown_function_exists,
  switch_view_function_exists, switch_view_patches_state
2026-04-15 17:46:01 -07:00
Brian Krabach 8ed0e94f80 feat: add view dropdown CSS styles (task-5)
Add CSS styles for the header view dropdown component:
- .view-dropdown wrapper with relative positioning and flex layout
- .view-dropdown__trigger with transparent border, 6px radius, transitions
- .view-dropdown__trigger:hover and [aria-expanded=true] states
- .view-dropdown__caret with 10px font and muted color
- .view-dropdown__menu absolutely positioned below trigger, z-index: 100
- .view-dropdown__item with flex layout, text-overflow ellipsis
- .view-dropdown__item:hover/:focus-visible with bg-surface
- .view-dropdown__item--active with accent color and checkmark pseudo-element
- .view-dropdown__shortcut right-aligned with monospace font
- .view-dropdown__separator as 1px height divider
- .view-dropdown__action and :hover states
- .view-dropdown__new-input with accent border
- .view-dropdown__count with dim color

Add 3 TDD tests: test_view_dropdown_trigger_styled,
test_view_dropdown_menu_styled, test_view_dropdown_item_styled
2026-04-15 17:34:08 -07:00
Brian Krabach 7f42d6cbe2 feat: add view dropdown trigger and menu to app header (task-4)
- Add #view-dropdown-trigger button between wordmark and header-actions
- Add #view-dropdown-menu container with role='menu' and hidden class
- Add #view-dropdown-label with default text 'All Sessions'
- Add caret span with aria-hidden='true'
- Trigger has aria-haspopup='true', aria-expanded='false', aria-controls attributes
- All wrapped in .view-dropdown div with id='view-dropdown'

Tests added:
- test_view_dropdown_trigger_exists
- test_view_dropdown_container_exists
- test_view_dropdown_trigger_has_aria
- test_view_dropdown_menu_has_role_menu

All 93 tests pass (4 new + 89 existing).
2026-04-15 17:23:00 -07:00
Brian Krabach ecca48a1ad feat: auto-add new session to active user view on creation
When a session is created via the + button while a user view is active,
auto-add the new session's key to that view's sessions list.

- In createNewSession(), after the successful POST call, check _activeView
- Skip auto-add for reserved views ('all', 'hidden')
- Build sessionKey correctly using device_id or remoteId
- PATCH /api/settings with updated views, catch errors gracefully
- Update local _serverSettings.views cache immediately

Test: test_create_new_session_references_active_view
2026-04-15 17:17:11 -07:00
Brian Krabach c3eb714977 feat: rewrite getVisibleSessions() to honor active view (task-2)
- Add _activeView state variable with default 'all' in App state section
- Rewrite getVisibleSessions() to filter by active view:
  - 'all' view: excludes hidden sessions
  - 'hidden' view: shows only hidden sessions
  - user view: filters to view's sessions list using sessionKey
  - Falls back to 'all' if active view no longer exists
- Add _getActiveView() and _setActiveView() test helpers
- Export new test helpers in window.MuxplexApp (module.exports)
- Add 6 new tests covering all acceptance criteria

Tests added:
- test_active_view_state_variable_exists
- test_get_visible_sessions_all_view_excludes_hidden
- test_get_visible_sessions_hidden_view_shows_hidden
- test_get_visible_sessions_user_view_filters_by_session_key
- test_get_active_view_helper_exported
- test_set_active_view_helper_exported

All 220 tests pass.
2026-04-15 17:08:04 -07:00
Brian Krabach acdfc9bafd docs: update patch_state docstring to include active_view 2026-04-15 17:03:17 -07:00
Brian Krabach b69a1f8c15 feat: add active_view to StatePatch model and patch_state handler
- Add optional active_view field to StatePatch Pydantic model
- Add active_view handling in patch_state to persist the field
- Add test_patch_state_sets_active_view: verifies PATCH persists active_view
- Add test_patch_state_active_view_defaults_to_all: verifies GET returns 'all' by default

Closes task-1: Add active_view to StatePatch Model
2026-04-15 16:56:43 -07:00