Commit Graph

878 Commits

Author SHA1 Message Date
kshitijk4poor db64ddb58e test(desktop): widen the execProbe event-loop test timeout
The 'execProbe keeps the parent event loop available' case asserts nothing
about timing; the 1s spawn budget only exists so a wedged child cannot stall
the run. A cold Windows CI runner can take longer than 1s just to start the
node child, which failed the test for reasons unrelated to what it guards.
5s keeps the safety bound without the flake.
2026-09-15 10:50:53 +05:30
kshitijk4poor 6915e9b4bc fix(desktop): keep the pool slot leased when a spawned start is superseded
runPoolBackendStart reused assertPoolEntryStillOwned at every cancellation
checkpoint, and that helper releases the local backend slot before it throws.
That is right before spawn (no child, nothing to wait for) but wrong at the
post-spawn checkpoints (after the port announcement, waitForHermes, token
adoption, WS probe): the lease was handed back while the superseded child was
still running, so a successor could spawn into an occupied slot, and the
caller's teardownFailedLocalBackend -> releaseLocalBackendSlotAfterExit then
found nothing left to release and became a no-op. This breaks the
pool-spawn-coordinator invariant that a lease is held until the child exits
or the start fails.

Add a `releaseSlot` option to assertPoolEntryStillOwned and pass
`{ releaseSlot: false }` at every site where `entry.process` is set. The
pre-spawn sites keep releasing. The post-spawn release now happens only via
teardownFailedLocalBackend (after the child has provably exited) or the
child's own exit handler.

