Commit Graph

7593 Commits

Author SHA1 Message Date
kshitijk4poor
57ba305a38 test(desktop): describe the interim-redelivery specs as they now stand
The header said the duplicate paints "on current main" (stale once the
fix lands) and pointed at boundary "specs below" when only the
message.start spec remains. Also apply prettier to the delta helper,
which --check flagged.
2026-10-03 16:34:20 +05:30
kshitijk4poor
a97f51c16c refactor(desktop): drop the dead streamIndex guard in identical-interim collapse
collapseDuplicateFinalOntoIdenticalInterim has one caller and it sits
inside `if (streamIndex >= 0)`, so the `streamIndex < 0` check could
never fire. Note the precondition in the comment instead.
2026-10-03 16:34:20 +05:30
kshitijk4poor
bc3de98ed7 refactor(desktop): index every part row in backfill and say why anchors differ
The query side reads identity through transcriptRowIds, which takes
sourceRowId from every part, while the index side still filtered on
`part.type === 'text'`. Only text parts are ever stamped, so the filter
did nothing but make the two sides use different rules; drop it.

Replace the comment that restated the rowId-vs-last-part ternary with
the reason for it: a fold with a rowId is addressed by its first source
row (hydration's key); without one, its last part row is the final
reply the settled live bubble holds, so pending rows land before it.
No behaviour change.
2026-10-03 16:34:20 +05:30
kshitijk4poor
9d1d52f24b refactor(desktop): gate identical-interim reuse on failure in one place
identicalInterimSiblingIndex now returns -1 for a failed completion, so
the stream path and the fallback settle path share one failure gate
(previously the fallback path settled a failed completion onto the
sealed interim). collapseDuplicateFinalOntoIdenticalInterim drops its
own hasFailure option and the dead guards a non-negative interim index
already implies, and shares the merge-and-drop tail with the tool-interim
collapse via a private helper. The no-fallback branch's
`identicalInterimIndex >= 0` arm was unreachable (any such index is a
visible assistant past lastUserIndex, which makes fallbackIndex >= 0) and
is removed.

partRowAnchor reuses transcriptRowIds instead of re-deriving the row
list; the anchor choice (message row first, else the last part row) is
unchanged.
2026-10-03 16:34:20 +05:30
kshitijk4poor
f9829a4887 fix(desktop): stop anchoring a backfilled fold on its first part row alone
The extra `existingPartRowIndices.get(message.rowId)` clause matched a
fetched fold as soon as its FIRST row was held as a part of another
bubble, short-circuiting partRowAnchor's every-row-held rule. Any unheld
row in that fold (e.g. the final reply) was then dropped and never
rendered. For single-row messages partRowAnchor already covers the
part-row match, so the clause only added the bug.

Add an it.each row for that shape and make the table assert explicit
merged ids instead of branching on a `kept` flag.
2026-10-03 16:34:20 +05:30
kshitijk4poor
effe6736b9 test(desktop): trim the interim-redelivery specs to the two that guard the fix
Keep the headline spec (red without the collapse) and the message.start
occurrence-bound spec (red without the guard); the distinct-segment and
tool-row boundary specs already pass on main, so they pinned nothing new.
2026-10-03 16:34:20 +05:30
kshitijk4poor
274117bd4c fix(desktop): only drop a backfilled fold when every row it carries is held
The part-row anchor dropped a fetched fold as soon as ANY of its text rows
was already held, so a fold carrying a tool call and narration the store
did not hold lost both (Enough1122's review on #124472). Anchor through
the part rows only when all of the fold's source rows are held; otherwise
append it as before. Also drops the 'ponytail:' note this replaces and
folds the case-B spec into the existing test as a parametrized pair.

Refs #123801

Co-authored-by: finn763 <165816600+finn763@users.noreply.github.com>
2026-10-03 16:34:20 +05:30
finn763
11d5eb81a3 fix(desktop): dedupe backfilled folds by final-reply part row
Backfill merge matched only message-level row ids. A hydrated fold is
addressed by its first source row while the final reply rides inside as
a text part sourceRowId, so an overlapping older page appended the fold
next to the settled live bubble and the reply rendered twice. Closes #123801

(cherry picked from commit 50021e8c0a668797d54db0874c6cc4662d702369)
2026-10-03 16:34:20 +05:30
jiangkoumo
f82e617ff3 fix(desktop): bound the identical-interim match to the current occurrence
message.start starts a new occurrence while keeping the prior messages (a
chained or prompt-less turn) and resets interimBoundaryPending. Scanning back
only to the prompt row let interim('X') -> message.start -> delta('X') ->
complete('X') match the PREVIOUS turn's interim, settle it and delete this
turn's live bubble.

Gate the scan on the same flag collapseDuplicateFinalAfterToolInterim already
requires — it is exactly 'an interim was sealed since the last message.start' —
and add the same-text restart regression (fails without the guard). Copilot
review on #126023.

(cherry picked from commit 45ced2713efa4cd92acae3783205cdf94c6f5e72)
2026-10-03 16:34:20 +05:30
jiangkoumo
1409d3877c fix(desktop): settle an identical reply onto its sealed interim instead of painting it twice
A reply the client sealed as an interim before it had the text on screen
(the interim materialized its own bubble because a superseded attempt's
frames had cleared the live stream) is painted twice when the turn then
streams the same text and completes: the sealed interim above, the settled
bubble below, while the store holds one row (#123801).

An interim whose text IS the final text is this turn's reply, not a second
segment, so completion now settles onto it and drops the live twin. Interims
carrying tool rows are never collapsed, and a distinct segment keeps its own
bubble.

Reproduced deterministically on main; the regression test fails without the
fix and the 222-file use-message-stream / chat-messages / chat suite passes
with it.

(cherry picked from commit f2980a9c9868a41e4ef3bf216c161acbc260e61b)
2026-10-03 16:34:20 +05:30
kshitijk4poor
3f4aedb37e fix(codex): Ultra stays codex's harness mode on the app-server
The app-server turn effort was clamped to the Responses vocabulary, so a Hermes
`ultra` pick reached codex as `max` and never ran codex's Ultra harness mode.
Live on codex-cli 0.147 with ChatGPT auth, `turn/start.effort = "ultra"` runs on
gpt-5.6-sol and gpt-5.6-luna (ladders reach `max`) and fails with 400
"'max' is not supported" on gpt-5.5, so `ultra` is now kept wherever the
model's ladder reaches `max` and clamped as before everywhere else. One rule,
route_supported_efforts(..., api_mode="codex_app_server"), drives the turn and
the labels: session.info's reasoning_effort_wire (the Desktop pill) and the CLI
and gateway /reasoning status read "ultra" instead of "ultra (sends max on this
route)" there. A named custom codex provider keeps the clamp.

From #130965 (Ultra passthrough and route-aware labels), narrowed to the
models that accept it.

Co-authored-by: FroRaut <79215537+Froraut@users.noreply.github.com>
2026-10-03 16:29:34 +05:30
teknium1
1faa572c16 test(desktop): count a poll's own dials, not the jittered ladder's
The #121865 regression test bounded ALL dials in one virtual second at 5,
but the reconnect ladder uses full jitter (delay in [0, ceiling)), so how
often the ladder itself redials depends on Math.random: a small draw gave
6 on CI, a 0.01 draw gives 12, a 0 draw 62, all with the throttle intact.

Timers never fire inside an awaited request under fake timers, so the
dials observed during each requestGatewayForAgent call are exactly the
poll's own. Assert those are zero (the #121865 bug was one per poll) and
that the ladder still redials. Green for draws 0, 0.01, 0.5, 0.999; red
when the background cooldown gate in openSecondaryForRequest is removed.
2026-10-03 01:08:52 -07:00
teknium1
7229edf52a fix(desktop): Edit Profile saves only edited fields; stale rows never reconcile
Review findings on the bot-identity fix:

- Edit Profile sent every seeded field on save, so a color change
  reverted a rename made elsewhere while the dialog was open (a thin
  row with no local record even cleared the backend title). The dialog
  now diffs against the values it opened with (edit-profile-look.ts)
  and sends only what the user changed.
- Rows retained from an earlier paint (source unreachable or
  connect-on-demand) were reconciled as fresh backend truth, reverting
  a newer local title during an outage. They are now tagged `retained`
  and mergeServerMeta skips them and any unreachable-source row.
- A title-only answer minted an empty direct record that shadowed a
  configured alias's appearance in botRosterMeta. A title now only
  corrects an existing record; the row already carries it.
2026-10-03 01:08:52 -07:00
teknium1
3326147075 fix(desktop): name each bot from its own backend; saves never publish cached fields
Two machines with a profile named `default` (local "Agent B", VPS "Agent A")
showed the VPS bot as "Agent B", told it so in group prompts, and the next
save on that row wrote `title: Agent B` into the VPS profile.yaml, from
where every relay roster republished it.

Root causes, all on the Desktop side:

- Write path: `saveBotMeta` sent the whole local cache entry as the
  server's `hermes-bots` namespace. Any field the cache held and the server
  did not (a restored or legacy name-keyed record) became that bot's server
  truth. A save now reads the bot's namespace from its own backend, writes
  that plus the save's own patch, and uses the existing per-key CAS
  (`ui_meta_expected_revisions`), re-reading on conflict.
- Read path, thin rows: Electron's enumeration read `profile.title` from
  `/api/profiles`, which has always named it `bot_title`, so bots on a
  non-active connection never carried their backend's title, and
  `mergeServerMeta` only ran over the active source's rows. Electron now
  maps `bot_title` (empty string included), and reconciliation runs over
  every live row: the name is whatever the bot's backend reports, and a
  local title it does not carry is dropped.
- Read path, bare keys: `botFriendlyNames` read `meta[bot.name]` for any
  non-remote row and `groupSpeakerLabel` fell back to `meta[profile]`, so a
  scoped `default` row picked up the bare-key title another machine's
  `default` wrote in single-source days. Both now read the row's own key;
  the bare legacy rung applies only to unscoped rosters. Mention completion
  goes through `botRosterMeta` too.
- A remote `default` with a real `display_name` no longer trades it for the
  connection label.
2026-10-03 01:08:52 -07:00
hermes-seaeye[bot]
26fe0005aa fmt(js): npm run fix on merge (#132011)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-03 04:22:32 +00:00
teknium1
e67255e7e4 fix(desktop): record the primary connection while a secondary is foregrounded
A connection apply made while a secondary scope was in front re-dialed the
primary socket to the new machine, but setPrimaryGatewayConnectionId
refused the id (#95628 hardening), leaving primaryConnectionId naming the
machine it had left. Owner routing then sent that machine's requests over
the primary socket to the other one. Live trace before:

  primary-publish refused vps keeping=local active=conn:vps::alpha
  conn:vps::default via=secondary primary=local/alpha dialed=vps

Every caller publishes the window's own primary socket (boot, reconnect,
connection apply, HMR survivor); #95692 already removed the secondary
write that #95628 was about. The id is now always recorded; the ambient
request connection still moves only while the primary is in front.
2026-10-02 21:11:01 -07:00
teknium1
90a0b34628 fix(desktop): Bots roster only caches rows from the connection it is keyed under
With a local and a remote backend that both run profile `default`, the
Bots pane showed the remote bot's title on "This device" (and the remote
profiles as "Status unknown" ghost rows) after a primary-connection
switch, and mergeServerMeta could carry that title into bot-meta-v2.

fetchRosterSnapshot binds activeConnectionId when it is called, but it
sent profiles.list over the ambient socket resolved later. A retry or late
run of a query keyed to the connection the window just left was answered
by the backend it switched to and cached under the old key. Live trace:

  published connection=vps
  [bot-roster] MISROUTED active=local answered=<vps install> expected=<local install>

Now the fetch refuses when its key is no longer the published connection
(stale retries never send), and asks the keyed connection by its exact
(connection, profile) route instead of the ambient socket. Without an
exact route (older Desktop host) it keeps the ambient request.
2026-10-02 21:11:01 -07:00
teknium1
c55a6e3d9f feat(desktop): identity-attribution traces for multi-connection bot rosters
Change-only desktop.log / agent.log lines that name which writer moved a
bot identity when two connections share a profile name:

- [gateway-route] per (connection, profile): via=primary|secondary and the
  window primary; `active` on every active-scope change (key, socket
  connection, profile) and `published` on every published-connection
  change, so a socket/label disagreement shows up side by side.
- [bot-roster] each Bots-pane snapshot: the install that ANSWERED
  profiles.list vs the install expected for activeConnectionId;
  MISROUTED when they differ.
- [bot-relay] rows per connection with the answering install; MISROUTED
  when two connections answer from the same install.
- [bot-meta] every $botMeta title change per scoped key.
- backend: bot_relay.roster.sync and ui_meta['hermes-bots'].title writes.

profiles.list now echoes install_id (same value as /api/status) so a
client can prove which machine answered a routed list. Additive field;
older clients ignore it. Plugins reach the tracer through
host.traceIdentityChange?.() so wholesale SDK mocks stay valid.
2026-10-02 21:11:01 -07:00
Yuan Li
efbb5c98da test(desktop): cover detached SSH startup readiness 2026-10-02 23:10:29 -05:00
Yuan Li
404a7c2b0b fix(desktop): do not gate SSH readiness on wrapper pid 2026-10-02 23:10:29 -05:00
liuzikaii
fb2db65eab fix(desktop): discard file tree reads from obsolete roots 2026-10-02 22:56:37 -05:00
Jakub Wolniewicz
e09e4018db fix(desktop): retain background turn leases across reconnect 2026-10-02 22:52:42 -05:00
Yagna Vudathu
6abf5f0232 fix(desktop): release terminal PTY handle on shell exit
The onExit handler forwarded the exit to the output gate but never killed
the session, leaking /dev/ptmx handles until macOS could not allocate any
PTY (#128942). Kill the PTY in onExit (the map entry still drops on flush,
so late attach keeps working) and dispose explicitly on renderer exit.
2026-10-02 22:46:04 -05:00
Brooklyn Nicholson
60bf909af9 fix(desktop): forbid .only in all Playwright configs
A committed test.only/describe.only shrinks the Playwright run to a
subset while the workflow still reports success — a silent partial
green. None of the three desktop Playwright configs set forbidOnly.

- Root config: forbidOnly: !!process.env.CI, mirroring the existing
  retries: process.env.CI ? 1 : 0 CI switch — local devs keep the
  ability to focus a test interactively.
- e2e/core and e2e/update configs: forbidOnly: true unconditionally.
  These lanes already hard-set retries: 0 because a required job must
  expose problems rather than hide them; the same contract forbids a
  focused test quietly shrinking a required job.

Verified on base (red): CI=1 npx playwright test with a temporary
test.only probe passed and reported '1 passed'. After the fix (green):
the same probe fails with "item focused with '.only' is not allowed
due to the 'forbidOnly' option" under CI=1 for the root config and
without CI for the core/update lanes; without CI the root config still
runs the focused probe (local behavior unchanged).

Fixes #93142
2026-10-02 18:37:44 -05:00
Brooklyn Nicholson
4fba38e2da fix(desktop): drop a settled live reply the page still carries as its streaming copy
The switch-back hydrate can be composed from the baseline that still held the
reply's streaming row, under the same id the store later settled. The overlay
replaced that row in place and never checked whether the page already had the
reply committed, so the answer painted twice until the next refresh (about one
run in three of the tool-turn race). The represented-on-page check now runs
before the in-place replace and drops the stale copy; a folded tool bubble is
also matched by its answer text when the completion carried no row receipt.
2026-10-02 18:31:06 -05:00
Brooklyn Nicholson
feca9757b7 fix(desktop): a finished tool turn stays one bubble when the switch-back hydrate races it
The tool-turn switch-back race still painted the answer twice, frozen on its
first chunk, in two places:

- appendLiveSessionProjection only stood the stale activate snapshot down for
  a tool-free committed reply. A tool turn's snapshot carries the turn's whole
  text and history folds narration, tools and answer into one bubble, so it
  never matched. It now stands down when the committed bubble ends in a reply
  that covers the snapshot; a mid-turn commit ends on a tool and still projects.
- overlayConcurrentMessageChanges only adds and replaces. A page composed from
  the older baseline could carry a live row the store had already retired into
  the committed reply. A baseline live row missing from the current store now
  stays retired.

The steer matrix also covers the steered tool finishing in the background:
the oracle skips backend notice rows (process_complete and friends) the way
hydration draws them.
2026-10-02 18:31:06 -05:00
Brooklyn Nicholson
3cf1b4197b fix(desktop): a tool turn completing during the switch-back hydrate renders once
The new switch-back-race tool-turn case caught it: when message.complete
settles the live reply while the switch-back REST page is in flight, the
overlay kept the settled stream row unless the page carried an assistant
message with the same words. History folds a tool turn's narration, tool and
reply into one bubble, so its text never equals the reply and the answer
painted twice. The completion already binds the reply to its stored row;
match that row first and keep the text rule as the fallback.

The oracle now compares a steer stored at a tool boundary by the user's
words, not the out-of-band wrapper the model sees. The review-row scenario
does not reproduce #131626's duplicate on main, so it is a plain guard
instead of a KNOWN entry.
2026-10-02 18:31:06 -05:00
Brooklyn Nicholson
72103912e0 test(desktop): sampler fails a transiently misordered transcript
The in-page sampler already sees every frame; it now also fails a reply
rendered above its own prompt and two messages that swap places between
frames, so a wrong order that heals by the final state still fails.
2026-10-02 18:31:06 -05:00
Brooklyn Nicholson
deceec1aa8 test(desktop): core e2e for the timings that render one reply twice
reply-settle-paths.spec.ts forces the duplicate-reply shapes under the
transcript oracle: an answer the backend repeats as the final reply, with
a review.summary row delivered before that turn's message.complete
(#131626, KNOWN), a steer at each point of a tool turn, then reload and
switch-back over folded tool turns. switch-back-race.spec.ts runs both
orders with a tool turn too (#128809). The scripted provider can now
script marker-less traffic such as the background review fork.
2026-10-02 18:31:06 -05:00
ethernet
91b17917e6 test(bot-mode): pin the roster busy flag across a stop/restart overlap
A stale roster run finishing after a restart must not clear the new
run's rosterBusy flag. Dropping the generation check in
syncRelayRosters' finally left every existing test green, but let the
next 60s tick start a third concurrent roster writer while the
restarted run was still in flight (expected 4 profiles.list calls, got
6). Add a regression for that overlap, and apply eslint
padding-line-between-statements fixes to the lines the salvaged commit
added so the touched files stay at zero warnings.

Follow-up to #131028; mutation gap reported in review there.
2026-10-02 19:00:44 -04:00
Gille
8dc8292883 fix(bot-mode): discard stopped relay roster refreshes 2026-10-02 19:00:44 -04:00
ethernet
cb326e32c7 fix(desktop): activate a tray-hidden window when anything but the tray restores it
On Windows, restore() or showInactive() on a window that the tray hid
paints the window, but it does not become the foreground window. The
window then drops all mouse and key input until the app restarts. A
relaunch from the taskbar or Start menu, a deep link, and a notification
click all restore the window this way. Only the tray menu and tray click
ran show() and focus().

The restore and show handlers now finish the activation for a window
that the tray hid: show(), then focus(). The call is deferred past the
native restore. Windows that the tray did not hide are not changed.

Fixes #127349
2026-10-02 18:43:06 -04:00
Brooklyn Nicholson
c3b4a30eff fix(relay): hand each streamed chunk over before the provider sends the next
With shared metrics on, every provider stream runs through Relay's managed
pipeline. Relay pulls the next provider chunk before it delivers the current
one, and the provider read blocked the stream's private loop, so each chunk
was withheld until the next arrived. On Desktop the reply looked stopped while
the timer kept going and queued follow-ups waited on a turn that was still
running; a steer aborted text the user never saw, stacking user bubbles with
no output between them.

The off-loop read from the previous commit fixes it. Correct its comment to
the real cause, replace the two-loop starvation test (production gives every
stream its own loop, and it bounded wall-clock at 150ms) with a single-stream
contract that deadlocks on the old code, and add a core e2e spec that steers
and queues against a paused provider with Relay managed execution on.
2026-10-02 16:29:18 -05:00
Brooklyn Nicholson
50a6abca7c test(desktop): keep background replies single across refresh 2026-10-02 12:34:28 -05:00
Brooklyn Nicholson
c24c8c2731 fix(desktop): merge folded replies by stored source identity 2026-10-02 12:34:28 -05:00
teknium1
83097ba008 fix(desktop): keep the hermes://skill/install deep link through the catalog revert
The plugin deep link (hermes://plugin/install?catalog=) and the skill
deep link are separate features: one per kind. Reverting #119381's card
catalog should not leave skills without a link, so this restores the
skill route on its own:

- Desktop: skill-deeplink-install store, the skill route in
  deeplink-routes, its use-desktop-integrations wiring, and the
  confirm-store onConfirm/details/busy/done phases its dialog uses
  (restored from main with their tests). Its nine strings move from the
  deleted catalog.* namespace to skillDeepLink.* (en + the six locales
  that translated them).
- Website: Skills Hub "Install in Hermes" button, plus the
  source-qualified installIdentifier from extract-skills.py so the link,
  the embedded picker and the CLI command all name the exact skill
  (apps/shared/src/catalog-install.ts keeps only the skill helpers).
- Docs: skills.md "Install from the website" section, desktop.md pointer.

The card catalog UI stays reverted.
2026-10-02 10:29:28 -07:00
teknium1
6cb7a64e59 revert: Skills and Plugins card catalog browser, third landing (reverts #119381)
Reverts all seven commits of #119381 (59c54c78..476ff4dd), which
re-landed the native card-based Skills/Plugins catalog browser after it
was reverted twice (#113037 reverting #112669, #115787 reverting
#113128). Capabilities -> Skills and Plugins return to the list/detail
tabs with the embedded catalog picker they had before #119381.

Removed with it:
- Desktop catalog/ module (browser, cards, detail dialog, filters,
  reels/masonry, install switch) and skill deep-link install
  (hermes://skill/install); skills/embedded-hub-picker.tsx comes back.
- apps/shared catalog-browse.ts and catalog-install.ts.
- The `catalog` i18n namespace in every locale (+ regenerated
  locales/_keys.desktop.json); the two i18n pack tests and the keys
  test now exercise connectorsPage.searchPlaceholder instead.
- Website /skills "Open in Hermes Desktop" skill links and the
  extract-skills install identifiers; docs return to the pre-#119381 text.
- Later fixes that only patched the reverted catalog code go with it:
  5832a6eb93 + 7aba2cbbe9 (catalog download deadline),
  9e63cdc4b8 (catalog lookalike rows), c756986db2 (catalog counts),
  the catalog half of c13232e90c / f265273347.

Kept on purpose (later work that depends on code #119381 introduced):
- DialogContent `chrome` / `overlayClassName` / nested-dialog layering
  (the image lightbox uses them).
- vitest.setup.ts / src/test/jsdom.ts stubs (other suites rely on them).
- Kanban toolset wiring on the Plugins Desktop switch (cd30fdadf6,
  7ca2f5665f) and its tests, the `hermes_not_connected` server tone,
  the SearchField flex-1 fix, skill provenance helpers. SkillDetail shows
  a provenance pill only for agent/hub skills, since the pre-#119381
  labels have no `external` entry.
2026-10-02 10:29:28 -07:00
hermes-seaeye[bot]
0f9e758088 fmt(js): npm run fix on merge (#131680)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-02 17:29:05 +00:00
ethernet
386e31a56f fix(desktop): ship exact-size, theme-aware MSIX icons instead of one 44px tile
The MSIX package carried a single Square44x44Logo.png, so every taskbar and
Start slot on a HiDPI Windows 11 machine was Windows upscaling a 44px bitmap,
and with no altform variants Windows plated the icon itself. The generator now
emits the qualified catalog Windows resolves by itself: <Logo>.scale-{125,150,
200,400} for all four manifest logos at Microsoft's pixel sizes, and
Square44x44Logo.targetsize-{16..256} in the three required theme forms —
unqualified and altform-lightunplated carry the light tile, altform-unplated
(dark theme) the dark tile, the same split macOS dark mode gets. before-build
stages the whole assets/appx directory; app-builder-lib runs makepri and
`makeappx /l` as soon as it sees a qualified asset, so nothing else changes.
The hand-listed *-dark.png logos, generated but never staged, are gone.

Exposed along the way: the commit badge sat outside the portrait's silhouette
clip, and at 16–30px its anti-aliased edge stacked coverage on the plate's
corner pixels and changed the alpha channel between build flavors. The badge
now shares the clip, is plain black, and is left off direct renders below
32px where no SHA is legible anyway.
2026-10-02 12:51:02 -04:00
Brooklyn Nicholson
5654863ccd fix(desktop,tui): /browser use [off] works outside the CLI
The Desktop and TUI subcommand pickers offer /browser use (it is in the
registry's subcommands), but both handlers only knew connect, disconnect
and status, so picking it printed the usage line.

browser.manage gains a use action (enabled: bool) that writes the session
profile's browser.backend through the same set_browser_use_mode helper the
CLI now calls, and status/use report browser_use. The handler is profile
scoped, so a worker session writes its own profile. A live agent keeps its
tools (prompt cache), so Desktop and TUI say the switch applies to new
chats instead of swapping tools mid-conversation. use and status also run
on remote backends; connect/disconnect stay local-only.
2026-10-02 11:24:58 -05:00
Brooklyn Nicholson
b3059921bc feat(desktop): show when a message was sent on hover
Hovering a message reveals its time with the hover actions ("3:27 PM",
"Yesterday, 3:27 PM", "Sep 28, 3:27 PM"); the tip adds the age for same-day
messages and the full date. User bubbles show it beside Restore/Stop, replies
at the head of the action bar. It stands down while display.timestamps is on,
since the transcript already prints every stamp (#118405).
2026-10-02 10:59:29 -05:00
Brooklyn Nicholson
69fdbb7cdc fix(desktop): model picker keeps names whole; provider labels its favorites once
The Favorites section put a provider chip on every row beside the variant,
fast and effort chips, and with names as the only element allowed to shrink
they collapsed to a letter (#130349). A mixed Favorites section now names
each provider once over its rows; the variant tag stays a filled chip after
the name, while fast and effort move to outlined chips by the caret that
edits them. An inherited effort is no longer stamped on every row: it shows
on the active model and on rows whose preset chose one.
2026-10-02 10:59:29 -05:00
ethernet
9a71d5a8b5 Merge pull request #122976 from NousResearch/ethie/macos-icon
feat(desktop): macOS 26 layered app icon, ring dropped from every icon
2026-10-02 11:37:47 -04:00
hermes-seaeye[bot]
b78931e3b0 fmt(js): npm run fix on merge (#131603)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-02 15:16:58 +00:00
ethernet
2bf86dd20c fix(desktop): run the Windows update checkers from bundled core code
A packaged Windows build reported "Microsoft Store returned invalid
JSON" on every update check. The Store checker, the App Installer
checker and the relaunch waiter were resolved under the payload's repo
snapshot (<payload>/hermes-agent/apps/desktop/scripts/), but
INERT_SNAPSHOT_DIRS has excluded apps/ and scripts/ from that snapshot
since f6713f2762. The installed 2026.6523.3313.0 package has no apps/
directory at all. The payload python then failed to open the script,
exited 2 with empty stdout, and parseResult reported invalid JSON.

The two checkers are python that runs under the payload interpreter, so
they belong with the core code, like the other hermes_cli helpers the
desktop runs with -m:

- apps/desktop/scripts/check-store-update.py becomes
  hermes_cli/windows_store_update.py and check-appinstaller-update.py
  becomes hermes_cli/windows_appinstaller_update.py (git mv).
- The runner spawns `python -P -m <module>` with PYTHONPATH set to the
  bundled core code, then the venv dependencies: the order the bin
  launchers give sys.path. -P keeps the working directory off sys.path.
- The relaunch waiter is PowerShell run outside the package, not core
  code. It ships as an app resource (win.extraResources) and resolves
  from process.resourcesPath.

The Python tests now import the modules, and a real `python -P -m`
subprocess test proves a checker failure still reaches the desktop as
JSON. A vitest checks the packaging config ships the waiter.
2026-10-02 10:42:07 -04:00
ethernet
baf6313149 test(desktop): copy scripts/mac-icon.cjs into the channel-manifest fixture 2026-10-02 10:33:44 -04:00
ethernet
0217a2a324 feat(icons): custom drive artwork as the volume icon of both installer DMGs
The desktop DMG's volume icon defaulted to the packager's icns, which is
actool's 256px fallback whenever mac.icon is the Icon Composer package, and
dmgbuild's badge option can only paste onto the stock removable-drive icon.
Ship our own drive-with-the-girl artwork instead: assets/dmg-volume.png
(hand-made, one artwork since volume icons have no appearance variants) is
emitted as apps/desktop/packaging/dmg-volume.icns, beside the DMG background
so the `files` whitelist keeps it out of the app bundle, and dmg.icon points
at it.

Tauri always uses the app icns as the volume icon and offers no override, so
the bootstrap workflow runs scripts/bundles/set_dmg_volume_icon.sh on the
built DMG before signing (the same .VolumeIcon.icns + custom-icon flag
dmgbuild sets), and the verify step checks the icon is on the mounted image.
Verified on macOS 26.6.2: both DMGs render the identical volume icon.
2026-10-02 10:33:44 -04:00
ethernet
383916b845 feat(icons): layered macOS 26 icon for the bootstrap installer and the DMGs
Tauri's bundler (cli 2.11.4) compiles an Icon Composer package listed in
bundle.icon into Assets.car with actool >= 26 and sets CFBundleIconName, so
the bootstrap installer gets an unbranded twin of the desktop package under
src-tauri/icons/icon.icon (generator target; the bundle stays the plain
brand because flavours only apply under apps/desktop/). Tauri only logs when
actool is too old, so the macOS job selects Xcode 26 and the verify step
asserts Assets.car and the plist key are in the built .app.

The desktop DMG's volume icon defaulted to the packager's icns, which is
actool's 256px fallback whenever mac.icon is the .icon package; point
dmg.icon at the full-resolution assets/icon.icns. Tauri's DMG already takes
--volicon from the bundle's .icns, which the regenerated outputs cover.
2026-10-02 10:33:44 -04:00
ethernet
c2b69de66d feat(icons): drop the brand ring everywhere and frost the mono icon
Next to macOS 26's own icons the flat plate + girl reads better than the
ringed tile, so the ring is off in every output: the tiles' inward stroke,
the layered icon's border layer and its targets. BORDER_ENABLED keeps the
ring code one flag away.

Without a ring the girl's lowest nodes are dragged past the tile edge
(EDGE_OVERSHOOT) so the outline crops her instead of leaving a gap.

Clear light/dark and Tinted light/dark now come from one grayscale mono
layer with Liquid Glass on: the parts of the girl that are black in light
mode become a near-black frosted ink (0.13 @ 90%), her white parts stay
white, each pixel painted exactly once so materials never stack. The art
layer hides under "tinted" and the mono layer shows only there.

Mac girl at 1.06x (between main's 1.12x and the layers' 1.0x), for the icns
and the layered icon alike. Chosen from a six-appearance matrix of real
IconServices renders next to Finder.
2026-10-02 10:33:44 -04:00
ethernet
e967eff52c fix(icons): register the layered icon's girl on the mac plate box
main scaled the girl 1.12x for every mac target (a white tile with a ring
reads small beside full-colour peers), but the macOS 26 layers were still
registered on the full-bleed squircle box, so macOS 26 would have shown a
smaller girl than macOS 15.

The layered icon's canvas is the plate itself, so derive its portrait box
from the mac box in plate coordinates. Ink overlap with the icns girl goes
from 0.65 to 0.996 IoU; only the two art layers change.
2026-10-02 10:33:44 -04:00