Commit Graph

4143 Commits

Author SHA1 Message Date
lepetitprince716-prog fc323cedf5 feat(desktop): add NSScreenCaptureUsageDescription to macOS bundle
Without the purpose string, macOS shows a bare Screen Recording
prompt when the desktop app (or a plugin driving desktopCapturer /
ScreenCaptureKit) first requests screen access, and some flows deny
silently instead of prompting.

Verified: package.json parses (python json.load).
2026-08-25 23:33:46 -07:00
Junior 8d27e1dfd0 fix(desktop): declare macOS calendar and reminder permissions 2026-08-25 23:33:46 -07:00
hermes-seaeye[bot] bc943d5672 fmt(js): npm run fix on merge (#95299)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-26 06:20:59 +00:00
Teknium 71d804d005 fix(desktop): sort titlebar-overlay-width import before window-connection-route (lint) 2026-08-25 23:15:49 -07:00
Jeremy cab6c4f70b fix(desktop): fail closed when messaging DELETE cannot resolve an owner
A listed profile-less row in a multi-profile setup must not DELETE/archive against the primary backend. Unresolved ownership keeps the row, pins, and unread state and never calls the mutation.
2026-08-25 23:15:49 -07:00
Jeremy add24666d5 fix(desktop): prefer messaging/cron slice when restoring a dual-listed session 2026-08-25 23:15:49 -07:00
Jeremy 718654a9a2 fix(desktop): route messaging session DELETE/archive to owning profile 2026-08-25 23:15:49 -07:00
Mabolla 7944ad2168 fix(desktop): isolate SSH routing by window 2026-08-25 23:15:49 -07:00
Mabolla d77114d0d0 fix(desktop): align SSH terminal routing precedence 2026-08-25 23:15:49 -07:00
Mabolla a70e7b0ca7 fix(desktop): route terminals through registry SSH 2026-08-25 23:15:49 -07:00
Mabolla 24ab1acbe0 test(desktop): cover v2 registry SSH terminal scope 2026-08-25 23:15:49 -07:00
Mabolla d2c8727db5 fix(desktop): resolve active v2 registry SSH scope 2026-08-25 23:15:49 -07:00
Shaq Wu 120430be47 fix(desktop): retain routed socket through turn completion 2026-08-25 23:15:49 -07:00
chelsealong 6b3977b577 fix(desktop): decide image/file byte-upload by the session's own connection, not ambient
uploadComposerAttachment's remote/local decision (image.attach vs
image.attach_bytes) read $connection.get()?.mode — the window's ambient
connection — at all three call sites. Since the multi-connection
routing work landed, a session can be owned by a different, registered
connection than the ambient one (Bot Mode, the unified Sessions list):
the RPC itself already routes to that owner via requestForSessionProfile,
but the byte-vs-path decision did not, so a local-ambient window chatting
in a remote-owned session shipped a client-local composer-images path to
a backend that can't read it (#94640).

