Commit Graph

473 Commits

Author SHA1 Message Date
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
Brian Krabach f7519a8058 fix: zero-session devices show 'No sessions' tile + grace period prevents flapping
Two federation UX fixes:

1. Zero-session devices: when remote returns empty sessions list, return
   {status: 'empty'} entry. Frontend renders 'No sessions' status tile
   instead of making the device invisible.

2. Flapping prevention: server-side cache of last-known-good federation
   results per remote. On transient failure, return cached sessions for
   up to 3 consecutive failures before marking unreachable. Eliminates
   the visible on/off/on/off pattern caused by occasional 5-second
   timeouts during 2-second poll cycles.

Tests added:
- test_fetch_remote_returns_empty_status_for_zero_sessions (Python)
- test_fetch_remote_uses_cache_on_transient_failure (Python)
- test_fetch_remote_marks_unreachable_after_grace_period (Python)
- renderGrid shows 'No sessions' status tile for status=empty devices (JS)

Also adds reset_federation_cache autouse fixture to prevent cross-test
contamination from the new module-level _federation_cache dict.
2026-04-13 14:12:18 -07:00
Brian Krabach 26a4d30d6f fix: status tiles show device name instead of blank — use deviceName not name
buildStatusTileHTML received session.name (undefined for status entries)
instead of session.deviceName. Offline/unreachable tiles showed blank.
Fixed all 4 call sites in renderGrid to pass session.deviceName.

Updated two existing tests to use deviceName (not name) in status entry
objects — reflecting the real federation data shape.
2026-04-13 13:20:29 -07:00
Brian Krabach 2c65e5b4c2 fix: filter status entries from visible sessions — no more duplicate blank/offline tiles
Unreachable/auth_failed status entries from the federation response
were rendered twice: once as a blank session tile (no name) by
buildTileHTML, and again as an 'Offline' status tile by the separate
status rendering loop. Fix: getVisibleSessions() now filters out
entries with a status field. Status tiles are still rendered separately
from the full sessions array.
2026-04-13 12:01:32 -07:00
Brian Krabach 8d271ddd58 chore: release v0.3.3 -- iOS/iPadOS touch scroll fix 2026-04-13 08:50:50 -07:00
Brian Krabach e3cedb70a2 Merge pull request #4 from samueljklee/fix/ios-touch-scroll
Fix touch scrolling on iOS and iPadOS devices
2026-04-13 08:34:04 -07:00
Samuel Lee f206485f0a Fix touch scrolling on iOS and iPadOS devices
The touch scroll handler was only enabled for Android devices,
leaving iOS/iPad users unable to scroll through terminal history.

Changes:
- Extend UA detection to include iPhone, iPad, and iPod
- Add iPadOS 13+ detection (reports as MacIntel with touch points)
- Rename function to initMobileTerminalScroll for clarity

Fixes #3
2026-04-11 13:01:41 -07:00
Brian Krabach 0b2ec74ee5 chore: release v0.3.2 -- hidden sessions federation fix 2026-04-09 02:38:22 -07:00
Brian Krabach df1960b7f9 fix: apply hidden sessions filter to all sessions including remote/federated 2026-04-09 02:37:18 -07:00
Brian Krabach 9424dcff5b chore: release v0.3.1 -- federation auth hotfix 2026-04-08 23:17:55 -07:00
Brian Krabach d541612843 fix: read federation key live on each request and check PUT sync response
Bug 1 (auth.py): AuthMiddleware.dispatch() was using self.federation_key
(set once at startup) for Bearer token validation. If the key was generated
or rotated via POST /api/federation/generate-key after startup, the old
(often empty) value caused all federation auth to silently return 401.

Fix: import load_federation_key from muxplex.settings and call it fresh on
every non-exempt, non-cookie request. Also adds a warning log when a Bearer
token is received but no key is configured on this server.

Bug 2 (main.py): _sync_settings_with_remotes() discarded the PUT response,
silently swallowing 401/500 errors from the remote sync endpoint.

Fix: capture the PUT response as put_resp. Handle 409 (Conflict = remote is
newer) with a debug log; raise_for_status() for any other non-2xx so errors
propagate to the outer except and are logged as warnings.

Tests:
- test_dispatch_calls_load_federation_key_live: pattern test confirming
  load_federation_key() is called inside dispatch()
