Commit Graph

463 Commits

Author SHA1 Message Date
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
Brian Krabach 3894a585c5 chore: release v0.3.6 -- browser context menu fix 2026-04-14 11:10:18 -07:00
Brian Krabach 56f7b99fd8 fix: suppress browser context menu on plain right-click in terminal pane 2026-04-14 11:10:15 -07:00
Brian Krabach 655b9ecab0 chore: release v0.3.5 -- connection pool exhaustion fix 2026-04-14 06:36:07 -07:00
Brian Krabach 2e57358e2e fix: replace setInterval with self-scheduling setTimeout for async polls
setInterval fires regardless of whether the previous async call has
completed. When federation requests time out (5s) during 2s poll cycles,
multiple requests stack up. Chrome's 6-connection-per-origin limit is
quickly exhausted → ERR_INSUFFICIENT_RESOURCES → death spiral.

Fix: self-scheduling setTimeout pattern — poll completes → wait → next
poll. At most one in-flight request at a time. Applied to both
pollSessions and sendHeartbeat loops.

A sentinel (true) is set on _pollingTimer/_heartbeatTimer immediately in
startPolling/startHeartbeat so the double-start guard works even during
the first async call before the real timer ID is assigned.
2026-04-14 05:30:39 -07:00
Brian Krabach 8345ee3323 chore: release v0.3.4 -- federation UI fixes 2026-04-13 14:56:42 -07:00
Brian Krabach b247f01ebc fix: remoteId=0 falsy bug in renderSheetList — use null check
The first remote instance (index 0) couldn't be opened from the mobile
bottom sheet because s.remoteId ? treated 0 as falsy. Changed to
s.remoteId != null to match buildTileHTML and buildSidebarHTML.
2026-04-13 14:18:25 -07:00