Commit Graph

752 Commits

Author SHA1 Message Date
arya 26932ea0bf fix(desktop): retain registry session ownership 2026-08-25 22:51:31 -07:00
hermes-seaeye[bot] 1a5ca62233 fmt(js): npm run fix on merge (#95114)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-25 23:31:59 +00:00
Brooklyn Nicholson 30b042e137 fix(desktop): clear the JS/TS check failures on the Browser pop-out
Import order, an unused test import, and the session-menu windows mock now that session-states reads isBrowserWindow.
2026-08-25 18:23:43 -05:00
Brooklyn Nicholson a608799689 feat(desktop): give the in-app Browser its own OS window
A ?win=browser renderer (query before hash) so HashRouter cannot swallow the flag, and so one window per tab can focus instead of cloning.
2026-08-25 18:23:43 -05:00
Teknium a87e8831f7 style(desktop): sort secret-storage-policy import per perfectionist lint 2026-08-25 14:10:19 -07:00
Teknium 6a6e16fa5d feat(desktop): OS-keychain encryption for stored secrets is now opt-in — no more macOS Keychain password prompt on every launch
Electron safeStorage parks a per-app key ('Hermes Key') in the macOS login
keychain; on machines with a locked/missing/corrupted default keychain that
turned every Hermes Desktop launch into a blocking 'Keychain Not Found' /
password dialog. Keychain-backed encryption is now an explicit opt-in:

- electron/secret-storage-policy.ts: standalone policy seam (default OFF,
  strict === true coercion, one-shot migration flag) + unit tests
- default path never calls any safeStorage API (including
  isEncryptionAvailable, which itself touches the keychain)
- one-shot legacy migration decrypts existing safeStorage blobs to plain
  0600 files at first launch; undecryptable blobs are kept but read as
  absent afterward (classify 'drop') so a dead keychain prompts at most once
- Settings -> Gateway toggle (all 5 locales) re-encodes every stored secret
  store in place when flipped (v1 connection.json, v2 connections.json,
  native-oauth-tokens.json)
- e2e: at-rest spec now covers both postures (opted-in unchanged contract,
  default saves without secure storage, owner-only bits, restart round-trip)
- docs: multi-connection-desktop + desktop-native-signin updated
2026-08-25 14:10:19 -07:00
Brooklyn Nicholson 90ee4460cb fix(desktop): translate sidebar recents_profile through SSH aliases
Managed SSH maps a Desktop profile label onto a different remote name.
The sidebar filter lives in recents_profile, so rewriting only ?profile=
left those reads on the remote default and the Sessions list came back empty.

Co-authored-by: noah <loahnisk@gmail.com>
2026-08-25 12:07:00 -05:00
kshitijk4poor 49442c7b70 fix(ws-only): address review blockers — opt-in gating, loopback fail-close, real v15 handshake tests
Review F1: --ws-only is now OPT-IN (HERMES_DESKTOP_WS_ONLY=1). The slim
server has no HTTP routes while the desktop's hermes:api REST plane only
speaks http — shipping it on-by-default made every REST call fail with
'Unsupported Hermes backend URL protocol: ws:'. The descriptor keeps
baseUrl http-only and flags the transport via wsOnlyTransport instead of
overloading baseUrl. Default flips after the REST consumers migrate to
JSON-RPC (#94484 phase 3).

Review F3: entry_ws.run() fails closed on non-loopback hosts — the static
query-token auth is the desktop loopback shortcut, not a posture fit for
network exposure (no gated one-time-ticket auth).

Review F2 (test half): tests/test_tui_gateway_entry_ws.py drives a REAL
websockets-15.0.1 server + sync client through the actual handshake —
valid token accepted (path read from the v15 ServerConnection), missing/
wrong token rejected with 4401, plus the loopback guard both ways.
2026-08-25 13:40:36 +05:30
kshitijk4poor 71e2924c5f fix: TypeScript casts + backend-ready test updates for { port, token } return 2026-08-25 13:27:28 +05:30
kshitijk4poor 434ea57eb0 feat(gateway): slim WS-only server — remove FastAPI/uvicorn from desktop boot path
The desktop app spawns `hermes serve --ws-only` instead of `hermes serve`.
The slim server (tui_gateway/entry_ws.py) uses the bare websockets library
to call handle_ws directly — same dispatch surface, same 158 RPCs, same
events, zero HTTP framework. tui_gateway.server has zero FastAPI imports;
the slim server never touches web_server.py (19.8K lines) at all.

Full backward compat: older runtimes strip --ws-only and fall back to
regular serve (still headless, just through uvicorn).
2026-08-25 13:27:28 +05:30
Clark Vines b637ee0fc6 fix(desktop): keep UI scale across in-page route navigation
Desktop is a HashRouter over one file:// document, so every route is a
distinct URL to Chromium's per-URL zoom store. A route the user never
zoomed on has no record at all and resolves to the host default (100%) —
that is every fresh session and every never-visited settings tab.

In-page navigation fires neither did-finish-load nor any window event,
so nothing re-asserted the persisted level. The window dropped to 100%
while the Appearance control kept reading the chosen scale, because the
renderer only learns of zoom changes through 'hermes:zoom:changed',
which never fired. Touching the setting sent a fresh apply, which is
why it appeared to fix itself.

Re-assert the persisted level on main-frame did-navigate-in-page.
Verified on real Electron 40.10.2 / Chromium 144 (win32): a recordless
hash route reports 100% at the event, so the existing drift-guard sees
the drop and re-applies, and still no-ops when the route's record
already matches.

Fixes #48658
Fixes #38854
Fixes #79863

Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
2026-08-24 22:01:47 -05:00
Brooklyn Nicholson 5ef1409f50 fix(desktop): say why window enumeration failed instead of swallowing it
`read_window_below` answers "could not enumerate windows on this system" on
macOS and Windows whatever went wrong, and the three failure paths behind it
discarded their errors — so a report where the HUD could see nothing had no
way to distinguish the module failing to load, the helper failing to spawn,
and the OS answering with nothing. Three different fixes, one sentence.

Enumeration now returns the reason, the tool's error carries it, and the HUD's
game-overlay watch logs it once before it gives up (it retries twice and then
goes quiet forever, which is the other half of why the log said nothing).
Linux keeps its environment-derived advice, which is more actionable than the
raw exception.
2026-08-24 20:15:03 -05:00
hermes-seaeye[bot] a251e87d82 fmt(js): npm run fix on merge (#94140)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-24 19:18:19 +00:00
Brooklyn Nicholson 652f5d740c fix(desktop): sort HUD windowing imports for eslint 2026-08-24 14:11:49 -05:00
Brooklyn Nicholson d7202d64ef refactor(desktop): derive HUD OS behavior from one windowing profile
Move, ignore-mouse, placement, resize edges, snap, cursor feed, and
overlay promote all read the same Ozone-normalized capabilities instead
of re-deriving linux/Wayland/X11 at each call site.
2026-08-24 14:11:49 -05:00
hermes-seaeye[bot] 74ad422d50 fmt(js): npm run fix on merge (#94046)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-24 17:03:13 +00:00
Brooklyn Nicholson 7e500d2ead fix(desktop): float and pin the HUD on Hyprland
Omarchy tiles the HUD like any other toplevel, so always-on-top and
xdg_toplevel.move never apply. Ask Hyprland to float+pin after map,
trying classic dispatch then Lua for 0.55+ configs.
2026-08-24 11:57:38 -05:00
Teknium d08f9e14f6 fix(desktop): never kill a healthy backend on a claim probe failure; surface real stderr (#93608)
A start-marker probe failure (Get-Process timing out on a PowerShell 5.1
cold start, #87169) in claimBackendChild used to stop the freshly spawned
backend and rethrow — killing a healthy backend, triggering the renderer's
repair respawn, and looping. And because stderr piping only attached after
the claim, every before-ready failure surfaced as a bare exit code.

- extract probe + claim policy into electron/backend-claim.ts:
  processStartMarker/execText (moved verbatim from main.ts), probeStartMarker,
  and a pure claimDecision(childAlive, probe) a Windows CI lane can drive
  with real PowerShell
- probe failure + LIVE child now degrades to PID-only identity
  (pid-only:<pid> marker, WARNING logged), matching the existing
  createParentStartMarkerResolver degrade pattern; processIdentityMatches
  verifies degraded identities by PID liveness (command check still layers
  on top in backendIdentityMatches)
- probe failure + DEAD child keeps the fail-closed throw, now carrying the
  child's buffered stderr/stdout tail
- ring-buffered ~8KB output tail attached at spawn time in BOTH spawn paths
  (pool + primary); tail appended to claim errors, before-ready exit
  messages, and backend-ready's exited-before-port-announcement errors so
  the real exit reason reaches desktop.log and the boot UI
- tests: claimDecision matrix (degrade test fails against the old
  stop+throw behavior), real processStartMarker probe, ring-buffer caps,
  and output-tail suffixes on backend-ready exit errors
2026-08-24 09:55:25 -07:00
howdeploy 81baae6bc3 fix(desktop): polish HUD movement and resizing on X11 2026-08-24 06:48:10 -05:00
Brooklyn Nicholson b595fcd5e1 fix(desktop): keep the Linux HUD clickable and recoverable
X11 cannot restore a window that has ignored the mouse, so stay solid
there. Native Wayland keeps click-through via the cursor poll. Add
desktop.ozone_platform_hint so COSMIC users can opt into XWayland for
always-on-top, and a layout reset that restores the default size.

Co-authored-by: Codex Metatron <47930664+BlakeB254@users.noreply.github.com>
Co-authored-by: DeseretSaint <202557515+DeseretSaint@users.noreply.github.com>
Co-authored-by: Shawn Wang <32839114+enwaiax@users.noreply.github.com>
2026-08-24 06:48:10 -05:00
Brooklyn Nicholson fab0de1c5b fix(desktop): debounce and re-verify zoom on Linux Wayland
Focus events fire for intra-app shifts on Wayland and Cosmic tiled
resizes can drop a just-applied zoom. Debounce focus with resize/move
and re-check a few times after the window settles.

Co-authored-by: joe0508 <75520452+joe050860@users.noreply.github.com>
Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
2026-08-24 06:48:10 -05:00
Teknium c7f1f4d6e9 fix(desktop): teach the torn-bundle guard to see missing lazy chunks
missingRendererAssets only checked the module refs index.html itself names
(<script type=module> + modulepreload), so a torn install whose boot-critical
files were intact but whose lazy chunks were gone passed the generation check
and died minutes later on the first React.lazy() route with 'Failed to fetch
dynamically imported module' (#93479: syntax-diff-*, shiki-*, mermaid-embed-*).

Walk the generation's module graph: for every present JS chunk, parse its
inline __vite__mapDeps filename table (the lazy-import manifest Vite bakes
into each chunk) and check those files too, transitively and cycle-safe.
resolveRendererIndex now skips a lazy-chunk-torn candidate in favor of the
intact copy instead of shipping a delayed crash.

Tests cover the mapDeps parser (definition table vs index-only call sites,
CDN refs), the exact #93479 tear shape, transitive/cyclic walks, and the
torn-vs-intact preference end to end.
2026-08-24 03:22:38 -07:00
Teknium 73af57a2ae fix(desktop): prefer the unpacked web dist over the asar-internal renderer index when packaged
The renderer index resolver tried APP_ROOT/dist/index.html — inside app.asar
when packaged — before the app.asar.unpacked copy that asarUnpack (dist/**)
ships and that resolveWebDist() already prefers for the embedded dashboard.
Loading the asar-internal index is how lazily imported chunks (syntax-diff-*,
shiki-*, mermaid-embed-*) end up fetched from a path that cannot serve them,
killing the workspace pane (#93479).

Reorder the candidate ladder to prefer the unpacked web dist when packaged,
following the unpackedPathFor/resolveWebDist precedent. All window loaders
(main, overlay, quick) share resolveRendererIndex, so one reorder covers
every surface. Dev behavior is unchanged: outside an asar both candidates
collapse to APP_ROOT/dist and keep the original order.
2026-08-24 03:22:38 -07:00
hermes-seaeye[bot] 91e867631e fmt(js): npm run fix on merge (#93566)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-24 05:11:39 +00:00
hermes-seaeye[bot] aff18ae21e fmt(js): npm run fix on merge (#93563)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-24 05:05:16 +00:00
Michael Nguyen 631148755f test(desktop): cover registered file fallback routing 2026-08-23 21:59:22 -07:00
Michael Nguyen 6eb77df1aa fix(desktop): route SSH media through active connection 2026-08-23 21:59:22 -07:00
Teknium 0f2416523e style: import order in main.ts (perfectionist/sort-imports) 2026-08-23 21:12:26 -07:00
Teknium feb419246d style: satisfy curly rule in api-transport (eslint --fix) 2026-08-23 21:12:26 -07:00
Teknium f114f4ea63 style: blank line between node and external import groups (perfectionist/sort-imports) 2026-08-23 21:12:26 -07:00
Teknium 859010b94e fix(desktop): gate transport retries to idempotent or provably-unsent requests
Follow-up hardening on #92977 (issue #92976). The cherry-picked retry
wrapped every verb, so an ECONNRESET arriving after the backend had
already processed a POST (prompt submitted, session created) would
silently double-submit on retry.

- Extract the transport policy into electron/api-transport.ts so it is
  unit-testable without Electron: keep-alive agent pools, transient
  error classification, and a verb-gated withRetry.
- Retry rule: GET/HEAD/OPTIONS retry on any transient transport error;
  POST/PUT/PATCH/DELETE retry only when the request provably never
  reached the server (connect-phase failures like ECONNREFUSED /
  ENOTFOUND, or an error thrown before the body was flushed —
  requestState.bodySent === false). Ambiguous resets after the body
  went out surface to the caller; when in doubt, don't retry.
- Separate keep-alive pools for JSON calls vs streaming downloads so
  long downloads can't starve latency-sensitive JSON calls.
- Destroy pooled agents on app will-quit.
- Tests: shouldRetryRequest truth table, withRetry behavior, plus LIVE
  transport tests against real misbehaving node HTTP servers: a GET
  burst where the server resets keep-alive sockets (bare attempt fails,
  retried succeeds) and a POST whose socket is RST after server-side
  processing (hit counter stays 1 — no double submit).
2026-08-23 21:12:26 -07:00
KHALIDagara 1cc76fce3a fix(desktop): harden Hermes API transport 2026-08-23 21:12:26 -07:00
Brooklyn Nicholson 3c19644419 feat(desktop): HUD game-overlay mode
While a fullscreen app owns the screen, the HUD becomes an in-game chat frame:
the idle bar steps back to a glanceable opacity, and the transcript is held
open for as long as the game is there rather than fading on a timer — you look
back at a chat log during a lull, not while the text happens to be fresh.

Detection is a pure pass over the same front-to-back window enumeration
read_window_below uses (electron/hud-game-overlay.ts); main polls it while the
HUD is open and pushes changes to the renderer, which owns the treatment. Two
details the enumeration forced:

- Hysteresis. Entering needs the game to be what the user is actually looking
  at, so a windowed app on top vetoes it. Staying only needs the game to still
  exist: clicking the HUD to type de-foregrounds the game and floats every
  other open window above it, which otherwise dropped overlay mode at the
  moment the user engaged with it.
- The last state is replayed on did-finish-load. The watch pushes only on
  change and its first tick fires at window creation, before the renderer has
  mounted its listener, so a HUD opened over an already-fullscreen game
  consumed its only message and sat at 'no game' forever.

The band itself is reworked for living over someone else's window:

- Light-on-dark unconditionally. The theme's near-black body ink is unreadable
  over a dark game, and every attempt to gate the light ink on some
  condition — focus, then the game flag — produced a state where it evaluated
  false and the words went black on black. The sheet is a dark scrim in every
  theme so white is always right; anything that paints its own light surface
  (a clarify question, an approval card, a code block, a form control) opts
  back into theme ink by re-pointing the ink variable, matched on the fill it
  paints rather than the feature it belongs to.
- Your own lines are gold rather than bubbled. With no card the log otherwise
  reads as one voice; blue and purple are what most game UIs use for their own
  text, so they disappear into the background.
- The scrollback ramps out at the top instead of being cut off, masked on the
  scroller (the band is a static box — its rows overflow the thread viewport
  nested inside it, so a mask on the band ramps over empty space).
- The sheet is inset under the bar, so its square top corners no longer poke
  out past the bar's rounded ones.
2026-08-23 22:34:38 -05:00
Brooklyn Nicholson 433f518cad fix(desktop): Windows HUD paints opaque white
setBackgroundMaterial on a transparent window permanently kills per-pixel
alpha on Win11 — every transparent pixel composites as opaque white, so the
HUD showed a white slab instead of the desktop behind it. Verified against a
minimal repro on Electron 40.10.2: the break happens with ANY material value
including 'none', which is exactly what the idle HUD asks for, and neither
'auto' nor a follow-up setBackgroundColor('#00000000') restores it.

The DWM backdrop and window transparency are mutually exclusive, so the
Windows HUD keeps the CSS tint its sheet already paints and skips the native
frost. macOS is untouched: setVibrancy composites correctly.
2026-08-23 22:34:38 -05:00
Brooklyn Nicholson 1c75e05982 fix(desktop): resolve get-windows from the staged copy first
window-below asked node_modules for get-windows, whose lib/windows.js locates
its native binding through preGyp.find() — by HOST platform. When the tree was
installed on one OS and Electron is running on another (a WSL-hosted dev run
driving a win32 Electron), pre-gyp picks the host's slot, ignores the correct
binding sitting beside it, and upstream's fail-soft path returns no-op stubs.
Enumeration then reports 'unavailable' on a machine that answers perfectly
well, which silently disables read_window_below.

scripts/stage-native-deps.mjs already writes a staged lib/windows.js that
requires its binding directly, so prefer it and keep the bare import as the
fallback.
2026-08-23 22:34:38 -05:00
hermes-seaeye[bot] 8c8193dce5 fmt(js): npm run fix on merge (#93429)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-24 01:49:17 +00:00
Brooklyn Nicholson aaf63220ca fix(desktop): heal v1/v2 connection drift instead of re-homing onto local
migrateV1ToRegistry runs exactly once, only when connections.json is absent.
A user who was local at that moment and pointed Settings -> Gateway at a
remote afterwards gets a live remote the registry cannot name: the descriptor
resolves to no connectionId, primary still says 'local', and the boot-time
launch pick force-switches the window onto a fresh local backend seconds after
the sessions list paints. That backend has no provider, so onboarding pops.

Reconcile on read: when the v1 global route names a remote with no matching
registry entry, register it and adopt it as primary/last-used, then persist so
the repair happens once. Narrow on purpose — an already-registered route is
left alone even when primary names something else, because that is the user's
pick in the Connections panel, not drift.

Replaces the hand-edit-connections.json workaround users have been trading.
2026-08-23 20:26:46 -05:00
Brooklyn Nicholson c5bf3b7ce3 test(desktop): replace apply/liveness source-regex assertions with behavior tests
The salvaged hardening tests matched main.ts source text to assert that
fetchConnectionStatus reaches for a bearer and that Apply preflights before
persisting. A rename breaks them while a real auth regression that keeps the
substrings passes.

Make the preflight a first-class option on applyConnectionConfigAtomically so
its ordering is observable, and assert it through the seam: preflight runs
before either write, and a rejected preflight leaves both stores and the
activation untouched.
2026-08-23 20:26:46 -05:00
Alessandro Levantini 96e5898542 fix(desktop): synchronize applied gateway registry 2026-08-23 20:26:46 -05:00
Alessandro Levantini 6a25ec0758 fix(desktop): authenticate remote liveness probes 2026-08-23 20:26:46 -05:00
Teknium b0af119963 fix(desktop): resolve the owning remote profile before a hint-less session read falls through to local
Fixes #85834 (Electron REST intercept fall-through). The
/api/sessions/{id}[/messages] intercept in electron/main.ts required an
explicit ?profile= (or request.profile) to route a read to its remote owner;
callers without a hint fell straight through to the LOCAL backend and 404'd
on its state.db even though the session lives on a configured remote — while
the list endpoints happily showed the row (remoteSessionList tags s.profile).

When no explicit profile resolves, consult the same remote session lists the
list endpoints use to find the owning profile (matching id or lineage root
id), memoized for 30s so a transcript+messages burst costs one sweep. Only
when the id is genuinely unknown remotely does the request fall through to
local, exactly as before. Pure lookup lives in profile-session-routing.ts
with unit tests (owner hit, lineage-root match, null on miss/dead
remotes/no remotes).

Maintainer commit (cluster salvage).
2026-08-23 17:43:39 -07:00
HexLab98 19e56b446b test(desktop): cover unsigned OAuth latch vs needsOauthLogin-only retry 2026-08-23 19:26:42 -05:00
HexLab98 48f2ccb65c fix(desktop): latch unsigned OAuth boots so the Sign in overlay stays put
No token and no cookie cannot self-heal. Tag that throw with
isReauthRequired so boot stops retrying and Sign in stays clickable.
Leave needsOauthLogin-only ticket 401s retryable for AT/RT rotation.
2026-08-23 19:26:42 -05:00
hermes-seaeye[bot] f303c695d7 fmt(js): npm run fix on merge (#93300)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-23 23:14:28 +00:00
Andrew Fiebert f2c204d689 fix(desktop): stop transient remote ticket blips locking the chat
Post-boot WebSocket ticket mint failures and prolonged reconnects were
promoting into the full-screen "Hermes couldn't start" overlay, locking
users out of reading/drafting during brief 1–3 minute remote flaps.

- Ignore non-reauth boot-progress errors after a healthy cold boot
- Escalate prolonged transport reconnects with a non-blocking toast
- Soft-reset remote liveness rebuilds (no boot UI reset)
- Retry transient ws-ticket mints; auth rejections still fail fast
2026-08-23 16:09:23 -07:00
hermes-seaeye[bot] fc0b2a0ab3 fmt(js): npm run fix on merge (#93123)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-23 18:38:31 +00:00
kshitijk4poor 4019518edd fix(desktop): stop no-op adapter swaps notifying the thread runtime + derive backend venv from the selected interpreter
Two independent desktop-boot/runtime bugs found driving the app over CDP
against current main, each pinned by a regression test:

1) Adapter no-op notify loop: IncrementalExternalStoreThreadRuntimeCore.
   __internal_setAdapter's fast path (same isRunning + same messageRepository)
   called _notifySubscribers() unconditionally. ChatRuntimeBoundary passes a
   fresh adapter literal every render, so any subscriber whose notification
   re-renders the boundary loops render->setAdapter->notify->render until
   React kills the tile with 'Maximum update depth exceeded' (reproduced live
   on every bot-profile switch; session tile dies behind its error boundary).
   Now the fast path notifies only when extras/suggestions/capabilities
   actually changed.

2) Dual-venv interpreter mismatch: findPythonForRoot() prefers .venv over
   venv, but createPythonBackend() hardcoded venvRoot=root/venv for
   PYTHONPATH. A checkout with BOTH venvs (dev .venv 3.12 + install venv
   3.11) got a 3.12 interpreter with 3.11-compiled native wheels on
   PYTHONPATH and died on the first import (pydantic_core) before the
   gateway bound - the renderer then showed 'Gateway offline' on every
   profile. venvRootForPython() now maps the selected interpreter back to
   ITS venv; root/venv remains the fallback for system pythons only.
2026-08-24 00:04:10 +05:30
hermes-seaeye[bot] f377140e3d fmt(js): npm run fix on merge (#92815)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-23 08:45:37 +00:00
Teknium ecf63ad3f3 fix: desktop plugins over 512 KiB no longer load truncated
Runtime desktop plugins read their source through hermes:readFileText,
the preview IPC that silently truncates at TEXT_PREVIEW_MAX_BYTES
(512 KiB) — a larger plugin.js evaluated as a partial file (cryptic
syntax error, or worse, a half-module that parses).

- electron/main.ts: dedicated hermes:readPluginSource handler — full
  read, 16 MiB cap enforced as a hard EFBIG via resolveReadableFileForIpc
  (same path hardening + sensitive-file blocking), never truncation.
- preload.ts / global.d.ts: bridge + types (optional — older shells).
- runtime-loader.ts: loadDiskPlugin reads via readPluginSource; on older
  shells falls back to readFileText but FAILS LOUDLY on truncated:true
  (error toast + error inventory row) instead of evaluating a partial
  file. Existence probe keeps the preview read (metadata is enough).
- tests: full-read path, loud old-shell truncation failure (sabotage-
  verified), small-plugin fallback.
2026-08-23 01:39:50 -07:00
Teknium e6962b818c style: eslint --fix across src/ + electron/ (CI lints the full tree); drop unused destructure 2026-08-22 22:35:55 -07:00