- test_dispatch_does_not_use_stale_self_federation_key_for_bearer: pattern
  test confirming self.federation_key is gone from the live bearer check
- test_sync_put_response_calls_raise_for_status: pattern test confirming
  raise_for_status() is called on the PUT response in the sync function
- Updated existing bearer tests to monkeypatch load_federation_key so they
  are isolated from any real key file on disk
2026-04-08 22:39:03 -07:00
Brian Krabach c4ff5c618f chore: release v0.3.0 -- federation state propagation 2026-04-08 22:06:20 -07:00
Brian Krabach 2ebd3b9929 feat: add settings sync logic to poll cycle with 30-second interval
- Add SETTINGS_SYNC_INTERVAL = 15 constant (15 poll cycles × 2s ≈ 30s)
- Add _settings_sync_counter module-level counter
- Add _sync_settings_with_remotes() async function that:
  - GETs /api/settings/sync from each remote instance
  - Adopts remote settings if remote timestamp is newer
  - Pushes local settings via PUT if local timestamp is newer
  - Silently skips 404/405 (older muxplex without sync endpoints)
  - Catches all per-remote errors and logs as warnings
- Wire sync call into _run_poll_cycle() step 13 (runs outside state_lock)
- Add test_settings_sync_poll.py with 10 tests covering all sync behaviours
2026-04-08 21:15:44 -07:00
Brian Krabach a0ad4921cf feat: add PUT /api/settings/sync federation endpoint with newer-wins logic 2026-04-08 21:03:23 -07:00
Brian Krabach 349f49824b feat: add GET /api/settings/sync federation endpoint 2026-04-08 20:50:46 -07:00
Brian Krabach 29cfb3c141 feat: sync-aware patch_settings and apply_synced_settings 2026-04-08 20:37:52 -07:00
Brian Krabach ce001d2b05 feat: add SYNCABLE_KEYS allowlist and settings_updated_at to settings 2026-04-08 20:35:08 -07:00
Brian Krabach 783611d143 fix: filter hidden sessions in browser indicators + add federation plan 2026-04-08 20:32:19 -07:00
Brian Krabach e7379c46c9 chore: fix step numbering in _run_poll_cycle comments 2026-04-08 19:40:04 -07:00
Brian Krabach 8fcb25adee fix: capture exception in federation bell clear warning log
Captures the exception as 'exc' in the except clause and includes it
as the third argument to the warning log message. This ensures that
the failure reason is observable in logs, improving diagnostics for
federation heartbeat failures.

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-04-08 19:32:20 -07:00
Brian Krabach f5ae0df9f5 feat: add heartbeat-driven bell clearing for remote sessions in poll cycle
- Add module-level _federation_client reference for use in background poll task
- Assign _federation_client in lifespan startup; clear on shutdown
- Add step 12 to _run_poll_cycle: iterate devices viewing a remote session
  in fullscreen with recent interaction (<60s), fire POST bell/clear to
  the active remote instance with Bearer auth (fire-and-forget, errors logged)
- Add test_poll_cycle_fires_federation_bell_clear_for_remote_session test

Closes task-3 of federation-state-propagation-plan
2026-04-08 18:50:17 -07:00
Brian Krabach 5eb727fe86 fix: use s.remoteId == null check instead of !s.remoteId in getVisibleSessions()
Fixes falsy-0 bug where sessions with remoteId=0 (first remote instance) were
incorrectly hidden because !s.remoteId treats 0 as falsy. The null check
s.remoteId == null correctly handles:
- remoteId=0 (first remote instance) → truthy, not hidden
- remoteId=null or undefined (local sessions) → falsy, hidden if in hidden list

Changes:
- muxplex/frontend/app.js: Line 533 condition in getVisibleSessions()
- muxplex/frontend/tests/test_app.mjs: Added 2 tests for remoteId=0 behavior
- muxplex/tests/test_frontend_js.py: Added pattern test verifying source code

All 330 JS tests pass, all 200 Python tests pass.