No vitest case: the helper and its callers live in main.ts alongside the
module-level backendPool / localBackendLifecycle state and are not importable
from a unit test without extracting them; the lease-after-exit ordering
itself is already pinned by pool-spawn-coordinator.test.ts ('a rejected wait
keeps the slot occupied').
2026-09-15 10:50:53 +05:30
kshitijk4poor 844e5f2610 fix(desktop): do not cache a timed-out serve-support probe
The serve-support resolver caches the probe outcome per resolved runtime
for the process lifetime. That is right for a genuine "unknown
subcommand" exit, but a probe that died by timeout says nothing about the
runtime — only that this machine was slow right then (cold AV scan on
Windows, first Python import after boot). Caching that as `false` routed
a modern runtime through the legacy `dashboard` form until the app was
relaunched.

Export isTimeoutError from backend-probes and evict the cache entry on a
timeout so the next check re-probes; non-timeout failures stay cached.
One vitest case covers timeout → re-probe; the existing case still pins
non-timeout failure → cached.

Also replace the last synchronous read on the discovery path
(dashboard.py fast path) with fs.promises.readFile, matching the stack's
goal of keeping runtime discovery off the main event loop.
2026-09-15 10:50:53 +05:30
kshitijk4poor 09cf145ec8 refactor(desktop): tighten runtime-discovery async and attempt guards
isActiveRuntimeUsable relied on async-return flattening for the trailing
canImportHermesCli promise: correct today, but appending any further `&&`
operand would have made the expression truthy regardless of the probe.
Await the probe explicitly so the intent survives future edits, and mark
unwrapWindowsVenvHermesCommand async like its siblings since it always
returns a promise.

connectRemote used two inline isCurrentAttempt/throw pairs with the same
message that backendConnectionState.assertCurrentAttempt already emits,
and the third guard in the same function already uses the helper. Use it
in all three places so the superseded-attempt message has one source.

The fast-path/probe/cache explanation for serve support lived above the
resolver call site in main.ts while the logic lives in
backend-serve-support.ts; move it next to the code and leave a pointer.
2026-09-15 10:50:53 +05:30
jango 3952878c2a [verified] test(desktop): tolerate loopback peer reset
(cherry picked from commit fcbadf23c95d153b8a1f8d68fc531734050fadfe)
2026-09-15 10:50:53 +05:30
jango 5d3cb88dbc [verified] test(desktop): guard nonblocking runtime probes
(cherry picked from commit ee61bbfa311a0a99a1f8c14c18e3cdc97e51f3d1)
2026-09-15 10:50:53 +05:30
jango 783a06d070 [verified] test(desktop): trim runtime probe coverage
(cherry picked from commit 83b7fed46834988f66051f73dab5c65dbf47a672)
2026-09-15 10:50:53 +05:30
jango e270766fe5 test(desktop): detect transient stale spawns and native path separators
(cherry picked from commit 807ef07f71a3ddceb63c8ee1d688739ab6ab9d62)
2026-09-15 10:50:53 +05:30
jango 78399c88da test(desktop): verify probe responsiveness and stale-start cancellation
(cherry picked from commit c12f7e0f677a234e7e5057a13485e39346da0d03)
2026-09-15 10:50:53 +05:30
jango 5d9f83253f fix(desktop): keep runtime discovery off the main event loop
(cherry picked from commit d116c193ca790331acda9ebf594931ac4033e0fd)
2026-09-15 10:50:53 +05:30
kshitijk4poor 45760ac022 fix(desktop): match app.asar segment on either separator in resolveOutsideAsar
The redirect out of the archive used a literal '/app.asar/' replace, so the
staged get-windows specifier built with path.join on a Windows packaged build
(...\resources\app.asar\dist\...) was never rewritten and the helper spawn
would still land inside the archive. Mirror the app.asar(?=$|[\\/]) regex
main.ts already uses for the same job, and cover the backslash path in the
test. The dev-tree no-op case folds into the segment-boundary test so the
describe block stays at three cases.

Closes #88468 (with the preceding commit from PR #89633).
2026-09-15 10:49:40 +05:30
Douglas Jennewein 763489a8b4 fix(desktop): exec get-windows helper from app.asar.unpacked so read_window_below works on packaged macOS builds
`read_window_below` answers "could not enumerate windows on this system" on
every packaged macOS build. get-windows does its real work by exec'ing a Swift
helper whose path it derives from its own module URL, and execFile cannot run a
file that lives inside app.asar: the kernel sees the archive as a file, so the
spawn fails ENOTDIR. Electron does rewrite asar paths for child_process, and
only once that module has been pulled through the CJS loader, which this ESM
main process never does (every import here is `from 'node:child_process'`).

Both specifiers now resolve outside the archive. The staged copy added in
1c75e05982 is redirected, and so is the node_modules fallback, which needs
`import.meta.resolve` first to have a path to redirect. electron-builder
unpacks the whole get-windows directory, so the redirected path exists on the
real filesystem and the derived helper path is executable.

`/app.asar/` only ever appears as a complete path segment in a packaged build,
so outside one the replace is a no-op. Tests cover the packaged redirect, the
dev-tree passthrough, and an `app.asar-tools` lookalike that must be left
alone.

Verified on a packaged build, macOS 26.6.2 arm64, ad-hoc signed: the same
machine that returns the enumeration failure on unpatched main answers with the
window underneath and its title.

(cherry picked from commit dce34f626a0796fcd945aa3885384a1ec86c819f)
2026-09-15 10:49:40 +05:30
ethernet 14efb46089 fix(desktop): skip the film, keep the guided onboarding
HERMES_SKIP_INTRO=1 turned off the intro film but also silently killed the
entire guided first launch: the guide can only queue on the film's
completion edge (queueGuideAfterIntro requires hasSeenIntroReveal), so with
the film disabled nothing ever fired and the app booted straight into the
normal shell with the free-tier flag on.

The film-to-guide seam now fires without the film: beginOnboardingFlowWithout
Intro records the film as watched and queues the guide directly, so
HERMES_SKIP_INTRO skips exactly the film. A relaunch adopts the persisted
guide (loadGate sees cinematic + seen), and a later launch without the flag
cannot replay the film over the completed flow.

Verified: tsc -p tsconfig.json and tsconfig.electron.json clean; vitest ui
(onboarding-gate, onboarding, onboarding-never-forces-sign-in: 30 passed
incl. 2 new invariant tests) and electron (guest-onboarding-flag,
preload-flags: 6 passed) green.
2026-09-14 13:41:54 -04:00
kshitijk4poor 9b199246e5 fix(desktop): cancelAndWait resolves only once the composed drain settles
Every caller tears the SSH connection down right after `await
cancelAndWait(scope)`, so resolving on this call's own barrier alone let a
connection-apply teardown overlap the pool stop's still-running afterStop
teardown for the same key. Wait for the composed barrier instead; chain the
barriers (drain promises never reject, so this equals allSettled). The test
now waits a macrotask and asserts that neither the apply nor the new
bootstrap runs before the first teardown completes.
2026-09-14 20:22:49 +05:30
kshitijk4poor 2aca48b416 fix(desktop): same-scope SSH drains compose instead of replacing the barrier
A pool stop blocked in teardownSshConnection() holds drains[scope]; a
concurrent connection apply calling cancelAndWait() for the same scope
replaced that barrier with its own and, having nothing to drain, cleared the
map entry as soon as it finished. start() then saw no drain and began a new
bootstrap while the first SSH teardown was still running.

cancelAndWait() now composes with the drain already in flight and the entry
is cleared only when the composed barrier settles, so start() waits for
every active teardown. Regression test reproduces the pool-stop / apply
race; it fails on the previous coordinator.

Reported by ehz0ah on #110025 (follow-up to #106935).
2026-09-14 20:22:49 +05:30
kshitijk4poor 39ef1c1f3d refactor(desktop): keepalive redials with capped backoff; staleness derived, not counted
A dead tunnel was redialled every 2 s until the scope was torn down;
delays now double from the base to a 30 s cap and reset on open. The
generation counter duplicated what the entry map and entry.socket
already say (stop() removes the entry, connect() replaces the socket),
so abandonIfStale reads those instead. afterStop's unused entry
parameter is dropped.
2026-09-14 19:56:15 +05:30
kshitijk4poor e24b344b6c fix(desktop): a failed SSH teardown in the pool-stop fence is logged, not thrown
afterStop awaited cancelAndWait(teardownSshConnection) with no catch; the
idle reaper calls stopPoolBackend un-awaited, so a teardown rejection
became an unhandled rejection on the main process. Log it through the SSH
log and let the fence release.
2026-09-14 19:56:15 +05:30
kshitijk4poor fa67a4c9ea test(desktop): trim keepalive suite to two behaviour cases
Four registry tests overlapped: sibling independence is a two-line
assertion inside hold-until-stop, and the reject-missing-target case is
the setup half of the empty-scope primary case. Folding them keeps every
invariant covered (hold, no-reconnect-after-stop, sibling isolation,
''-is-a-real-key, baseUrl/token required) with fewer fixtures to keep in
sync. The pool-stop teardown fence is already covered by
pool-stop.test.ts::afterStop and ssh-bootstrap-coordinator.test.ts.
2026-09-14 19:56:15 +05:30
kshitijk4poor 109fbe097f refactor(desktop): drop the fail-open branches when WebSocket is not a function
Electron bundles a global WebSocket in the main process; a missing
constructor is a build/environment error, not a runtime state to route
around. The two silent `typeof WebSocketImpl !== 'function'` returns in
start()/connect() would turn that build error into an armed-looking
registry that never opens a socket, letting web_server_idle_exit retire
an owned SSH-isolated sibling with no log line — exactly the bug this
module exists to prevent. Let `new WebSocketImpl(url)` throw into the
existing catch, which logs and schedules a reconnect.
2026-09-14 19:56:15 +05:30
KoNit-K 5350f9021a test(desktop): trim SSH keepalive suite to four behaviour cases
Review banned source-reading main.ts greps and asked the remaining registry
cases down to ~4: hold-until-stop with no reconnect, empty-string primary,
reject missing target, and sibling isolation. Prettier the helper.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 19:56:15 +05:30
KoNit-K 134c5226af fix(desktop): hold the pool-stop fence through SSH teardown
Process-less SSH entries finish child exit immediately. Keep inFlight
and the bootstrap drain up until keepalive teardown completes, and
stop reading main.ts from tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 19:56:15 +05:30
KoNit-K be69a8d043 fix(desktop): tear down SSH lifecycle when a pooled backend is stopped
Idle-reaper and LRU eviction only called stopPoolBackend, which left sshConnections and the keep-alive WS armed on process-less remote descriptors.
2026-09-14 19:56:15 +05:30
KoNit-K e785128934 fix(desktop): hold SSH-isolated keepalive WS so idle-exit won't retire owned siblings
Desktop keeps live sockets on the profile-less backend while the --profile
sibling only sees short RPC sockets; idle-exit then retires the sibling and
the app restarts broadly. Arm a main-process /api/ws keep-alive for every
published sshConnections scope (including '') until teardown, without
treating spawn artifacts as liveness (#101626).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 19:56:15 +05:30
teknium1 5b0103ffdc refactor(desktop): extract terminalLcCtype so the Linux locale rule is testable
The picked fix inlined the platform ternary inside terminalShellEnv(), which
reads process.platform and can only be exercised by booting Electron. Lift it
into a pure terminalLcCtype(env, platform) that takes the platform as data
(root AGENTS.md: never fake the host OS) and pin the contract with one vitest:
Linux reuses LANG, falls back to C.UTF-8, respects an explicit LC_CTYPE; macOS
keeps the bare "UTF-8" it accepts. Red on origin/main (helper absent), green here.
2026-09-12 08:18:50 -07:00
NaviElLay 6f8d975fe7 fix(desktop): use valid LC_CTYPE on Linux terminals 2026-09-12 08:18:50 -07:00
hermes-seaeye[bot] 3e09e5a15f fmt(js): npm run fix on merge (#109094)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-12 13:20:20 +00:00
Teknium b0c383cdf7 fix(desktop): served profiles show running and route lifecycle to the multiplexer from a pooled local backend
Electron sends a local sub-profile's REST to its pooled `hermes --profile X serve` without
?profile=; inside that process the unscoped branches never reached the multiplexer rung, so a
profile served by the default multiplexer read as 'Messaging gateway stopped' on the system and
messaging pages, start/stop spawned a child that exited 78 while the UI reported success, and
restart ran `gateway restart` under X's HOME (same exit 78). Remote-backend topology was already
correct because its requests carry ?profile=.

Unscoped liveness/status/messaging now take the multiplexer rung for the process's own home;
lifecycle verbs resolve the own profile, refuse start/stop with 409 and restart the multiplexer via
-p default; Electron routes POST /api/gateway/{restart,start,stop} through the primary with
?profile= so the action lives on the backend the status poll asks and outside the pooled
backend's shutdown SIGTERM.
2026-09-12 06:13:44 -07:00
Teknium ab7f03049e refactor(desktop): large-paste writer in its own electron module, 3k threshold
Move writeComposerPaste out of electron/main.ts into composer-paste.ts
(placement gate: no new behaviour appended to the facade). Lower the
conversion threshold from 10k to 3k characters so a pasted stack trace or
log excerpt already becomes a chip. Trim the policy tests to two
invariants (strict threshold boundary; chip size label is byte-based) and
drop vendor references from code comments.
2026-09-12 05:09:01 -07:00
Teknium 22e3888543 Inspired by ChatGPT Work: convert large pastes into .txt attachments in the Desktop composer
Pasting more than 10k characters of plain text into the Desktop composer
now converts the content into a 'Pasted content (NN KB)' .txt attachment
chip instead of flooding the input, mirroring ChatGPT's large-paste
handling (OpenAI release notes, Aug 4 2026). Short pastes stay inline;
the exact text is preserved byte-for-byte in a Hermes-managed
composer-pastes file and rides the existing @file: attachment pipeline.
If the desktop bridge is missing or the write fails, the paste falls
back to inline insertion so nothing is ever lost.

Implements #66622.
2026-09-12 05:09:01 -07:00
hermes-seaeye[bot] f7cd8bf084 fmt(js): npm run fix on merge (#108814)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-12 04:36:01 +00:00
Siddharth Balyan cbe9e5b294 chore(desktop): literal comments across the onboarding flow (#108438)
* chore(desktop): literal comments in the guide script and runbooks

Comment-only change to onboarding-script.ts and setup-profile.ts. The module
headers now state the purpose and the constraints that shaped each file. The
notes beside the runbook strings keep one fact per sentence, or are deleted
when the string beside them says the same thing. The runbook text, the
persona, the option pills and the SOUL text are unchanged.

Both versions transpile to identical output with comments removed.

* chore(desktop): literal comments in the guide chat cards and stores

Comment-only change to the guided chat's cards, directive dispatcher, option
catalog, assembly module and chip. Metaphor and personification are replaced
by the name of the atom, effect or CSS property they stood for. Comments that
restate the code are deleted. Two stale facts are corrected in place: the
mini layout trees point at app/contrib/layout-presets.ts, and the skip button
sets the onboarding phase to skipped rather than done.

One comment line in cards/frame.tsx from bb/connector-ui-e2e-v2 loses a
metaphor and an em dash; its fact is unchanged.

* chore(desktop): literal comments in the handoff and first build

Comment-only change to the handoff wiring, the kickoff, the receipt store,
the first-build check-ins, the handoff tour, the connector rows and the
machine profile store. Every kept comment names the caller, the constraint or
the defect it prevents. The claim that the tour never throws is removed: the
function can reject and its caller does not catch.

Five comment blocks in connector-tool.tsx written on bb/connector-ui-e2e-v2
lose personification, dramatic capitals and em dashes. Every fact in them
stays, and no block moves.

* chore(desktop): literal comments in the intro reveal

Comment-only change to the intro reveal's clock, timeline, cube renderer,
sound, scenes, store and README. Animation comments now name the actual
ramp, easing or offset with its number. Four comments that contradicted the
code are corrected: the first texture slot opens at 3700 ms, the tear settles
from 1 to 0 over 460 ms, the typing weight delays the character it sits on,
and INTRO_EXIT_MS is wall time in index.tsx but score time in the overlay.

* chore(desktop): literal comments in the Electron onboarding windows

Comment-only change to the window growth geometry and the two onboarding
windows. The 768 px floor keeps its one fact: the floor uses Math.ceil where
the deltas round, because rounding 906.24 DIP down leaves the media query
false. The comment that placed the CSS-pixel to DIP conversion at getBounds
now points at growWindowBounds, where it happens.

* chore(gateway): literal docstrings in the onboarding RPCs and the tour tool

Docstring and comment-only change. The module summaries state what each
module does and where authorization comes from, without contrast pairs. The
tool descriptions the model reads are unchanged. Two words in the tour tool's
module docstring lose personification; the rest of that docstring is as it
was.

ast.dump of both versions, with docstrings stripped, is identical for all
three files.

* chore(desktop): literal punctuation in the relaunch and film-end notes

Comment-only change to four lines that bb/connector-ui-e2e-v2 added to the
boot gate, the gate store and the intro gate. Each em dash becomes a colon, a
full stop or a pair of parentheses; one emphasis capital is lowercased. The
facts in the notes are unchanged.
2026-09-12 10:00:08 +05:30
brooklyn! 4f1966edac feat(desktop): connector cards that wait for the sign-in, and a guided first launch that holds together (#108292)
* feat(desktop): give Button a loading prop that swaps label for spinner without layout shift

The label stays in the box, invisible, and the spinner is absolutely
centred over it, so a Connect or Approve button keeps its width while it
works instead of collapsing to a spinner. The approval bar had the same
thrash and moves onto it.

* refactor(desktop): one consent card for connectors and MCP setup

McpSetupTool rendered its own copy of the connector card's markup. It now
renders ConnectorCard for the pending question and ConnectorSummary once
settled, and the card gains what MCP needed: keyboard accelerators, a
source line, a question heading. The card also gets an avatar variant
(40px mark in the left gutter, text and buttons on one column) and a
collapseWhenSettled switch so a connector can stay a full card with a
green Connected pill in the action slot while MCP keeps its one-line
summary. Brand marks for Gmail, Calendar, Drive, Discord, Telegram and
Spotify; Slack via Tabler because simple-icons dropped the mark.

* feat(desktop): connector card drives the agent through manage_connections wait

The offer used to end in a Continue in chat button, and the agent, seeing
an unconnected status, would improvise around the app. Now the card does
what the TUI does. Clicking Connect opens the browser and sends one hidden
line telling the agent to park in manage_connections action=wait for that
slug and to never call connect again (a second link cancels the one being
signed into). Not now sends its own line. A hidden request that lands
while the turn is busy steers it, or queues if the turn just ended.

Which call owns the live card changes too: consecutive calls naming the
same apps are one exchange (connect, the wait, the status that follows),
and the first of the last exchange is the card, so the agent's wait no
longer demotes the card mid-authorization and mints a fresh one below it.
A targeted ask renders one or two bare cards; only a real catalog gets the
header, search and refresh.

* feat(desktop): onboarding connects apps in chat and keeps tasks finishable without them

The welcome chat knew connectors only as preferences to pick and wire up
later, so asked to connect Gmail it invented a Settings page that does not
exist. Both scripts now carry one rule set: status once, one batched
connect for every app named, the card is the ask so write a line and end
the turn, never route around a declined app with another client or
credential. The build handoff checks real connection status instead of
asserting none are connected, and the first task must be finishable, not
free of, the apps they picked. The connectors card explains what
connecting means and reports the count on its Continue button.

* fix(tools): resolve the Nous identity for share_auth profiles in the connector gate

A profile created with share_auth has no auth.json of its own and signs
in through the root store. Every other credential reader falls back to
the global root; the connector gate read HERMES_HOME/auth.json directly,
saw nothing, and stripped manage_connections from the profile's tool
list, so the welcome chat's agent truthfully reported the tool missing.
The gate now goes through get_provider_auth_state.

* fix(agent): name a provider retry backoff on the live status line

The retry status is buffered and replays only when every retry fails, so
during a 60s backoff after a 5xx the user saw a bare spinner. Right after
a connector sign-in landed this read as the agent going silent. The
backoff now also rewrites the live wait notice, which the desktop already
renders in the thread status row; it is transient and clears on recovery.

* test(desktop): connector rehearsal launcher and flagged connector spec

connector-rehearsal.mjs starts the real desktop and backend under a fresh
HERMES_HOME with no copied credentials, a fixed Vite port and CDP on 9344,
so the onboarding connector flow can be driven end to end by hand or from
outside. The Playwright spec covers the flagged connector step.

* fix(desktop): send the agent back into wait when the user keeps waiting after a timeout

The card's Keep waiting re-entered the poll but the agent's own wait had
timed out too and nothing told it to go back in, so it would start
talking mid-authorization. keepWaiting now fires onWaiting like connect
does. Tests also pin that an expired or revoked grant asks the gateway
for reconnect, not connect.

* style(desktop): blank lines in connector-flow test per lint

* feat(desktop): HERMES_SKIP_INTRO=1 / --skip-intro skips the first-run film

The intro is a one-time reveal, so anyone rehearsing the guided chat behind
it sits through it on every fresh HERMES_HOME. The flag rides the existing
launch-flags path (main → preload → renderer) next to guestOnboarding and
only gates isIntroRevealEnabled; the backend never sees it. The rehearsal
launcher sets it.

* fix(desktop): onboarding card Continue stays Done after the transcript rebuilds

The card kept its Done flag in component state. The hidden submit and the
turn-end hydrate both rebuild the message list, so the card remounted with
the flag false and Continue came back live, letting a step be answered
twice. The committed steps now live with the other onboarding answers,
keyed by step, and the first-build chip pick rides the same store.
remember_onboarding projects by key, so the new field never reaches USER.md.

* fix(desktop): no provider picker or free-tier chip over the guided first launch

Two sign-in surfaces leaked into the guide. A credential probe on the
setup profile (a free-tier token mid refresh, a session before its runtime
settled) hit requestDesktopOnboarding and dropped the provider picker over
the chat the user was in; and the statusbar free-tier chip sat there
offering a second sign-in the whole time. Both now yield while the gate
phase is cinematic, guided or handoff. The free tier is the provider for
those phases, and the guide offers sign-in on its own ready screen.

* fix(desktop): onboarding connector picks are real catalog slugs

The picker offered Spotify, GitHub and Stripe, none of which the deployed
connector catalog carries, and spelled Calendar and Drive with hyphens the
gateway does not use. A pick the build chat could not honour ended as
"Spotify isn't in the connector list" after the user had been told to
expect it. The list is now twelve slugs from the live status catalog,
spelled as the gateway spells them; GitHub is out (the terminal has git
and gh), chat channels stay on Messaging. Marks for the new entries; the
Google marks answer both spellings. The build runbook offers the picked
connections in its first turn rather than after the work is underway.

* fix(desktop): the free-tier ready screen never interrupts the guided chat

A readiness round fires when the layout pick assembles the window, and it
raised the free-tier ready screen over the conversation: the user was
dropped into the main app, dismissed it, and came back to a card they had
already answered. The guide is the introduction. The ready screen now
yields while the gate is cinematic, guided or handoff, and the notice is
acked the moment the guided chat takes the screen, not only when the film
does, so a skipped film no longer leaves it pending.

* feat(desktop): tour options that lead to building, and a fork that follows the tour

"Just the basics" and "Show me around" read as a click-through with no
exit; "I'll figure it out" read as declining help. Now Quick tour, Show me
everything, and Skip, let's build something. The script also folds the
fork into the same turn as the tour, so when the user closes the overlay
the next ask is already waiting instead of a transcript that ends on the
tour call.

* feat(desktop): the onboarding connector picker reads the live catalog

A hardcoded list, however carefully copied from today's catalog, is the
next drift. The picker now asks connectors.list through the same
session-owned RPC the connector cards use and offers exactly what the
gateway carries: a curated lead order puts the everyday apps first, chat
channels stay on Messaging, everything else is reachable by search. The
picks are gateway slugs, handed straight to manage_connections. No
catalog (toolset off, gateway unreachable) ends the step honestly with
Skip instead of inventing apps.

* test(desktop): the guided first launch never forces a sign-in

The acceptance criterion the guided onboarding was built to, as a test:
while the gate is cinematic, guided or handoff, the provider picker does
not open and a credential warning is dropped rather than deferred to the
next send. Outside the guide the picker opens as before. Red against the
tree before the guards landed (6 of 9).

* fix(desktop): a relaunch mid-guide resumes the guide, in the guide's shape

Closing the app during the guided first launch and reopening it booted the
normal shell around the persisted solo layout: the connecting splash, the
stock composer and model picker, a small window whose sidebars would not
open, while the gate still read guided. The gate now queues a kickoff for
the guided phase too (the kickoff adopts the existing guide chat by title),
takes the solo shape before the gateway opens rather than after, and the
connecting overlay yields to the guide's own opening. A typed reply in the
composer now closes an ask card and the first-build chips the same way a
click does; the layout card's Continue comes back Done.

* style(desktop): one answeredAfter helper for the ask card and first-build chips

* fix(desktop): the guide takes its shape on the tick the film ends, not after the window shows

Between the film and the greeting the full-size shell painted for a beat:
finishIntroReveal showed the main window, then the kickoff shrank it once
the setup profile answered. The listener on the intro's hidden edge now
takes the guide's shape (solo layout + small centred window) synchronously,
so the window is already the guide when it is shown. One takeGuideShape
owns the pair; kickoff and the boot gate call it idempotently.

* style(desktop): the 'nothing connects yet' line reads first on the connectors card
2026-09-12 10:00:06 +05:30
hermes-seaeye[bot] 7469c0f2a5 fmt(js): npm run fix on merge (#108522)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-11 19:00:50 +00:00
xxxigm ad03f20dd6 test(desktop): pin local media seeking through the production fetchLocal
The protocol suite mocked fetchLocal to 206, so a missing Range hookup
stayed green. Drive the handler with fetchLocalMedia and a real file.
2026-09-11 10:17:09 -05:00
xxxigm 73a2597c81 fix(desktop): serve local hermes-media ranges so chat video can seek
Electron's file:// loader ignores Range, so long clips stay unseekable even
though media-range.ts already landed. Wire fetchLocal through that helper.
2026-09-11 10:17:09 -05:00
Teknium 7dec81568e test(desktop): trim salvaged pool tests to invariants
Drop two PrimaryProfilePin cases that only restate the constructor
defaults and blank-string normalisation, and the wiring-routing test that
froze POOL_LIMITS_SETTINGS_ROUTE to a literal string — a snapshot of the
constant, not a behaviour contract. The two kept pin tests cover the bug
(a live primary keeps answering for its booted profile after the stored
preference moves; teardown releases the pin), and the notifications tests
cover the toast action end-to-end.
2026-09-11 06:23:18 -07:00
Mabolla 19cff347ad fix(desktop): wait for an evicted backend to release its slot
Await LRU teardown in each pooled backend creation path so replacement wakes do not race an exiting child for the hard spawn slot.
2026-09-11 06:23:18 -07:00
Mabolla 919dea9020 fix(desktop): await LRU teardown before queued wake
Wait for selected stale backend processes to exit before a replacement profile wake enters the bounded spawn queue.
2026-09-11 06:23:18 -07:00
joaomarcos a863bbcc28 fix(desktop): make pool slot timeouts actionable 2026-09-11 06:23:18 -07:00
Alexandru Ionescu d27180ba7f fix(desktop): pin primary backend routing to its booted profile
`primaryProfileKey()` re-read active-profile.json on every call. The rail's
live workspace switch rewrites that file via `hermes:profile:remember`
WITHOUT re-homing the primary, so after a switch the routing table disagreed
with the running process: a request for the profile the primary actually
booted as (e.g. "default") no longer matched `primaryProfile` in
`resolveProfileBackendRoute`, fell through to the pool, and spawned a second
backend for the same HERMES_HOME.

The duplicate was keepalive-fresh so LRU eviction spared it, it burned a pool
slot, and with the default cap of 3 every further profile queued and failed
with `Local backend start for "<profile>" timed out while waiting for a free
slot` (repro in desktop.log: "default" spawned as a pool backend while the
primary "default" was still running; coder/qwen then timed out for 20+ min).

Snapshot the launch profile in `startHermes()` (PrimaryProfilePin.pin) and
release it in `resetHermesConnection()` so the next start follows the stored
preference again. The pin is a tiny pure module with tests; main.ts only owns
the file read and the two call sites.
2026-09-11 06:23:18 -07:00
Teknium d1bd7b00df fix(desktop): dispatch probe falls back to /api/status on pre-/api/health remotes
Switching the pooled dispatch probe to /api/health (salvaged from #97914)
would 404 on every dispatch against a remote older than 0.19, retire the
tunnel and reconnect forever - the same storm #107997 describes, moved to
old backends. Fall back to /api/status on an explicit 404 exactly the way
the boot readiness probe already does (backend-health.ts). The legacy
fallback idea and its test are taken from #101976 (@edosulai); the rest of
that PR (timeout-tolerance streak, ServerAlive SSH options) is not adopted.

Co-authored-by: Edo Sulaiman <edosulai@icloud.com>
2026-09-11 06:22:35 -07:00
bennybuoy ee8257d601 fix(desktop): probe /api/health on pooled SSH dispatch, not /api/status
Cold /api/status through a Windows no-mux SSH forward routinely exceeds
the 2.5s dispatch budget, so Desktop retires a live tunnel and respawns.
Use the cheap /api/health route (5s, same as DEFAULT_HEALTH_PROBE_TIMEOUT_MS).
Background liveness still probes /api/status at 10s.
2026-09-11 06:22:35 -07:00
KoNit-K 8ab08968f4 fix(desktop): give pooled remote dispatch probes the cold-start liveness budget
A 2500ms dispatch probe is shorter than quiet-box hermes serve cold-start (~6-8s), so a just-woken pooled backend always fails and reconnects. Reuse REMOTE_LIVENESS_TIMEOUT_MS (10s) for that probe.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 06:22:35 -07:00
hermes-seaeye[bot] efca39279a fmt(js): npm run fix on merge (#108214)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-11 12:53:17 +00:00
Siddharth Balyan 0591da2ba6 Guided first launch: review fixes from #107985 and the free-tier chip badge (NS-848, NS-855) (#108211)
* fix(desktop): centralize guide handoff receipt reads

Resolve the guide receipt key and value together in setup-profile. Use the helper at all four read sites so connection scoping follows one implementation.

* fix(desktop): recover from unreadable handoff receipts

Memoize receipt reads and show Retry only for the error phase. Quarantine corrupt data before retrying, and resolve the guide identity when the failed request did not retain it so a fresh build can start.

Cover preservation of corrupt data and removal from the active receipt key with an invariant test.

* fix(desktop): validate persisted onboarding phases from one list

Derive OnboardingPhase and persisted-value validation from the same phase list so future phases survive relaunch. Verify every persisted phase reloads and an unknown value falls back to idle.

* fix(desktop): share window centering arithmetic

Extract centeredBounds and use it for onboarding boot and window growth. Keep the existing work-area clamps and coordinate rounding unchanged.

* fix(desktop): compute progress steps inline

Remove the ineffective ProgressCard memo because streaming flushes replace the messages array. Keep the same transcript scan and rendered steps.

* fix(desktop): center the free-tier status chip detail

Wrap the model label and sign-in badge in an inline flex span with a shared gap. This centers the badge beside the model text without changing other status-bar details.

* fix(desktop): derive the guide receipt key in one place

The Retry path spelled the key derivation out again because the read helper throws on a corrupt receipt before it can return the key. A separate guideHandoffReceiptKey serves both the reader and the quarantine, so the derivation has one home again.

* fix(desktop): keep the free-tier badge at its intended leading

Badge declares leading-none, but the class merger drops it behind the size variant's font-size class, so the badge inherits a 1.5 leading and renders 16px tall next to an 11px label. That height, not the inline alignment, is what read as a detached badge. Restating leading-none on the chip's badge brings it to 11.6px, inside the label's cap height. The Badge component itself is left alone; every other badge in the app has the same dropped leading and that is a separate decision.
2026-09-11 12:46:48 +00:00
brooklyn! e6aa2e9fe4 fix(desktop): satisfy Electron permission type check 2026-09-11 07:37:59 -05:00
YuhGuan 244a42f636 fix(desktop): media-range review follow-ups — ignore multi-range as a whole, fstat the handle being streamed, 404 for missing files
- Multi-range requests (any comma) now fall back to a full 200 instead of silently serving only
  the first part (RFC 7233 permits ignoring Range); documented + pinned by a test.
- Open the file first and fstat that handle, then stream from the same FileHandle, so
  Content-Length and the bytes delivered come from one open file (no stat/stream race).
  Handing the FileHandle (not the raw fd) to createReadStream avoids a double close.
- ENOENT/ENOTDIR return a 404 Response instead of rejecting; covered by a test.
2026-09-11 07:37:59 -05:00
Youssef 360c1ee836 fix(desktop): allow HTML5 video fullscreen through permission handlers
The custom setPermissionCheckHandler only allowed media/audioCapture/
videoCapture, which made Electron deny the 'automatic-fullscreen'
permission consulted during HTML5 video requestFullscreen(). The
request handler's isMediaCapturePermission() also returned false for
'fullscreen'. Result: the native fullscreen button on <video controls>
in chat silently did nothing.

Allow 'fullscreen' + 'automatic-fullscreen' in both handlers.

Verified with a minimal Electron repro using Hermes' exact handlers:
requestFullscreen() failed with 'TypeError: Permissions check failed'
before; works after. User-verified in the packaged desktop app.
2026-09-11 07:37:59 -05:00
brooklyn! f36b6b0ce6 fix(desktop): cancel bootstrap manifest work during quit 2026-09-11 07:33:17 -05:00
brooklyn! 7b9808e43b fix(desktop): complete quit through one bounded teardown barrier
Let settled remote sessions continue their first quit. Fence late local starts and join existing local and SSH drains without cancelling managed update recovery.

Co-authored-by: Gille <4317663+helix4u@users.noreply.github.com>

Co-authored-by: ChanPark03 <parkchan0302@gmail.com>
2026-09-11 07:33:17 -05:00