Commit Graph

469 Commits

Author SHA1 Message Date
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
Brian Krabach ab5560a623 fix: address COE verification findings — bell-clear regression, active_view fallback, device_id consistency
- BUG 1: Replace isinstance(active_remote_id, int) guard with _lookup_remote_by_device_id()
  so bell-clear fires correctly when active_remote_id is a UUID string (new format).
  Old code silently skipped the POST for all non-integer active_remote_id values.
  Regression test: test_poll_cycle_fires_federation_bell_clear_for_remote_session_with_uuid

- GAP 4: Add _resolveActiveView(activeView, views) helper in app.js.
  Falls back to 'all' when active_view references a deleted/unknown view name.
  Ready for Phase 2 view-switching code to call at read time.

- ISSUE 6: _createDeviceSelect now uses remotes[i].device_id || String(i) for option
  values instead of always String(i), so session creation routes receive correct device_id.

- ISSUE 9: identity.json written with indent=2 + trailing newline, consistent with
  settings.json and state.json formatting conventions.

- ISSUE 11: Fix state.py module docstring — 'tmux-web muxplex' → 'muxplex'.

- ISSUE 12: Settings panel hidden_sessions checkboxes now use s.sessionKey || s.name
  as the checkbox value (and checked state) so remote sessions are stored in
  device_id:name format, consistent with getVisibleSessions() lookups.
2026-04-15 14:42:50 -07:00
Brian Krabach 70822d8033 feat: remove 'filtered' gridViewMode, keep only flat and grouped
- renderGrid: remove all _gridViewMode === 'filtered' branches (device
  filter application and filter bar rendering); filter bar is now always
  cleared for flat/grouped modes
- loadGridViewMode: add 'filtered' → 'flat' fallback so existing users
  who had the setting stored don't get a broken state
- _setGridViewMode (test helper): same guard so tests can't accidentally
  set the removed mode
- test_frontend_js.py: three new static-analysis tests assert the guards
  are present and the renderGrid filtered checks are gone
2026-04-15 14:05:28 -07:00
Brian Krabach 68fa80b694 feat: update frontend to use device_id-based session keys and API URLs
- buildTileHTML: prefer session.deviceId over legacy integer remoteId for data-remote-id
- buildSidebarHTML: prefer session.deviceId over legacy integer remoteId for data-remote-id
- getVisibleSessions: check s.sessionKey (device_id:name) in hidden_sessions list for
  backward compatibility with both old (plain name) and new (device_id:name) formats
- openSession: rename _remoteId → _deviceId to reflect value is now a device_id string
- createNewSession: rename internal remoteId variable → deviceId for clarity

All federation API URLs continue to work via opts.remoteId (now contains deviceId value).
Backward compatibility maintained: fallback to remoteId when deviceId not present (old server).

Tests: updated test_open_session_fires_bell_clear_for_remote to check _deviceId guard;
added 5 new tests covering all structural changes.
2026-04-15 13:59:03 -07:00
Brian Krabach 5510d9d008 feat: tag sessions with device_id-based sessionKey in federation_sessions 2026-04-15 13:48:39 -07:00
Brian Krabach c26c61b7da fix: extract auth_headers to local var to prevent ruff splitting inline guards
Ruff was reformatting the ternary header constructions to multi-line form:
  headers={"Authorization": f"Bearer {remote_key}"}\n  if remote_key\n  else {},

This caused test_federation_auth_headers_guard_empty_key to fail because
the source-inspection test requires 'if' to appear on the same line as
the Authorization header dict.

Fix: assign to a local variable before use so ruff has no reason to split:
  auth_headers = {"Authorization": f"Bearer {key}"} if key else {}

Applied at two sites:
- poll-cycle bell-clear (line ~232)
- federation WS proxy endpoint (line ~1094)
2026-04-15 13:02:32 -07:00
Brian Krabach 665b74b455 style: apply ruff format to main.py (task-9 follow-up) 2026-04-15 12:42:56 -07:00
Brian Krabach 8d7990b1c5 feat: switch federation proxy endpoints to device_id lookup (task-9)
Change all 5 federation proxy URL patterns from {remote_id:int} to
{device_id} (str) and use _lookup_remote_by_device_id() for lookup.

Affected endpoints:
- federation_connect: /api/federation/{device_id}/connect/{session_name}
- federation_bell_clear: /api/federation/{device_id}/sessions/{session_name}/bell/clear
- federation_create_session: /api/federation/{device_id}/sessions
- federation_delete_session: /api/federation/{device_id}/sessions/{session_name}
- federation_terminal_ws_proxy: /federation/{device_id}/terminal/ws

All endpoints now use _lookup_remote_by_device_id(device_id) which
provides integer index fallback for backward compatibility.

Also update existing test to expect 404 instead of 422 for non-integer
device_id (since str type accepts any string, lookup returns None->404).

Tests:
- test_federation_connect_by_device_id: POST with device_id='aaa-111-bbb' returns 200
- test_federation_connect_device_id_not_found: unknown device_id returns 404
2026-04-15 12:16:56 -07:00
Brian Krabach cc9bb81bc9 feat: add _lookup_remote_by_device_id helper for federation proxy
Implements _lookup_remote_by_device_id(device_id: str) -> dict | None
in muxplex/main.py, inserted before the federation WebSocket proxy section.