Generated with Amplifier (https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-04-08 18:04:45 -07:00
Brian Krabach 4856094ca9 docs: add federation state propagation design 2026-04-08 17:38:08 -07:00
Brian Krabach 37c2b47ec9 fix: read version from package metadata — single source of truth (pyproject.toml) 2026-04-08 14:36:58 -07:00
Brian Krabach e39e37d585 docs: update CHANGELOG and README for settings consolidation 2026-04-08 14:01:32 -07:00
Brian Krabach 4b0718eff9 fix: terminal.js fontSize regression and stale test references
Issue 1 (CRITICAL): terminal.js createTerminal() read fontSize from
localStorage.getItem('muxplex.display'), which always returns null after
the server-settings migration. This caused terminal font size to permanently
default to 14 regardless of the server-side fontSize setting.

Fix: Pass getDisplaySettings().fontSize from app.js to window._openTerminal
as a third argument. Update openTerminal(name, remoteId, fontSize) and
createTerminal(fontSize) to use the parameter, removing all localStorage
reads from terminal.js.

Issue 2: Fix 9 stale test references in test_app.mjs that set
_localStorageStore['muxplex.display'] instead of app._setServerSettings().
Tests now correctly exercise the server-settings path.

Issue 3: Fix stale initSidebar test that used
delete _localStorageStore['muxplex.sidebarOpen'] — replaced with
app._setServerSettings(null) since initSidebar reads from _serverSettings.

Also update 5 Python tests in test_frontend_js.py that were checking the
old localStorage-based createTerminal() behavior:
- Renamed test_create_terminal_reads_font_size_from_localstorage to
  test_create_terminal_accepts_font_size_parameter
- Renamed test_create_terminal_parses_json_for_font_size to
  test_create_terminal_does_not_parse_json_from_localstorage
- Updated 3 remaining tests to use regex that matches createTerminal(fontSize)
  instead of createTerminal()

Verification:
- grep -rn 'localStorage' app.js: only tmux-web-device-id (3 lines)
- grep -rn 'muxplex\.display|muxplex\.sidebarOpen' frontend/: zero matches
- All 372 JS tests pass (44 terminal + 328 app)
- All 199 Python frontend_js tests pass
2026-04-08 12:39:08 -07:00
Brian Krabach 5f09d0b0f0 refactor: eager settings load on startup and update all frontend tests
Part A: DOMContentLoaded now async with await loadServerSettings() before
first render. Removes lazy loadServerSettings() from inside .then() block.

Part B: Fix 18 failing tests in test_app.mjs:
- Sidebar tests: replace _localStorageStore['muxplex.sidebarOpen'] with
  app._setServerSettings({sidebarOpen: ...}); update toggleSidebar mocks
  to include classList.contains(); assert _serverSettings.sidebarOpen
  instead of localStorage values
- loadGridViewMode/saveGridViewMode tests: replace localStorage setup with
  _setServerSettings({gridViewMode: ...}); assert _serverSettings results
- cycleViewMode test: replace loadDisplaySettings/saveDisplaySettings with
  _setServerSettings/getDisplaySettings
- activityIndicator tests (7): replace _localStorageStore['muxplex.display']
  JSON.stringify() with app._setServerSettings({activityIndicator: ...})
- killSession test: increase substring limit from 500 to 600 chars

Part C: Add _getServerSettings test helper to app.js exports so tests can
read back _serverSettings state after sidebar/display mutations.
2026-04-08 11:55:31 -07:00
Brian Krabach 5da5fa8a45 refactor: rewrite sidebar functions to use server-side settings
- Remove SIDEBAR_KEY constant (was an undefined variable bug)
- Rewrite initSidebar() to read sidebarOpen from _serverSettings
- Rewrite toggleSidebar() to derive state from DOM class and persist
  to server via patchServerSetting
- Rewrite bindSidebarClickAway() to persist collapsed state via
  patchServerSetting instead of localStorage
- Add 7 tests verifying SIDEBAR_KEY removal and _serverSettings usage
2026-04-08 11:29:55 -07:00
Brian Krabach ecc5e6d42a test: update frontend tests for server-side display settings 2026-04-08 11:18:16 -07:00
Brian Krabach 0467d522c4 refactor: replace loadDisplaySettings/saveDisplaySettings callsites with server-settings API
- Global rename: all loadDisplaySettings() calls → getDisplaySettings() (16 occurrences)
- cycleViewMode(): replace saveDisplaySettings(ds) with server patch:
  _serverSettings.viewMode + patchServerSetting('viewMode', ds.viewMode)
- saveGridViewMode(): replace localStorage body with:
  _serverSettings.gridViewMode + patchServerSetting('gridViewMode', mode)
- onDisplaySettingChange(): replace saveDisplaySettings(ds) with batch PATCH:
  build 6-key patch object, Object.assign to _serverSettings,
  api('PATCH', '/api/settings', patch) with showToast/catch
- Bell sound handler: replace saveDisplaySettings with
  _serverSettings.bellSound + patchServerSetting('bellSound', checked)
- Notification permission handler: remove getDisplaySettings/saveDisplaySettings
  lines entirely (notificationPermission no longer stored)

Acceptance criteria verified:
  grep saveDisplaySettings: 0 matches
  grep loadDisplaySettings: 0 matches
  grep auto_open: only auto_open_created references

Task: task-3-update-all-frontend-callsites
2026-04-08 10:37:20 -07:00
Brian Krabach 9d18954a21 refactor: replace localStorage display settings with server-settings cache
- Delete DISPLAY_SETTINGS_KEY ('muxplex.display') constant
- Delete SIDEBAR_KEY ('muxplex.sidebarOpen') constant
- Update DISPLAY_DEFAULTS: remove notificationPermission, add gridViewMode: 'flat'
  - Now contains 9 keys: fontSize, hoverPreviewDelay, gridColumns, bellSound,
    viewMode, showDeviceBadges, showHoverPreview, activityIndicator, gridViewMode
- Replace loadDisplaySettings() with getDisplaySettings()
  - New function reads from _serverSettings cache with DISPLAY_DEFAULTS fallback
  - Uses Object.assign and hasOwnProperty for proper defaults
- Delete saveDisplaySettings() function entirely
- Update module.exports to export getDisplaySettings instead of loadDisplaySettings
- Add 11 new TDD tests covering all functionality changes (all passing)

Note: This is a transitional commit. Callsites in app.js still reference
loadDisplaySettings/saveDisplaySettings (expected breakage, fixed in Task 3).

Generated with Amplifier

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-04-08 10:21:43 -07:00
Brian Krabach 122d9c30c8 feat: add 10 display settings keys to DEFAULT_SETTINGS
Add 10 new flat display settings keys to DEFAULT_SETTINGS in settings.py,
after the tls_key line:
- fontSize (14)
- hoverPreviewDelay (1500)
- gridColumns ('auto')
- bellSound (False)
- viewMode ('auto')
- showDeviceBadges (True)
- showHoverPreview (True)
- activityIndicator ('both')
- gridViewMode ('flat')
- sidebarOpen (None)

DEFAULT_SETTINGS now contains 27 keys total (17 existing + 10 new).

No changes needed to load/save/patch functions — they iterate
DEFAULT_SETTINGS keys automatically.

Add two new pytest tests:
- test_defaults_include_display_settings: verifies all 10 keys exist
  with correct default values
- test_display_settings_round_trip_via_patch: verifies patch_settings +
  load_settings cycle preserves custom display values

All 44 tests pass.

Co-authored-by: Amplifier <amplifier@example.com>
2026-04-08 10:03:51 -07:00
Brian Krabach 3bb3944b52 docs: add design for consolidating settings server-side
Move all display/UX settings from browser localStorage to server-side
settings.json. Flat keys approach (Approach A) - adds 10 new keys to
DEFAULT_SETTINGS, requiring zero changes to existing load/save/patch
functions or API endpoints.

Key decisions:
- No federation settings sync (each server owns its own settings)
- Drop notificationPermission (browser API is source of truth)
- sidebarOpen defaults to None for auto-detect on first load
- No migration needed - users reset preferences
- Fix auto_open vs auto_open_created naming alignment

Verified compatible with muxplex v0.2.0 (1b5207b).
2026-04-08 09:24:12 -07:00
Brian Krabach 1b5207b5af chore: release v0.2.0 -- version bump, changelog, FastAPI version fix 2026-04-08 08:32:05 -07:00
Brian Krabach 12c8f8f550 feat: add federation proxy for deleting remote sessions
- Add DELETE /api/federation/{remote_id}/sessions/{session_name} endpoint
  to proxy session deletion to remote instances with Bearer auth
  (follows same pattern as existing create/connect/bell-clear proxies)
- Update killSession() to accept optional remoteId parameter and route
  through federation delete proxy when present
- Update delegated click handler to extract data-remote-id from parent
  tile/sidebar element and pass to killSession() for remote routing
- Now users can delete sessions on remote devices from their local client

Fixes: https://github.com/bkrabach/muxplex/issues (delete remote sessions)
Generated with Amplifier
2026-04-08 08:21:22 -07:00
Brian Krabach 67d68d8cf0 fix: resolve federation, settings, and session creation bugs
Seven bug fixes across five files:

1. auth.py: Add .json to static extension allowlist so /manifest.json
   bypasses auth. Previously, unauthenticated requests got redirected to
   the login page HTML which the browser tried to parse as JSON.

2. sessions.py: Catch FileNotFoundError in enumerate_sessions() alongside
   RuntimeError. If the session command binary is missing from PATH,
   asyncio.create_subprocess_exec raises FileNotFoundError, which was
   unhandled and broke the poll loop.

3. settings.py: Fix federation key erasure when remote instance URLs are
   edited. The key-preservation logic only restored keys by exact URL
   match. Editing a URL (e.g. http:// to https://) changed the lookup
   key, permanently erasing the real federation key. Added position-based
   fallback for same-slot URL edits.

4. main.py: Replace fire-and-forget subprocess.Popen in create_session
   with asyncio.create_subprocess_shell that checks the return code and
   returns proper HTTP 500 errors. Added shutil.which() pre-flight check.
   Commands that exit non-zero but still create the session (e.g.
   amplifier-workspace which tries to attach after create) are detected
   by checking enumerate_sessions() and treated as success.

5. main.py: Redact sensitive keys in PATCH /api/settings response,
   matching the existing GET endpoint behavior.

6. main.py: Include exception type and message in federation 503 error
   responses (connect, bell-clear, create-session).

7. app.js: Fix auto_open vs auto_open_created key mismatch. The settings
   toggle read/wrote 'auto_open' but the backend key is
   'auto_open_created'. The PATCH was silently dropped and the toggle
   was completely non-functional.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
2026-04-08 08:08:54 -07:00
Brian Krabach 9335964494 chore: release prep v0.1.1 — version bump, changelog, README update
Bump version 0.1.0 → 0.1.1. Add CHANGELOG.md covering all changes
since initial release. Update README clipboard section to reflect
native xterm.js paste handling.
2026-04-07 10:01:34 -07:00
Brian Krabach 12ad92986e fix: consolidate WS proxy Bearer header guard to single line for CI test
The source-inspection test checks each line individually for an 'if'
guard. The multi-line formatting of the 'if remote_key else {}' guard
split it across 3 lines, making the test see an unguarded header.
Consolidated to match the single-line pattern of the other 4 endpoints.
2026-04-07 08:10:14 -07:00
Brian Krabach d2691b9205 refactor: remove ALL custom paste handlers — clean slate per COE review
After 9 clipboard commits that kept breaking each other, COE review
determined: ttyd uses ClipboardAddon, not custom handlers. Ctrl+Shift+V
on Linux and Cmd+V on macOS both trigger native browser paste events
that xterm.js catches via its hidden textarea handler. Zero custom
paste code needed. Deleted synthetic ClipboardEvent dispatch, readText
calls, and all paste-related comments. Kept: Ctrl+Shift+C copy,
auto-copy on selection, OSC 52, Ctrl+F search.
2026-04-07 07:44:14 -07:00
Brian Krabach e4a4c97eec feat: restore Ctrl+Shift+V via synthetic paste event on xterm textarea
Ctrl+Shift+V does not trigger a native browser paste event (unlike Cmd+V
on macOS). Fix: read clipboard via navigator.clipboard.readText(), then
dispatch a synthetic ClipboardEvent on xterm.js's hidden textarea. This
triggers xterm.js's built-in handlePasteEvent which handles CR/LF
normalization, bracketed paste mode, and correct UTF-8 encoding — the
same code path as Cmd+V.

Also replaces the old regression test that asserted Ctrl+Shift+V was
NOT intercepted, replacing it with the correct test for the synthetic
paste approach.
2026-04-07 02:12:49 -07:00
Brian Krabach ced0c62dd6 fix: decode UTF-8 WebSocket output before writing to xterm.js
xterm.js write(Uint8Array) treats each byte as Latin-1, not UTF-8.
Multi-byte UTF-8 characters like box-drawing ─ (U+2500, bytes E2 94 80)
were rendered as â (Latin-1 interpretation of 0xE2). Fix: decode with
TextDecoder before _term.write(), matching ttyd's official client pattern.

Add module-level _decoder (alongside existing _encoder) and use
_decoder.decode(payload) in the type 0x30 message handler.
2026-04-06 22:16:19 -07:00