isSessionRemote() resolves the session's own owner route (falling back to
ambient only when no owner route is known, matching the existing RPC
routing behavior in session-states.ts) and replaces the three ambient
reads in use-prompt-actions/index.ts, session-tile-actions.ts, and
user-edit-composer.tsx.
2026-08-25 23:15:49 -07:00
Kavyrocom 76af834ddd fix(desktop): route ambient SSH profile deletion to remote 2026-08-25 23:15:49 -07:00
Teknium 682a34a7c6 test(desktop): isolate warm-cache mapping describe from prior gateway mock traffic
Follow-up for salvaged PR #93451: main grew branchStoredSession tests (#93444)
that drive the same hoisted requestGatewayForProfile/ForAgent mocks, so the
not-called assertions in the warm-cache describe saw stale recorded calls.
2026-08-25 22:51:31 -07:00
Leonid Skorobogatyy 2a4460b58b fix(desktop): guard gateway-open resume against active fresh-draft transition (#68594)
When switching profiles, useRouteResume() could treat the target profile
gateway opening as a resume command for the old routed session before
React Router commits the /new pathname. The gatewayBecameOpen trigger
was not guarded by the existing freshDraftReady discriminator, unlike
the stuckOnRoutedSession guard on line 137 which already uses it.

Fix: add && !freshDraftReady to the gatewayBecameOpen condition in
shouldResume (line 143). This is consistent with the existing pattern
and preserves reconnect behavior (freshDraftReady=false during normal
reconnects).

Adds a regression test that simulates the exact race: profile switch
clears refs and sets freshDraftReady=true, gateway closes, then profile
B gateway opens while pathname is still /session-a. Asserts no resume
fires.
2026-08-25 22:51:31 -07:00
echoes666 4b21f970aa fix(desktop): release profile activation lease on dial failure
Clear the profile-door activation lease in a finally block whether the
secondary dial succeeds or rejects. Preserve reconnect scheduling and the
original error propagation.

Add a regression proving a rejected activation can be pruned immediately.
2026-08-25 22:51:31 -07:00
echoes666 c06d3c0bd9 fix(desktop): surface profile-switch dial failures instead of silently activating
Rebase of #81165 onto post-#87600 main, slimmed to the error-surfacing UX
half as requested - the silent-misroute half already landed via #87600.

- ensureGatewayForProfile: keep scheduleReconnect() on a failed secondary
  dial (transient failures still self-heal) but rethrow so the caller can
  surface the failure instead of falling through to setActive() with a
  closed socket (#81094). openSecondary logs the dial target and rethrows
  the ORIGINAL error so reconnectSecondary's message-based fail-stop
  classification ("No connection with id", "no longer exists") keeps working.
- profile.ensureGatewayProfile: propagate the rejection to await-callers
  (session actions, slash commands surface it in their own flows); the three
  fire-and-forget call sites (selectProfile, newSessionInProfile, voice
  wiring) get explicit .catch(notifyError) so the failure is always visible.
- Tests: gateway.test.ts pins rethrow-without-activation plus backoff
  self-heal; profile-switch-failure.test.ts pins the profile-door
  propagation; gateway-shared-remote's pooled-reconnect test updated to
  the new contract (reject first, publish once the backend returns).

Verified: tsc --noEmit clean; 65 tests across the gateway*/profile*
suites pass; eslint and prettier clean on touched files.
2026-08-25 22:51:31 -07:00
cmoiccool f3ae1a0c3a refactor(desktop): use shared local connection id 2026-08-25 22:51:31 -07:00
cmoiccool 7d6bf56c7b fix(desktop): preserve profile overrides on local source picks
Treat the explicit local registry source like the legacy profile-only path so per-profile remote overrides still resolve. Keep non-local registry sources connection-scoped and cover both profile selection entry points.
2026-08-25 22:51:31 -07:00
Tom Reinelt 613822afff fix(desktop): invalidate stale runtimes before profile reopen
Remember secondary scopes across renderer pool pruning and clear their process-local runtime bindings before a later socket can publish open. This prevents eager session RPCs from racing a respawned profile backend with an ID minted by the previous process.
2026-08-25 22:51:31 -07:00
Luke Roberts 893c8b1fdd fix(desktop): preserve remote session routing 2026-08-25 22:51:31 -07:00
arya abe584288b fix(desktop): preserve registry owner for session sends 2026-08-25 22:51:31 -07:00
arya 26932ea0bf fix(desktop): retain registry session ownership 2026-08-25 22:51:31 -07:00
arya 1ec32e7385 fix(desktop): reuse primary during owned route activation 2026-08-25 22:51:31 -07:00
arya 6c0ddecf66 fix(desktop): reuse registry primary for owned session RPCs 2026-08-25 22:51:31 -07:00
Zeus-Deus 502298ef62 fix(desktop): preserve recovery refresh ownership 2026-08-25 22:51:31 -07:00
Zeus-Deus 693dd5d042 fix(desktop): guard stale refresh ownership 2026-08-25 22:51:31 -07:00
Zeus-Deus b3bdf0c816 fix(desktop): guard async switch publications 2026-08-25 22:51:31 -07:00
Zeus-Deus ee3dc554f5 fix(desktop): enforce gateway switch publication ownership 2026-08-25 22:51:31 -07:00
Zeus-Deus f23b3c805c fix(desktop): preserve switch loading ownership 2026-08-25 22:51:31 -07:00
Zeus-Deus 559a56c360 fix(desktop): preserve gateway switch recovery ownership 2026-08-25 22:51:31 -07:00
Zeus-Deus c2bce09d48 fix(desktop): recover failed gateway switch setup 2026-08-25 22:51:31 -07:00
Zeus-Deus b687c6b2c1 fix(desktop): cancel timed-out gateway activations 2026-08-25 22:51:31 -07:00
Zeus-Deus 300fcdbfbd fix(desktop): harden gateway-switch commit ordering for stalled and overlapping switches (#93937)
Review follow-up. The two-phase switch closed the runtime-id leak for a
single switch, but two shapes still broke the wipe→publish ordering:

Overlapping: click A commits (wipes) and waits for its activation behind
the profile-store mutex; click B finishes its dial and wipes too; then A's
activation lands and publishes A AFTER B's destructive wipe, and A's
endGatewaySwitch() dropped the (boolean) barrier while B was mid-commit.
Now the wipe runs INSIDE the serialized section via a `beforeActivate`
commit hook on ensureGatewayAgent, synchronously right before the socket
is activated; the hook re-checks the click revision and declines when
superseded, so a queued-then-superseded switch neither wipes nor
activates. The barrier is token-owned (latest switch wins), and the mutex
wait is a loop so waiters that wake together can't run interleaved.

Stalled: a wedged spawn / ticket mint / IPC (the #93454 class) left the
spinner up, swallowed later clicks on the same source, and — inside
ensureGatewayAgent — latched the mutex and the barrier. Every await is now
bounded (dial, activation, descriptor lookups, setLastUsed) via a shared
withTimeout helper extracted from use-gateway-boot. A commit that timed
out after the new source was already published counts as committed; one
that stalls after the wipe lowers the barrier and repaints the source
that is still active.

Tests: queued-then-superseded switch, dial that never answers (and retry
not swallowed), activation stalled after/before publication, barrier
ownership, commit-hook ordering inside the mutex, bounded descriptor
lookup releasing the mutex. All fail against the previous revision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 22:51:31 -07:00
Zeus-Deus 5472d1d66c fix(desktop): commit gateway switches before publishing the new source (#93937)
The Sessions sidebar switcher (selectConnection) activated the target
gateway first and wiped session bindings afterwards — across an IPC
round-trip — so route/session effects saw the new source while
$activeSessionId still named the previous backend's runtime id, sent it
to a backend that never minted it, and got "session not found". The
Settings → Gateway apply (softSwitch) never had this problem: it raises
$gatewaySwitching, runs beforeConnectionSwitch and wipes before dialing.

Give both doors one commit point. store/gateway-switch gains
beginGatewaySwitch()/endGatewaySwitch(): barrier up, the registered
machine-context reset, session wipe — in one synchronous step.
useGatewayBoot registers its beforeConnectionSwitch/refreshSessions with
the store and softSwitch goes through the same entry point.

selectConnection becomes two-phase: dial the target WITHOUT activating
(openGatewayAgent → openGatewayForAgent with an activation lease, so a
live-work recompute can't prune it mid-spawn), then beginGatewaySwitch()
and activate the already-open socket with no await between the wipe and
the publication. A dead target fails with the current workspace intact;
a superseded click never activates its target; an activation that fails
after the wipe repaints the still-active source instead of leaving the
sidebar skeleton.

Tests: real-store end-to-end regression (real useGatewayBoot + gateway
registry + selectConnection over fake sockets) — red on the previous
code with the leaked runtime id observed at publication — plus store
ordering/failure-path, commit-point and prune-lease coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 22:51:31 -07:00
Jack Lau a7b8281a16 test(desktop): pin the clock in the session-row timestamp test
The sidebar row's Tip trigger test derives its fixture from the wall clock:

    const startedAt = Math.floor(Date.now() / 1000) - 5 * 60
    ...
    expect(age.getAttribute('aria-label')).toMatch(/^5m, Today at /)

"Five minutes ago" is only today when the run does not straddle local
midnight. Between 00:00 and 00:05 the timestamp falls into the previous
day, formatMessageTimestamp correctly returns the yesterday label, and
the test fails on a day boundary it was never written to exercise:

    AssertionError: expected '5m, Yesterday at 11:56 PM'
                    to match /^5m, Today at /

This is a real CI failure, not a theoretical one - it took down a
check:test:ui shard on an unrelated desktop PR at 00:01 UTC, and it will
do so for any PR whose shard happens to land in that five-minute window.

Pin the clock to local noon before deriving the timestamp so the fixture
can never cross a day boundary. Only Date is faked (toFake: ['Date']),
so the component's own timers - the running arc and the tooltip open
delay - keep running for real; the neighbouring tooltip tests that
advance timers are unaffected. The describe block gains the
useRealTimers teardown its sibling already had.

The production formatter is not changed: rendering "Yesterday at 11:56
PM" for a session started five minutes before midnight is correct, and
the assertion is about the label's composition, not about which day it
names.
2026-08-25 22:00:11 -07:00
Kudakwashe Paradzayi 458f26c0dc fix(desktop): stop the Bots roster hanging on a live profile
profiles.list opened every profile state.db as a writable SessionDB,
which waits out write-lock patience while that profile's backend is
mid-turn. The desktop RPC timed out and Bot Mode's infinite React
Query retry kept the sidebar on a spinner.

Inspect those DBs read-only and bound roster retries so names still
paint.
2026-08-25 21:59:38 -07:00
hermias1 7d9953d34f fix(desktop): allow microphone from macOS setup launcher 2026-08-25 21:59:23 -07:00
Teknium bc4ebcf3b0 test(bootstrap): pin the exit-2 self-marker heal contract + on-demand windows-latest Rust lane
The heal decision is extracted into should_heal_self_marker_refusal()
so the contract is testable: heal ONLY on exit 2 + a marker naming this
process. Five tests pin it — self-owned heals, foreign owner (real live
sibling process) never heals, missing/garbage marker never heals,
non-exit-2 never heals, and the full acquire -> refuse -> drop-claim ->
retry-precondition lifecycle with a real UpdateMarkerGuard.

windows-rust-e2e.yml mirrors the wine2e pattern: fires only on
wine2e-rust/** pushes, runs the crate's cargo test --lib on
windows-latest (the shipping platform). The permanent Linux lane stays
authoritative for the unix-gated pipe-drain fixtures.
2026-08-25 21:59:19 -07:00
Teknium d8d3a219fe docs(bootstrap): correct marker_owned_by_self justification for current live_marker_owner semantics
Main's live_marker_owner has adopted self-owned markers since
160586ff8/dbc2a9c8e (#74761), so the cherry-picked comment's claim that
it 'maps self-ownership to None' is stale. The raw read is still the
right tool — the heal needs the single fact 'does the marker name our
PID' without age/liveness policy folded in.
2026-08-25 21:59:19 -07:00
3x3xX3N0N 26c987c38d fix(install): heal the updater's self-owned-marker refusal loop on Windows
An updater binary spawns 'hermes update' while holding the update marker
with its own PID. A checkout that predates the HERMES_UPDATE_HANDOFF_PID
env fix (8c76fe19f) and the ancestor-pid fallback runs its pre-pull
update_lock.py, reads that marker as a live foreign update, and exits 2
— and the updater deliberately skips its retry for exit 2, so the
refusal loops forever: the update being refused is the one that ships
the fix, and the failure screen's Retry re-enters the same state.

Detect the case with a raw marker read (live_marker_owner deliberately
maps self-ownership to None, so it cannot answer this), drop our own
claim, and retry the child once with the marker absent. The guard
re-removes on Drop (idempotent) and the desktop is already gone at this
point, so nothing races the brief marker-free window.

Fixes #75788

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 21:59:19 -07:00
Brooklyn Nicholson b742be711a fix(desktop): skip Tip delay when moving between adjacent chrome
First hover still waits 200ms so a sweep does not flash a trail. After
a tip has opened, the next trigger within 300ms opens instantly.
2026-08-25 22:16:24 -05:00
BrunoBza 250faa69fb fix(desktop): keep the Cronjobs pane subscribed to roster hydration
RoutinesPane resolved its cron owner from a bare $lastRoster.get()
snapshot. BotsHomeView owns the roster fetch, so whenever the pane
mounted before that fetch landed (fresh boot ordering, renderer reload
resetting the atoms) it captured an empty roster forever: the pane
stayed pinned on "Cronjobs are unavailable until this agent appears in
the roster." and Create Cronjob silently no-oped until some unrelated
atom happened to re-render it (#94483).

Subscribe via useValue($lastRoster) instead, matching every other
consumer of the shared roster. Scoping intent is unchanged: a complete
focused owner without an exact roster row still fails closed rather
than routing cron reads/mutations through a stale selection or an
unscoped profile name (contracts in routines-selected-bot.test.mjs).

The source contract in focused-bot-highlight.test.mjs pinned the bare
.get() shape; it now pins the subscription form while keeping the
socket-home-atom prohibition that motivated it.

Fixes #94483
2026-08-25 16:34:12 -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
Teknium 903354587a test: pin CreateRoutineDialog owner-object label resolution (follow-up for salvaged #93572) 2026-08-25 16:31:02 -07:00
zhangfei1231231-sketch b73e057855 fix(hermes-bots): stop New Cronjob dialog crash when the owner is a roster object
CreateRoutineDialog receives routineCreateTarget() output, which is an
owner OBJECT for roster-scoped bots; wrapping it in {name: bot} rendered
'[object Object]' and broke the meta lookup keyed by object. Resolve the
label through the object-aware botRosterMeta() path instead.

(Salvaged from #93572; the defensive coercion inside displayName was
dropped in favor of fixing the call site only.)
2026-08-25 16:31:02 -07:00
Jack Lau fab931fa28 docs(desktop): name the timer drain in the effect that performs it
Review follow-up, comments and one type annotation. No behaviour change.

The unmount effect now clears pending timers, but its leading comment is
still entirely about the focus bus, so the cleanup reads as unrelated code
that happens to be in the same block. Say why it lives there: both concerns
are "this composer is going away", they unmount together by definition, and
a sibling unmount-only effect would only be a second place to forget.

In the regression test, the clearTimeout mock declared id as number. Nothing
that reaches it is a number: jsdom under node returns a Timeout object, which
is why the scheduled and cleared arrays are unknown[] and compared by
identity. The annotation documented a shape the test never sees, so it is now
unknown with the cast moved to the one call that genuinely wants a number.
2026-08-25 16:25:50 -07:00
Jack Lau 37a8c10ab1 fix(desktop): stop the edit composer's timers from outliving it
Confirming an inline edit unmounts UserEditComposer while its 200ms submit
latch is still pending, so the callback resumes on an unmounted tree and calls
setSubmitting. Five window.setTimeout calls in the component, none cleared on
unmount; the latch is the one with a delay long enough to reliably survive.

In the app the stray state update is an invisible no-op. In vitest it can land
after the test file's jsdom environment is gone, and React reaches for a window
that no longer exists:

  Test Files  570 passed (570)
       Tests  5434 passed (5434)
      Errors  1 error

  ReferenceError: window is not defined
   at resolveUpdatePriority  react-dom-client.development.js:1308:7
   at dispatchSetState       react-dom-client.development.js:9126:14
   at Timeout._onTimeout     user-edit-composer.tsx:606:9

That is an unhandled error, so the job exits 1 with every test passing, on
whichever PR happens to be running rather than on anything related. It reads
exactly like infrastructure noise, which is the reason to fix it rather than
re-run it.

The component already knows about this hazard. Its unmount effect opens with a
comment calling it "the one composer that routinely unmounts", and two of the
other timer callbacks carry defensive try/catch for the composer core being
torn down underneath them. The timers themselves were simply never cancelled;
this cancels them instead of surviving them, which removes the race rather
than tolerating it.

All five sites now go through scheduleTimeout, which records the id and drops
it when the callback runs. The existing unmount effect clears whatever is
left. Two useCallback dep arrays gained scheduleTimeout, which is stable.

One regression test, in the file the CI error originated in: spy on
setTimeout/clearTimeout, submit an edit, unmount, assert the latch id was
cleared. It fails when the clear loop is removed. The 200ms delay is matched
explicitly so unrelated library timers cannot make it pass by accident, and
ids are compared by identity because jsdom under node returns a Timeout object
rather than a number.

Fixes #92462
2026-08-25 16:25:50 -07:00