Logic:
- Load settings and get remote_instances list
- Primary: iterate remotes, return first where remote.get('device_id') == device_id
- Fallback: if device_id parses as integer, use index-based lookup for
  transition compatibility (0 <= idx < len(remotes))
- Return None if not found

Tests added:
- test_lookup_remote_by_device_id_found: two remotes with device_ids
  'aaa-111' and 'bbb-222'; lookup 'bbb-222' returns Desktop remote
- test_lookup_remote_by_device_id_not_found: one remote with device_id
  'aaa-111'; lookup 'zzz-999' returns None

Task: task-8
2026-04-15 11:45:06 -07:00
Brian Krabach 6984c71823 style: add explanatory comment and remove redundant import
- Add clarifying comment to lazy import of enforce_mutual_exclusion
  in apply_synced_settings() to document circular-import avoidance pattern
- Remove redundant 'import json' from inside
  test_apply_synced_settings_enforces_mutual_exclusion()
  since json is already imported at module level

No behavior changes. All 62 tests pass.

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-04-15 11:37:00 -07:00
Brian Krabach a3ff22dc02 feat: wire post-sync mutual exclusion invariant repair into apply_synced_settings
- Call enforce_mutual_exclusion(current) in apply_synced_settings() after
  applying SYNCABLE_KEYS and before save_settings()
- Import enforce_mutual_exclusion from muxplex.views inside the function
  to avoid circular import issues
- Update docstring to describe the mutual exclusion invariant repair step
- Add test_apply_synced_settings_enforces_mutual_exclusion to verify that
  sessions present in both hidden_sessions and a view's sessions are
  removed from hidden_sessions after sync (task-7)
2026-04-15 11:33:49 -07:00
Brian Krabach 569ba44848 feat: add --reset-device-id CLI command (task-6)
Add reset_device_id_command() function to cli.py that:
- loads the current device_id via load_device_id()
- generates a new device_id via reset_device_id()
- prints new device_id, identity file path, previous device_id
- warns that existing session keys are now orphaned

Register 'reset-device-id' subparser with appropriate help text
and add dispatch branch in main().

Tests added:
- test_reset_device_id_writes_new_id: verifies command writes new ID
  and prints required output including orphan warning
- test_main_dispatches_to_reset_device_id: verifies CLI routing
2026-04-15 11:26:32 -07:00
Brian Krabach ad2c0e1fe2 feat: extend /api/instance-info to return device_id
Add load_device_id import from muxplex.identity and include device_id
in the instance-info response dict. Update endpoint docstring to
mention device identity.

Test: test_instance_info_includes_device_id verifies device_id is
present as a non-empty string when IDENTITY_PATH is redirected to
tmp_path.
2026-04-15 11:20:08 -07:00
Brian Krabach 353ea5fd76 feat: add views.py with mutual exclusion invariant and name validation 2026-04-15 11:10:34 -07:00
Brian Krabach eb2c02b477 feat: add views to DEFAULT_SETTINGS and SYNCABLE_KEYS
- Add 'views': [] to DEFAULT_SETTINGS (after 'hidden_sessions')
- Add 'views' to SYNCABLE_KEYS in Session behavior section (after 'hidden_sessions')
- Add 4 new tests: test_views_in_default_settings, test_views_in_syncable_keys,
  test_views_roundtrip_through_save_and_load, test_patch_settings_syncs_views

All 61 tests pass (57 existing + 4 new).
2026-04-15 11:03:42 -07:00
Brian Krabach d9e9ab2fb6 feat: update state path to muxplex, add MUXPLEX_STATE_DIR env var, add active_view field
- Change _default_state_dir from 'tmux-web' to 'muxplex'
- Update STATE_DIR to prefer MUXPLEX_STATE_DIR env var with TMUX_WEB_STATE_DIR fallback
- Add 'active_view': 'all' to empty_state() between active_remote_id and session_order
- Update module docstring schema to document active_view field
- Add tests: test_empty_state_has_active_view_key, test_empty_state_active_view_defaults_to_all, test_state_dir_uses_muxplex_name
2026-04-15 10:59:57 -07:00
Brian Krabach 59b1672275 feat: add device identity module with persistent UUID v4 storage
- Add muxplex/identity.py with IDENTITY_PATH, load_device_id(), reset_device_id()
- load_device_id() creates identity.json when absent, regenerates on corrupt JSON
  or missing device_id key, creates parent directories as needed
- reset_device_id() generates a new UUID v4, overwrites identity.json, returns it
- Add muxplex/tests/test_identity.py with 8 tests covering all spec requirements
2026-04-15 10:56:11 -07:00
Brian Krabach 93889f7dda docs: add Views feature design
Adds design specification for user-defined Views — curated session
collections that span devices and replace the filtered gridViewMode.

Covers:
- Stable device identity prerequisite (device_id UUID in identity.json)
- Data model: views array in settings, active_view in state
- Three view tiers: All (virtual), user-created, Hidden (virtual)
- Mutual exclusion invariant between hidden and view membership
- UI: header dropdown view switcher, tile flyout menu, add sessions panel
- Migration strategy for session key format change
- Known limitations: rename breakage, atomic sync, shortcut cap
2026-04-15 09:38:29 -07:00