Commit Graph

2387 Commits

Author SHA1 Message Date
brooklyn! 623d5c93e0
Merge pull request #80736 from NousResearch/bb/reasoning-summary-blocks
Reasoning steps read as separate blocks again instead of one glued paragraph
2026-08-06 22:02:13 -06:00
Brooklyn Nicholson 6f1072c83c fix(desktop): drop the gateway-pill dogfood plugin
It was a 1:1 rebuild of the core statusbar gateway item and shipped
enabled by default, so the pill showed up twice. Core chrome stays in
shell; demos that clone it belong in hermes-example-plugins.
2026-08-06 22:29:33 -05:00
Brooklyn Nicholson a5cddcd8dc fix(desktop): render already-glued reasoning as separate blocks
Repairs what is already in the transcript: reasoning persisted before the
backend fix, and any provider still gluing its parts. Handles both shapes —
heading-onto-heading (the **** run) and prose-onto-heading (vercel/ai#6742).
Verified against 46 real glued messages from a gpt-5.6-sol session; all repair
cleanly and idempotently.
2026-08-06 22:02:46 -05:00
hermes-seaeye[bot] eb8421ba98
fmt(js): `npm run fix` on merge (#80725)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-07 02:42:42 +00:00
brooklyn! aacd3194c9
Merge pull request #80719 from NousResearch/bb/running-process-elapsed
Keep elapsed status text from overlapping
2026-08-06 20:33:35 -06:00
brooklyn! 4b0238578e
Merge pull request #80711 from NousResearch/bb/sidebar-pin-sort
Pin as many sessions as you want, and they stay where you put them
2026-08-06 20:33:14 -06:00
brooklyn! 01a61c945f
Merge pull request #80718 from NousResearch/bb/show-earlier-threshold
Show earlier messages no longer hides most of a session
2026-08-06 20:31:40 -06:00
Brooklyn Nicholson 45f23205d7 fix(desktop): show every pinned session, however many there are
Pinned was capped at half the viewport by its own nested scroller, so past
roughly a dozen pins the rest were reachable only by scrolling inside a
scroller — a pin you have to go hunting for isn't doing its job.

Drop the cap and let the section grow into the sidebar's existing scroll,
and stop virtualizing Pinned: virtualization needs a bounded viewport to
measure against, which is exactly what's being removed. No count badge, no
"show more" — pin as many as you want and they all render.

Also back-fill pins on the API-server list route, which was the one list
path still windowing purely on recency.
2026-08-06 21:27:27 -05:00
Brooklyn Nicholson 0265797b8d fix(desktop): keep elapsed status text from overlapping 2026-08-06 21:26:29 -05:00
Brooklyn Nicholson 75717d29eb feat(desktop): stop hiding a session behind Show earlier
On real sessions the button showed up two or three turns from the bottom, over
a screen and a half of transcript that had barely painted anything.

The budget now spends paint weight, which is what the DOM actually mounts, and
600 units of it — 10-20 agentic turns measured, where a tool-heavy turn prices
at 30-90 and a plain exchange at 5-10. A floor of 8 turns covers the session of
enormous turns that a weight-only cut still truncates hard; it applies to a
real page only, so the small first-paint commit stays small and the backfill a
frame later fills the rest.

Measured on four stored sessions at the same budget: one went from 3 turns
visible to 12, another from 3 to 4, two unchanged. The store window still caps
what the DOM can reach at all.
2026-08-06 21:20:57 -05:00
Brooklyn Nicholson 31459ef0c9 feat(desktop): price a turn by what it paints
One weight function served two budgets that protect different things. The
store window protects the heap: every message it admits is normalized into the
runtime repository whether or not the transcript collapses it, so it has to
price the payload it holds. The DOM budget protects the paint, and what a turn
mounts is decided by the grouping, not by the bytes behind it.

Charging the DOM budget for payload made it count work that never happens. A
settled run of twelve reads is one grey summary line, a thought is one
collapsed disclosure, a todo is hoisted out of the transcript, and an image is
one img however long its data URL — all of it priced as if fully expanded.

messageStoreWeight keeps the payload price for the window. messagePaintWeight
prices what mounts: collapsed rows flat, silent rows free, cards fixed, and
markdown and diffs by size, since those really do build DOM. Both share one
character ceiling per message rather than one per part.
2026-08-06 21:20:41 -05:00
Brooklyn Nicholson 7eb461d693 refactor(desktop): share how a tool row renders
The transcript decides what a tool call draws and the render budget has to
price it. Both sides need the same answer, so the classification moves out of
the tool renderer into its own module rather than the budget importing the
formatting and i18n weight of fallback-model to ask one question.

Adds isSilentTool for the rows that render nothing at all: todo is hoisted to
its own panel, and a reaction's UI is the emoji on the bubble.
2026-08-06 21:19:44 -05:00
Brooklyn Nicholson 03b759db86 fix(desktop): rank dragged sessions inside their date group
Dragging one row switched the entire sidebar into a frozen manual mode
with no date dividers at all — permanently, for every session, because
the manual order replaced the recency sort outright instead of layering
on it. Chronology and ranking are separate concerns: keep the calendar
buckets where recency put them and apply the hand-picked order only
within a bucket, so a drag ranks a chat among its own day's chats and the
dividers survive.

Rows move as clusters, so a reorder can't strand a branch child from its
parent, and a session the saved order doesn't name keeps the slot recency
gave it. Two supporting fixes fall out: dnd-kit now receives the ids it
actually renders (it was handed the unrendered session order, so a drop
computed its target against a list the user wasn't looking at), and an
older page that loads no longer jumps above the hand-picked rows — new
ids fold in by position rather than all hoisting to the top.
2026-08-06 21:12:35 -05:00
Brooklyn Nicholson 256aac54d9 fix(desktop): show a pinned session once, and keep its drag order
Two ways a pin got misfiled. The duplicate: a pin is stored on the
durable lineage root, but recents, the messaging slice and the backend
project tree are three independent fetches and each can surface the same
conversation under either its live tip or its root — so the filter
compared one identity against the other, missed, and the session rendered
in both Pinned and its project group. Match on every id the pin is
reachable under.

The lost reorder: a drag only reports the pins whose row is loaded, and
setPinnedSessionOrder required that list to match the stored one in
length, so a single unresolved pin discarded the whole reorder. Treat it
as a permutation of a subset — re-slot the named ids, leave the rest.
2026-08-06 21:12:07 -05:00
Brooklyn Nicholson daeedf67c9 fix(desktop): hold the pin write guard until a page confirms it
The guard that stops a stale list page from reverting a fresh pin was
released on the PATCH's own ack. A list request issued just before the
write is slower than the write, so it lands after the ack still carrying
the old value, with no guard left to fence it: the pin flips back and the
next reconcile pushes that wrong value to the server, making it durable.

Keep the guard until a page actually confirms the value written, with a
cooldown so a row that never returns can't fence itself forever, and drop
it outright when the write fails — the server never changed, so it stays
authoritative.

Also reset the mirror bookkeeping on a gateway switch. mirrored/pending
are per-backend facts; carrying them across a re-home told us the pins
were already pushed to a backend that has never seen them.
2026-08-06 21:11:38 -05:00
Brooklyn Nicholson fd9fc50dd2 fix(desktop): stop dropping pinned sessions past the page limit
The list endpoints deliberately back-fill pinned conversations past their
LIMIT, then the client sliced the response back down to that same limit
and threw them away — so only pins that happened to land inside the most
recent page ever rendered, which reads as a cap on how many sessions you
can pin.

Keep the back-filled rows when trimming, and discount them from the
"window came back full" test that drives Load more. Counting a back-fill
as a loaded row invented a page that could never be fetched, leaving a
Load more button that refetched the same rows forever.
2026-08-06 21:11:10 -05:00
Brooklyn Nicholson ac745a0b07 docs(agents): surface capability belongs to the session, not the process env
The rule the preview-tool bug broke, written down so the next GUI-adjacent tool
does not rediscover it: the client and the backend are separate machines, so
"was this process spawned by Electron?" cannot answer "is a GUI watching?".
Names the working pattern (toolset gates the surface, check_fn answers only
reachability or user opt-in), the process-wide check_fn TTL cache that makes it
the wrong home for a per-session answer, and the test that would have caught
it — assert the GUI session gets the tool with the env var absent.
2026-08-06 19:35:47 -06:00
hermes-seaeye[bot] 25c7827ec9
fmt(js): `npm run fix` on merge (#79521)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-05 16:51:56 +00:00
brooklyn! 69bedb7bed
fix(desktop): stop reporting false failure on successful backend updates (#79513)
* feat(update): emit an action-scoped terminal receipt from hermes update

The dashboard now mints an action_id per backend update, hands it to the
spawned `hermes update` via HERMES_ACTION_ID, and reuses an in-flight
update action instead of spawning a duplicate. The updater prints a
bounded `=== hermes-update completed <id> ===` receipt on every success
path — normal, zip, dependency-repair, and the no-op "Already up to
date!" path that previously ended with no terminal marker at all
(#58764) — so the Desktop can prove completion across the dashboard
restart boundary instead of guessing from stale log text.

Co-authored-by: Vitor Cepeda Lopes <vitor@vitorcepedalopes.com>
Co-authored-by: doncazper <caztronics@yahoo.com>

* fix(desktop): make remote backend updates terminal-state driven

Remote backend updates failed with "Backend update failed." on nearly
every run: applyBackendUpdate() polled for only 30×1.5s ≈ 45s, then
read exit_code null off the still-running action and called it a
failure. Real updates (backup + uv sync + npm install + vite build)
routinely run longer, and the no-op "Already up to date" path never
restarted the gateway so the old return-check timed out too.

A still-running, reachable action is now never converted into failure
by an elapsed budget — only a nonzero exit is. The apply loop keeps one
in-flight promise, tolerates reconnects during the dashboard restart
without extending the fixed six-minute deadline forever, and confirms
success by the action-specific receipt that survives the restart,
falling back to proving the requested commit / up-to-date check for
older backends without action_id support. Inconclusive completion
fails closed.

Fixes #47359
Fixes #58764

Co-authored-by: Vitor Cepeda Lopes <vitor@vitorcepedalopes.com>
Co-authored-by: Mark Vlcek <markvlcek@gmail.com>
Co-authored-by: doncazper <caztronics@yahoo.com>

---------

Co-authored-by: Vitor Cepeda Lopes <vitor@vitorcepedalopes.com>
Co-authored-by: doncazper <caztronics@yahoo.com>
Co-authored-by: Mark Vlcek <markvlcek@gmail.com>
2026-08-05 10:42:02 -06:00
brooklyn! 64646dda56
Hermes can read the in-app browser (#79482)
* feat(agent): read_preview — the desktop-gated tool that reads the in-app browser

The agent could open the preview pane (open_preview) and read the embedded
terminal (read_terminal), but the browser it had just opened was a black box —
'what does this page say?' had no answer. read_preview mirrors read_terminal
end to end: HERMES_DESKTOP-gated via check_fn (zero schema footprint outside
the GUI), dispatched through the same agent callback pattern, windowed with
start/count so a long page pages instead of flooding context.

* feat(gateway): preview.read blocking bridge

Same lifecycle as terminal.read: the tool blocks on preview.read.request, the
renderer answers preview.read.respond (allow_expired — a slow page extraction
losing the 45s race must not surface a raw 4009), and a timeout emits
preview.read.expire so late answers resolve quietly.

* feat(desktop): the renderer serializes the active preview tab for the agent

preview-reader.ts is the preview analog of the terminal's buffer registry: the
URL pane registers a page reader (webview executeJavaScript → title + visible
innerText) keyed by tab id; readActivePreview resolves the ACTIVE tab, windows
the text (24k cap per read), and answers file/artifact tabs with identity plus
a note pointing at the tool that reads that content directly. The gateway
event handler answers preview.read.request beside terminal.read.request.
2026-08-05 16:35:00 +00:00
brooklyn! 28a3fe5c33
Merge pull request #79507 from NousResearch/bb/remote-pdf-preview
fix(desktop): render remote PDFs in preview rail
2026-08-05 10:34:28 -06:00
Brooklyn Nicholson eb68ffbe43 fix(desktop): make remote backend updates terminal-state driven
Remote backend updates failed with "Backend update failed." on nearly
every run: applyBackendUpdate() polled for only 30×1.5s ≈ 45s, then
read exit_code null off the still-running action and called it a
failure. Real updates (backup + uv sync + npm install + vite build)
routinely run longer, and the no-op "Already up to date" path never
restarted the gateway so the old return-check timed out too.

A still-running, reachable action is now never converted into failure
by an elapsed budget — only a nonzero exit is. The apply loop keeps one
in-flight promise, tolerates reconnects during the dashboard restart
without extending the fixed six-minute deadline forever, and confirms
success by the action-specific receipt that survives the restart,
falling back to proving the requested commit / up-to-date check for
older backends without action_id support. Inconclusive completion
fails closed.

Fixes #47359
Fixes #58764

Co-authored-by: Vitor Cepeda Lopes <vitor@vitorcepedalopes.com>
Co-authored-by: Mark Vlcek <markvlcek@gmail.com>
Co-authored-by: doncazper <caztronics@yahoo.com>
2026-08-05 10:34:18 -06:00
hermes-seaeye[bot] 9e9b3fc669
fmt(js): `npm run fix` on merge (#79505)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-05 16:32:54 +00:00
brooklyn! 7e16241825
feat(wake): hands-free wake word for remote desktop via client mic streaming (#79491)
* feat(wake): client-capture wake word for remote desktop

Remote headless backends have no PortAudio mic, so "hey hermes" fails even
when openWakeWord is installed. Let the desktop stream 16 kHz int16 PCM via
wake.feed while detection stays server-side.

- wake_word.capture: auto|local|client (+ GUI client_capture prefer)
- WakeWordDetector external_audio queue + feed_audio API
- wake.feed RPC; wake.start/status report capture + frame_length
- Desktop getUserMedia feeder; stop on wake.detected, re-arm after voice
- Docs + unit tests (26 pass in tests/tools/test_wake_word.py)

* fix(wake): address review on client-capture re-arm and feed queue

- wake.status reports effective capture from the armed detector (client vs
  local), plus frame_length/sample_rate; GUI status probes prefer client
- Gateway test doubles accept external_audio on start_listening
- Desktop PCM feeder uses a bounded ordered queue instead of dropping frames
  while a wake.feed RPC is in flight
- /wake on and status/re-arm paths pass client_capture so remote reattach works

* fix(wake): auto capture keeps the backend mic when one exists

With capture:auto the desktop always preferred client streaming, so a local
desktop with a working backend mic silently switched from PortAudio to
getUserMedia default-device — dropping wake_word.input_device selection
(#74363). A ready backend input now wins under auto; client capture is the
fallback for a preferring surface on a mic-less backend, and capture:client
still forces streaming.

Also removes the dead auto branch (both arms returned local) and lets the
client-feed test skip cleanly when numpy is absent.

* perf(desktop): coalesce wake.feed frames

Sending one 80 ms frame per RPC is ~12.5 gateway calls/s for as long as the
ear is armed. Drain up to 4 queued frames into a single wake.feed payload
(backend feed() already splits long buffers into engine frames) — ~3 RPCs/s
steady-state. Fix the wake.feed size-cap comment (64000 bytes = 2 s, not
0.5 s).

* docs(config): document wake_word.capture in cli-config.yaml.example

---------

Co-authored-by: Andrew <drew@kainotomic.com>
2026-08-05 10:32:38 -06:00
Matt Prusak c8fdc51740 fix(desktop): render remote PDFs in preview rail
PDFs were classified as generic binary/text previews, rendering raw %PDF
bytes locally and failing entirely for remote-only files. Classify PDFs as
their own preview kind, load bytes through the existing local/remote
filesystem bridge, convert them to revocable Blob URLs for Chromium's
embedded viewer, migrate persisted pre-PDF tabs at restore, and retry
restored previews when the active filesystem connection changes.

Salvaged from #76008-era base onto current main: PDF classification now
composes with the remote-HTML enrichment branch, and the persisted-tab
migration runs before the One-Browser URL rekey in decodePreviewTabs.

Supersedes #76565.

Co-authored-by: Brooklyn Nicholson <brooklyn@brooklyn.sh>
2026-08-05 10:26:48 -06:00
Brooklyn Nicholson c6fe31a9be test(desktop): expect client_capture in wake.start/status params
The GUI now passes client_capture: true on wake.start, wake.status, and the
post-voice re-arm; update the store and slash-handler tests to the new
param shape. 123/123 pass locally.
2026-08-05 10:25:57 -06:00
brooklyn! c8648278c3
In-app browser and previews are real layout-tree tabs (#77705)
* feat(desktop): shared pane-strip primitives — one bar, one glyph, one close menu

The zone header hand-rolled its tab bar, its close-verb context menu, and the
bare-glyph "+" inline; the preview rail kept a second copy of all three. Extract
PaneTabStrip (the bar), PaneStripGlyph/PaneStripTool (glyph buttons as data,
titlebar-tool style), and paneTabCloseItems (the four close verbs) into the
pane-tab primitives, and render the zone header through them. Panes contribute
strip glyphs via PaneChrome.stripTools; $stripToolsRevision tells the strip to
re-read.

* refactor(desktop): preview tabs are layout-tree tiles like session and page tiles

The in-app browser / preview rail carried its own tab strip beside the zone's
own — a second bar at a different height with its own close menu, label casing,
⌘W rung, and welded to the file browser's zone so ⌘J toggled it away. It
predated the layout tree.

$previewTabs now mirrors into pane contributions through the same paneMirror
session and route tiles use, so a preview tab IS a zone tab: one strip, drag/
stack/split, the shared close verbs, plain ⌘W, its own zone docked beside main.
URL tabs are titled Browser (the tab names the surface, not the page); files
keep their filename and a file-type lead glyph.

Deleted with the rail: the preview pane contribution + PREVIEW_PANE_ID + its
visibility binding, the 'preview' placements in the default tree and presets,
the ⌘W rail rung, the reveal listener, and the preview.close* i18n keys (copies
of zones.*). lone-header now keys on "closeable placement:main" instead of the
session-tile: id prefix, so any tile dragged into its own zone keeps its tab.

* fix(desktop): preview console/DevTools live on the strip, and DevTools tells the truth

The two toggles were titlebar tools — far from the preview they act on and one
ambiguous global pair once two previews were open. They're strip glyphs now,
contributed per-tab as PaneStripTool data with real tooltips: the console store
is cached by tab id so the glyph and the panel read the same logs, and the pane
registers a DevTools handle for its tab.

DevTools active state was also a lie: it tracked our click handler, so closing
the DevTools window itself left the glyph stuck on. The webview's
devtools-opened/closed events drive it now.

* fix(desktop): ⌘W and ⌃Tab work over preview and page zones

The generic tab verbs keyed zone eligibility on the CHAT strip (workspace /
session-tile: ids), so a zone holding only a Browser or page tile was invisible
to them: ⌃Tab skipped it, and ⌘W fell through the chat rung and emptied the
MAIN chat while you were looking at a preview. ⌘1…⌘9 already worked — the
verbs disagreed about what counts as a tab strip.

New isMainStripPane (any placement:'main' tenant — sessions, pages, previews)
drives ⌘W and ⌃Tab; isSessionStripPane keeps gating what it should: where a
session may dock (⌘T's anchor, the strip's +).

* fix(desktop): preview tab selection follows the tree, not just the reverse

openPreview drove tree reveals, but clicking a preview TAB only activated its
pane in the tree — $rightRailActiveTabId kept naming the previous tab, so
$previewTarget (⌘L quote labels, the titlebar's has-preview state) reported a
tab that wasn't on screen. The mirror now also listens tree→store: when the
interacted zone's active pane is a preview tile, the store selection follows.
Both directions converge on the same id, so no ping-pong.

* fix(desktop): session drags land in preview and page zones

tileZoneHost replaces chatZonePane: a zone hosting any main tile (a Browser
tile, a page) accepts stack and split drops — the known asymmetry where you
could drag a preview tab out but never drag a session in. Only a CHAT zone's
center is the link-to-composer drop; a preview zone's center stacks, since
there's no composer to link to.

* chore(desktop): drop the rail's dead multi-close verbs

closeActiveRightRailTab / closeOtherRightRailTabs / closeRightRailTabsToRight
lost their last callers when ⌘W and the close menu moved to the zone strip's
shared rungs; the tests now exercise closeRightRailTab's own fallback
behavior directly.

* fix(desktop): open_preview lands whenever its session is on screen

The preview.open handler honored the event only when its session was the
FOCUSED one — but the turn that runs open_preview is usually a tile's session,
and by the time the tool fires the user's last click has often parked focus on
main (or anywhere else). The tool reported success, the store never wrote, and
nothing appeared: an explicit 'open reddit' silently vanished.

On-screen is the right bar: honor the open when the session is the primary
chat or any open tile, which keeps truly invisible background sessions from
yanking the pane (offer, don't hijack) without eating opens the user asked
for.

* fix(desktop): one Browser — a second URL navigates it, not a second tab

Tabs were keyed url:<address>, so every distinct page the agent opened
stacked another BROWSER tab — three opens, three Browsers, each titled
identically because the tab deliberately names the surface, not the page.
The title already said singleton; the key disagreed.

URL targets now share one url:browser id: openPreview re-fronts the tab and
swaps its target, and the pane rebuilds its webview against the new address.
Files and artifacts keep per-identity tabs. Restored storage rekeys old
per-address rows and keeps only the most recent.
2026-08-05 16:22:58 +00:00
brooklyn! 4aeffb89c7
fix(desktop): open remote file rows in the in-app preview (#79494)
A plain click on a composer file row in remote mode handed the backend's
file:// URL to the local browser bridge, which cannot resolve a path that
only exists on the gateway host. Route remote non-HTML file targets to the
gateway-backed in-app preview pane instead; local files, ordinary URLs, and
remote HTML (staged locally by openPreviewTargetInBrowser) keep their
existing browser path.

Supersedes #70296 and #57878.

Co-authored-by: lesterlxt <153183032+lesterlxt@users.noreply.github.com>
Co-authored-by: cj52973 <cjenkins@scacpa.org>
2026-08-05 10:17:03 -06:00
hermes-seaeye[bot] 18ed612e5c
fmt(js): `npm run fix` on merge (#79496)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-05 16:16:43 +00:00
Brooklyn Nicholson 7c33b806a1 chore: fix import order, map contributor email
- perfectionist/sort-imports in store/wake-word.ts
- contributors/emails mapping for drew@kainotomic.com -> appletechie
2026-08-05 10:12:31 -06:00
Brooklyn Nicholson 17a5a95871 fix(desktop): open remote file rows in the in-app preview
A plain click on a composer file row in remote mode handed the backend's
file:// URL to the local browser bridge, which cannot resolve a path that
only exists on the gateway host. Route remote non-HTML file targets to the
gateway-backed in-app preview pane instead; local files, ordinary URLs, and
remote HTML (staged locally by openPreviewTargetInBrowser) keep their
existing browser path.

Supersedes #70296 and #57878.

Co-authored-by: lesterlxt <153183032+lesterlxt@users.noreply.github.com>
Co-authored-by: cj52973 <cjenkins@scacpa.org>
2026-08-05 10:07:53 -06:00
Bear Huddleston 069551d19b
fix(desktop): preview remote HTML over SSH (#76008)
* fix(desktop): preview remote HTML over SSH

* fix(desktop): harden remote HTML sanitization
2026-08-05 10:07:35 -06:00
Brooklyn Nicholson 6df3912e0a perf(desktop): coalesce wake.feed frames
Sending one 80 ms frame per RPC is ~12.5 gateway calls/s for as long as the
ear is armed. Drain up to 4 queued frames into a single wake.feed payload
(backend feed() already splits long buffers into engine frames) — ~3 RPCs/s
steady-state. Fix the wake.feed size-cap comment (64000 bytes = 2 s, not
0.5 s).
2026-08-05 10:06:49 -06:00
Andrew d401c27edf fix(wake): address review on client-capture re-arm and feed queue
- wake.status reports effective capture from the armed detector (client vs
  local), plus frame_length/sample_rate; GUI status probes prefer client
- Gateway test doubles accept external_audio on start_listening
- Desktop PCM feeder uses a bounded ordered queue instead of dropping frames
  while a wake.feed RPC is in flight
- /wake on and status/re-arm paths pass client_capture so remote reattach works
2026-08-05 10:03:55 -06:00
Andrew 105fbf6b7d feat(wake): client-capture wake word for remote desktop
Remote headless backends have no PortAudio mic, so "hey hermes" fails even
when openWakeWord is installed. Let the desktop stream 16 kHz int16 PCM via
wake.feed while detection stays server-side.

- wake_word.capture: auto|local|client (+ GUI client_capture prefer)
- WakeWordDetector external_audio queue + feed_audio API
- wake.feed RPC; wake.start/status report capture + frame_length
- Desktop getUserMedia feeder; stop on wake.detected, re-arm after voice
- Docs + unit tests (26 pass in tests/tools/test_wake_word.py)
2026-08-05 10:03:48 -06:00
ethernet b879df27fb fix(desktop): worktree dialog names the project, not the branch 2026-08-05 11:43:09 -04:00
ethernet cb7f594be8 feat(desktop): let convert-a-branch reach remote branches too 2026-08-05 11:43:09 -04:00
ethernet b818c427c8 fix(desktop): mount one worktree dialog instead of one per composer
Every CodingStatusRow mounted its own WorktreeDialog and subscribed to the
same global `$newWorktreeRequest` token, so a single ⌘⇧B with two composers on
screen opened two stacked dialogs — dismissing the front one revealed an
identical empty dialog behind it, which read as the dialog "staying open" after
creating a worktree.

Mount it exactly once in the sidebar (beside ProjectDialog) and drive it from a
`$worktreeDialog` atom, mirroring how the project dialog already works. One
mount cannot double-open. Every entry point (⌘⇧B, the rail's kebab, the
sidebar's + button) now publishes intent instead of rendering its own copy; the
rail and the button pin their own repo so a tile's kebab still targets that
tile's worktree.

The target is resolved at open time by `resolveWorktreeRepoPath`, which walks
the focused surface's cwd then the entered project's root, validating each
candidate against the repo-status probe cache — a project's root folder is not
necessarily a git repo, so existence alone isn't proof. That makes the resolver
the sole authority, so the hotkey no longer pre-gates on `$repoStatus` and now
works from a detached session that sits inside a project. When nothing in reach
is a repo it is a silent no-op: a worktree only exists inside a repo, so there
is nothing to report.

Also adds a project picker to the dialog so the repo can be retargeted before
naming the branch.

E2E: extends worktree-branch-status.spec.ts with a 10-branch repo, visual
snapshots of the base-branch picker and the convert-branch view, a geometry
assertion that the picker isn't clipped by the dialog (fails headlessly on
regression rather than waiting for a human to compare diff images), and a
two-composer test asserting one keypress opens exactly one dialog. Tests 1 and
4 fail against the previous code and pass now.
2026-08-05 11:43:09 -04:00
ethernet b846f0c002 fix(desktop): stop dialogs clipping popovers opened inside them
DialogContent published itself as the portal container for popovers opened
inside a dialog (so focus stays in the dialog and dismissal doesn't close it),
but that same element carried `overflow-y-auto`. Every Select/Popover/
DropdownMenu in a dialog was therefore born inside a scroll box and got
cropped at the dialog's edge — most visibly the worktree dialog's base-branch
combobox, where the branch list was cut off entirely and only the search field
showed.

Split the box in two: the shell keeps position/size/skin and no longer clips
(it stays the portal container), while a new inner body div owns layout and
scrolling. Popovers remain DOM descendants of the dialog, so focus and
dismissal behave exactly as before, but they can now paint past the dialog's
bounds. The banner variant had the same `overflow-hidden` on its shell; its
clip moves to the banner itself, which keeps the rounded bottom edge.

Callers that passed layout/scroll classes (grid, gap-*, p-*, overflow-*) now
pass them via the new `bodyClassName`; `className` keeps sizing and skin.
2026-08-05 11:43:09 -04:00
hermes-seaeye[bot] 2221010717
fmt(js): `npm run fix` on merge (#79155)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-05 07:06:05 +00:00
Tranquil-Flow 530d8148aa fix(desktop): scope restored navigation by profile (#67709)
Scope remembered desktop route and session keys by the encoded active
profile. Discard ambiguous legacy global navigation keys instead of
assigning them to an arbitrary profile. Delay cold-start restoration
until the primary profile and session list have reached renderer.ready.
Preserve explicit deep-link and hidden-window destinations. Restore
and persist session routes only when a direct ID or lineage root is
explicitly owned by the active profile. Clear both profile-scoped route
and session state after resume exhaustion.

Closes #67709
Co-authored-by: Tranquil-Flow <tranquilflow@users.noreply.github.com>
2026-08-05 12:27:17 +05:30
brooklyn! 0a48af25bb
Merge pull request #78812 from NousResearch/bb/profile-share
Share your whole setup: export/import profiles with theme, layout, and skills
2026-08-04 13:35:41 -06:00
brooklyn! ec9572f876
Merge pull request #78854 from NousResearch/bb/session-move-project
Right-click a session to move it into another project
2026-08-04 13:35:09 -06:00
brooklyn! 0106844c9b
Merge pull request #78849 from NousResearch/bb/multi-tab-drag
Select multiple tabs and drag them to a zone together
2026-08-04 13:26:30 -06:00
Brooklyn Nicholson edae3eed10 feat(desktop): move a session to another project from its row menu
'Move to project' submenu in the session actions menu (kebab and right-click,
via the shared MenuKit) listing every project with a folder except the current
owner. Picking one calls session.workspace.move at the project root, mirrors
the new cwd/branch/root into the $sessions cache, and refreshes the tree so
the row hops immediately.
2026-08-04 13:23:00 -06:00
Brooklyn Nicholson 33c1d1f266 feat(desktop): shift-click and opt-click select tabs to drag together
Chrome's grammar on every zone tab strip: Shift-click ranges from the
anchor, ⌥-click (Ctrl-click off-Mac — ⌘ stays close, ⌃ stays the macOS
context menu) toggles, plain click collapses back to one tab. Selected
tabs wear an accent wash; dragging any of them carries the block — the
ghost chip counts it — into a strip slot, a zone edge, or a Shift-span,
so three tabs land in a new zone as one gesture.
2026-08-04 13:18:40 -06:00
Brooklyn Nicholson cc8e97499c feat(desktop): the layout tree moves a tab block as one unit
movePanes/reorderPanesInGroup/mergeZonesWithPane now take a block of pane
ids in strip order: the lead pane decides the drop geometry (slot, split,
span-merge) and the rest stack in behind it, with the pressed tab fronting
at the destination. The tab-selection store holds the block (Chrome
grammar: toggle, anchor range, collapse on plain click), drag-session
carries it — every dragged tab dims, the insertion slot skips the whole
block, and a landed drop spends the selection while a deny-area release
keeps it for a retry.
2026-08-04 13:18:33 -06:00
Brooklyn Nicholson 6e7eafc7e8 feat(desktop): share a profile as a portable bundle - theme, layout, skills
Export stages desktop.json (skin + mode, bundled user-theme definitions,
rail color, layout tree) into the CLI's own profile archive; import applies
it, so the receiver gets the whole look as a ready-to-use profile. Doors:
Export/Import profile... in Cmd-K, an import button beside the rail's +,
and Export in each profile square's context menu. New selectSavePath IPC
(native save dialog); credentials never leave the machine (CLI filter).
2026-08-04 12:07:29 -06:00
brooklyn! b281b2c87b
Merge pull request #78683 from NousResearch/bb/55191-transcript-window
fix(desktop): oversized sessions open without crashing the renderer
2026-08-04 11:57:44 -06:00
brooklyn! 6acae30751
feat(desktop): ctx.os — plugins get a curated door to native notifications, links, files, and clipboard (#78685)
* feat(desktop): expose native OS notifications to plugins via ctx.notifyNative

Desktop plugins can toast in-app (host.notify) but have no sanctioned way to
reach the OS notification pipeline the app's own approval/turn alerts use, so
a plugin surfacing a genuinely notable background event (e.g. a discovery
plugin finding a match) stays invisible once the user steps away from Hermes.

Add a curated per-plugin door instead of exporting the raw dispatcher:

- ctx.notifyNative({ title, body?, silent? }) on PluginContext — attributed
  to the plugin id, routed through dispatchNativeNotification so every
  existing gate applies (master + per-kind prefs, post-connect baseline,
  away-from-app gating, throttle).
- New 'plugin' native-notification kind with its own Settings ▸ Notifications
  toggle (default on), so users silence plugins without losing app alerts.
- New optional `tag` discriminator on the notify payload keys the renderer
  throttle and main-process cross-window dedupe per plugin, so two plugins
  can't collapse each other's session-less notifications.

Consumer: the Index Network desktop plugin wants background opportunity
alerts; anything in ~/.hermes/desktop-plugins gets the same door.

* feat(desktop): ctx.os — the curated OS door for plugins

Fold ctx.notifyNative into a ctx.os namespace so every way a plugin
reaches outside the app window lives behind one attributed door instead
of accreting one top-level ctx method per capability:

- ctx.os.notify — the native-notification door from the previous commit,
  unchanged semantics (plugin kind pref, away-gating, per-plugin throttle).
- ctx.os.openExternal / ctx.os.revealPath / ctx.os.writeClipboard — the
  existing window.hermesDesktop bridge capabilities, now sanctioned and
  result-shaped: each resolves false (never throws) when the bridge or
  member is missing, so a plugin branches on the result instead of
  sniffing the preload surface or crashing on an older shell.

No new Electron surface: everything routes through bridge members the
app already ships; the notification path keeps every existing gate.

---------

Co-authored-by: seref <1573640+serefyarar@users.noreply.github.com>
2026-08-04 11:52:57 -06:00
Brooklyn Nicholson e8ccb4a2ea feat(desktop): ctx.os — the curated OS door for plugins
Fold ctx.notifyNative into a ctx.os namespace so every way a plugin
reaches outside the app window lives behind one attributed door instead
of accreting one top-level ctx method per capability:

- ctx.os.notify — the native-notification door from the previous commit,
  unchanged semantics (plugin kind pref, away-gating, per-plugin throttle).
- ctx.os.openExternal / ctx.os.revealPath / ctx.os.writeClipboard — the
  existing window.hermesDesktop bridge capabilities, now sanctioned and
  result-shaped: each resolves false (never throws) when the bridge or
  member is missing, so a plugin branches on the result instead of
  sniffing the preload surface or crashing on an older shell.

No new Electron surface: everything routes through bridge members the
app already ships; the notification path keeps every existing gate.
2026-08-04 11:33:25 -06:00
Brooklyn Nicholson 62012a5362 feat(desktop): Show earlier pages the DOM, then pulls older history from the store
Show earlier spends the already-materialized DOM budget first and only asks the
session store for another page once that is exhausted, so the click stays cheap
and the store window stays as small as it can be.

Paging has no ceiling: each expand grows the window by one budget page until the
whole transcript is loaded. Branch persistence stays wired throughout —
setMessages is never dropped, so switchToBranch and applyBranchVisibility keep
working on a windowed session.

Co-authored-by: HexLab <8422520+HexLab98@users.noreply.github.com>
2026-08-04 11:32:11 -06:00
Brooklyn Nicholson a538b1c989 fix(desktop): bound the transcript reaching assistant-ui by render cost (#55191)
An oversized session rebuilt an unbounded runtime repository on every store
update and exhausted the renderer's V8 heap, crash-looping the window. The DOM
budget in thread/list.tsx bounds what PAINTS, but every message was still
normalized into the repository first, so a session only had to be heavy — not
visible — to kill the renderer.

selectTranscriptWindow keeps the tail that fits one render-weight page. Weight,
not message count: measured against a real 1,175-session store, a 400-message
cap disengages on 37 sessions that are heavy but short (one is 133 messages /
1.05MB) while firing on 92 long-but-light sessions that were never at risk.

The cut aligns off branch-group boundaries. useRuntimeMessageRepository records
a group's fork point the first time it sees the group, so a window starting
mid-group would re-parent the surviving branches to whatever happened to
precede them.

Co-authored-by: HexLab <8422520+HexLab98@users.noreply.github.com>
2026-08-04 11:32:01 -06:00
Brooklyn Nicholson 1ed702be73 refactor(desktop): share render weight between the two transcript budgets
messageRenderWeight moves out of thread/list.tsx into lib/render-weight.ts.
The DOM page budget already spends render cost rather than message count —
the store window added next needs the same currency, and one weight function
keeps the two layers from drifting apart.

No behavior change.
2026-08-04 11:31:51 -06:00
seref 5d24594ab3 feat(desktop): expose native OS notifications to plugins via ctx.notifyNative
Desktop plugins can toast in-app (host.notify) but have no sanctioned way to
reach the OS notification pipeline the app's own approval/turn alerts use, so
a plugin surfacing a genuinely notable background event (e.g. a discovery
plugin finding a match) stays invisible once the user steps away from Hermes.

Add a curated per-plugin door instead of exporting the raw dispatcher:

- ctx.notifyNative({ title, body?, silent? }) on PluginContext — attributed
  to the plugin id, routed through dispatchNativeNotification so every
  existing gate applies (master + per-kind prefs, post-connect baseline,
  away-from-app gating, throttle).
- New 'plugin' native-notification kind with its own Settings ▸ Notifications
  toggle (default on), so users silence plugins without losing app alerts.
- New optional `tag` discriminator on the notify payload keys the renderer
  throttle and main-process cross-window dedupe per plugin, so two plugins
  can't collapse each other's session-less notifications.

Consumer: the Index Network desktop plugin wants background opportunity
alerts; anything in ~/.hermes/desktop-plugins gets the same door.
2026-08-04 11:28:20 -06:00
Brooklyn Nicholson 91337e5789 fix(desktop): ⌘1 / ⌃Tab return to the chat from a full-page view
Hitting ⌘1 (or cycling ⌃Tab onto the main tab) while Capabilities /
Messaging / Artifacts covered the workspace looked dead: the workspace
pane was already the zone's active tab behind the page, so fronting it
changed nothing on screen.

activateTreeTabSlot / cycleTreeTabInFocusedZone now return the activated
pane id, and the keybind handlers route back to the loaded session (or
the new-chat draft) when the landing pane is the workspace under a full
page — the same rule openSession already applies.
2026-08-04 11:26:12 -06:00
izumi0uu cab8673ea6 fix(dashboard): reload loopback tabs after stale session-token closes
Loopback dashboard tabs now share one one-shot stale-token recovery path across REST 401s, the PTY socket, the structured event socket, and the shared JSON-RPC gateway wrapper. The shared client exposes only an optional close-event interception hook; the dashboard remains responsible for deciding that loopback 4401 means reload.

Constraint: Current main delegates the web gateway to apps/shared JsonRpcGatewayClient, and #54022 review requires a shared-client-compatible close-code hook plus direct ChatSidebar event-socket coverage.
Rejected: Restore the dashboard's old direct WebSocket implementation | stale against the shared JSON-RPC client and would duplicate transport behavior.
Confidence: high
Scope-risk: moderate
Directive: Keep stale-token policy dashboard-specific; the shared JSON-RPC client should expose close events without learning dashboard auth semantics.
Tested: npm --workspace web test (21 files, 106 tests); focused stale-token tests (5 files, 14 tests); npm --workspace web run typecheck; npm --workspace @hermes/shared run lint; npm --workspace @hermes/shared run typecheck; focused web eslint; git diff --check.
Not-tested: Manual browser smoke test across a real dashboard restart.
2026-08-04 15:26:37 +05:30
hermes-seaeye[bot] 9076adaca5
fmt(js): `npm run fix` on merge (#78271)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-04 07:10:23 +00:00
kshitij be54f28b16 test(desktop): cover minimized/hidden window-state + visibilitychange pause for GlyphSpinner
Regression coverage requested in review of #74357: mock
window.hermesDesktop.onWindowStateChanged (pattern from
persistent.test.tsx) and assert minimized/hidden clears the spinner
interval while restore resumes it; also cover document.visibilityState
hidden/visible via visibilitychange.
2026-08-04 12:30:44 +05:30
Mykola Bashmakov 9a20d7f680 perf(desktop): keep spinner frames out of React commits
Advance the existing animated status glyph through its DOM text node instead of React state, and pause its timer for hidden panes or inactive windows. Cover frame advancement, zero update-phase commits, and timer suspension with behavior tests.
2026-08-04 12:30:44 +05:30
kshitij 9d76d48d0a fix(lint): import sort + eslint-disable for timer-handle ref clear in effect
CI-caught: cron-jobs-section had an extra blank line between sorted imports; use-message-stream's visibility-flush effect assigns flushHandleRef.current=null inside a useEffect (legitimate timer-clear, not an atom mirror) — eslint-disable-next-line per the rule's documented convention.
2026-08-03 20:29:58 +05:30
kshitij aece98c5f3 refactor(desktop): shared pulse beat + fully-gated cron peek (simplify folds)
Two findings from the simplify pass on the final trio diff:

- status-pulse: one pause controller + one aligned period timer shared by all StatusPulse instances (ref-counted), instead of N x (document/window/bridge listeners + unsynchronized 5s wakes) — a sidebar can show dozens of pulsing dots. Pause still cancels in-flight animations so the compositor sleeps immediately.
- cron-jobs-section: the runs-peek effect created its interval even while the pane was hidden (callback no-oped but the timer still woke the renderer every 8s/60s per expanded job). Early-return when hidden — visibility is already in the dep array, so becoming visible restarts load + timer.
2026-08-03 20:29:58 +05:30
kshitij e2a2149df4 style(desktop): restore alphabetical import order in agents/index.tsx 2026-08-03 20:29:58 +05:30
Daisuke Suzuki 52fb96de4b perf(desktop): pause hidden-pane timers in agents view, cron sidebar, and floating pet
Partial pick of the surviving renderer hunks from #75395 (perf commit
6502e441d plus fixup 3fbbc9c1d): gate the 500ms subagent now-ticker and
the cron sidebar 1s ticker/run-poll on usePaneVisible, and skip the
legacy floating-pet poll while the document is hidden. Dropped hunks
(electron/main.ts, vitest.setup.ts/config) intentionally excluded.
2026-08-03 20:29:58 +05:30
Mykola Bashmakov 7700597a17 perf(desktop): stop scroll and status loops in busy sessions 2026-08-03 20:29:58 +05:30
M S 416b56b7eb fix(desktop): flush queued deltas on window focus 2026-08-03 20:29:58 +05:30
brooklyn! c2c95f5339
Merge pull request #77713 from NousResearch/bb/review-76744-fold
fix(desktop): stop the inflight dump sandwiching structured mid-turn rows
2026-08-03 08:39:39 -05:00
brooklyn! b536f0697d
Merge pull request #77644 from NousResearch/bb/review-75967
fix(desktop): keep a mid-turn reply on screen when its session is reopened
2026-08-03 08:16:10 -05:00
kshitij c4ac62a7ee fix(desktop): cancel the pending commit-cost measurement rAF
Follow-up to #77652: each runFlush registered a fresh requestAnimationFrame and never cancelled it. Chromium parks rAF callbacks for hidden renderers, so a long hidden stream at the 33ms floor accumulates thousands of parked closures that all fire in the first frame on refocus (all but one no-oping through the stale-frame guard). Track the pending handle, cancel it before requesting a new one (only the newest flush's measurement matters), and cancel on unmount.
2026-08-03 18:45:38 +05:30
Brooklyn Nicholson 7a4d047e37 refactor(desktop): one live-tail vocabulary for transcript reconciliation
Two fixes landed overlapping helpers on the same statement: the mid-turn
reply guard grew `isLiveProjectionRow` / `hasStreamedContent`, while the
inflight-dump guard grew `isLiveTailRow` / `hasStructuralParts`. Two
definitions of "is this row live" and "does it carry content" in one
function is how the next change silently reshapes one of them.

Collapse to a single module-level pair. `isLiveTailRow` now covers pending,
stream ids, inflight projections and sealed interim rows, so the reply guard
also stops treating an interim row as committed history; `hasStreamedContent`
is defined in terms of `hasStructuralParts`. Both text-extension checks route
through `isStrictAnswerTextExtension` rather than a bare `startsWith`.

Also hoists the live-tail lookup out of an inline IIFE and fixes the lint
warnings it carried.

Co-authored-by: 686f6c61 <github@00b.tech>
2026-08-03 08:13:57 -05:00
686f6c61 dbefd27ffb fix(desktop): require structure-bearing row for live-tail same-turn carry
Structure-only same-turn carry used (live(previous) || live(message)), so a
new live text-only assistant at a compression-rewritten ordinal could inherit
reasoning/tool parts from an unrelated historical structured row.

Require the structure-bearing cached row itself to be live-tail (pending /
assistant-stream-* / interim). Add regressions for non-extending live dump
carry and the compression graft rejection.

Addresses salvage path on #76744 / #76444.
2026-08-03 08:08:47 -05:00
686f6c61 2ecc1db276 fix(desktop): scope inflight dump suppression to the live turn tail
Only skip/graft structure for the current live assistant (stream id,
pending, or after the latest user), not completed historical tool rows.
Require live-tail identity for same-turn structure carry. Align journal
overlay with strict answer-text extension.

Addresses review + CI on #76744.
2026-08-03 08:08:47 -05:00
686f6c61 53398ff485 fix(desktop): do not sandwich structured mid-turn rows with inflight dump
Skip pure-text inflight.assistant projections when the transcript already
has reasoning/tool-call structure, and only overlay journal answer text
on strict extension.

Fixes #76444
2026-08-03 08:08:47 -05:00
kshitij 1f692a8be2 refactor(desktop): hoist the reference-line matcher; drop dead textWithoutImageRefs
Follow-up to #77653: textWithoutReferenceLines built a fresh /g RegExp per call and hand-managed lastIndex — but it runs on both sides of every message comparison in the reconcile loops. An anchored non-global regex has no shared-lastIndex hazard and can be hoisted to module scope. Also removes textWithoutImageRefs, whose last production consumer #77653 replaced (kept IMAGE_REF_LINE_RE for extractImageRefs), and retargets its now-stale comment.
2026-08-03 18:37:31 +05:30
Brooklyn Nicholson 34f0427f7f fix(desktop): stop a finished reply rendering twice after history catches up
When a turn's reply commits under its own id, the settled local
`assistant-stream-*` row shifts one assistant ordinal earlier, so ordinal
pairing finds nothing at its slot and re-appends it — the same answer twice.

Drop a settled stream row only when the authoritative transcript already
carries that exact text. Keying `isPendingAssistant` on the explicit pending
flag alone would also have fixed this, but it discards the sibling case in
the same report: a reply that finished locally before the gateway committed
it, where the local row is the only copy that exists.

Co-authored-by: Dolverin <59100064+Dolverin@users.noreply.github.com>
2026-08-03 07:57:33 -05:00
Brooklyn Nicholson b6f15f546d fix(desktop): keep a mid-turn reply on screen when its session is reopened
Switching sessions while a turn streams (or right as it completes) could
leave the assistant reply missing until restart. Resume merges stored
history with the gateway's `inflight` projection, whose assistant row is
text-only and often an empty `assistant-stream-${sessionId}` shell; both
reconcile paths then dropped the local pending row that held the only copy
of the streamed text, reasoning and tool calls.

A shared pair of guards replaces the ad-hoc comparisons at all three sites.
`localPendingSupersedes` accepts the cached row only when it is the same
reply further along — an empty shell it has content for, or text it strictly
extends — so a longer unrelated row can no longer hijack an ordinal or reuse
a stream id, and a retained `inflight.error` snapshot is never mistaken for
an empty shell. `withAuthoritativeTurnState` then takes content from the
renderer while liveness, row id and reactions stay the backend's call, so a
settled shell cannot leave a finished reply spinning.

Co-authored-by: arimu1 <19286898+arimu1@users.noreply.github.com>
2026-08-03 07:52:37 -05:00
Lex 82fd574bad perf(desktop): stop idle chat re-renders — memo ChatView, stable tile props, gated adapter re-sync
Re-derive of PR #38470 on today's main (its target file desktop-controller.tsx no longer exists after the contrib/ refactor; the three surviving ideas are applied at their new homes):

- incremental-external-store-runtime: the dep-less setAdapter effect ran every render; gate on [runtime, store] — behavior-preserving because __internal_setAdapter early-exits on identical store.
- ChatView is now memo()d, and session-tile hoists its inline arrow props to useCallbacks/module constants so the memo actually holds.
- Render-count regression test (mocked Thread) proves an unrelated parent re-render no longer re-renders the chat shell.

Credit: idea and original implementation by @hdd69 in #38470.
2026-08-03 18:17:24 +05:30
rerdi92 1306ac0897 fix(desktop): un-break the .btn-arc rule — '*/' inside a CSS comment ended it early
The comment above .btn-arc contained 'bg-*/', whose */ terminated the comment mid-sentence, leaving 'text-* variant utilities. */ .btn-arc {' as an invalid prelude — CSS error recovery can drop the whole .btn-arc rule. Reword so no */ appears inside the comment.

Extracted from #59352 by @rerdi92 (the rest of that PR — a month-stale icons.ts rewrite and a chunk-size warning-ceiling bump — is superseded/masking and was not salvaged).
2026-08-03 18:16:46 +05:30
kshitij 5ffbea81e9 fix(desktop): escalate gateway reconnect on elapsed time, not attempt count
With the full-jitter backoff (300ms base) six attempts can elapse in ~9s,
so the old RECONNECT_ESCALATE_AFTER=6 attempt threshold raised the
recoverable boot error during a brief post-boot blip — breaking the
'a remote that drops post-boot keeps looping with NO boot.error' contract.
Escalate after RECONNECT_ESCALATE_AFTER_MS (45s, matching the old
deterministic 1->15s ladder's calibration) elapsed since the first failed
reconnect of the episode. Reset on clean open, manual/wake reconnect, and
soft switch, preserving the reset-on-success path.
2026-08-03 18:10:30 +05:30
Hermes Coder ed66ff17d8 fix(desktop): full-jitter backoff on gateway WS reconnect loops
All three desktop reconnect loops (primary gateway boot, secondary
multi-profile gateway pool, plugin event socket) used bare exponential
backoff with no jitter. After a gateway restart every disconnected
client redials on the exact same schedule, so the reconnect attempts
land in lockstep instead of spreading out -- a burst that can starve
the gateway's file descriptors while it's still coming back up.

Add reconnect-backoff.ts implementing AWS-style full-jitter backoff
(random delay in [0, min(cap, base * 2^attempt))) and wire it into all
three call sites in place of their local Math.min/2**attempt math.
Manual reconnect paths already reset the attempt counter and bypass
the timer entirely -- unchanged.
2026-08-03 18:10:30 +05:30
Paulo Nascimento 2ebe175dc1 fix(desktop): sort reference-kinds import per lint gate 2026-08-03 18:10:23 +05:30
Paulo Nascimento fed10fa245 test(desktop): cover wire reference normalization edges 2026-08-03 18:10:23 +05:30
Paulo Nascimento 3060beea07 fix(desktop): dedupe optimistic user turns for all wire references, not only images 2026-08-03 18:10:23 +05:30
Adolanium b9b0505cfc fix(desktop): measure adaptive stream flush through the deferred commit frame
scheduleDeltaFlush's adaptive floor is driven by lastFlushCostRef, but
runFlush only timed flushQueuedDeltas(), the synchronous store write.
While a session streams, syncSessionStateToView defers the $messages
publish (React commit + Streamdown re-parse) to its own rAF, so the
measured cost stayed near zero and the floor collapsed to the fixed
33ms path no matter how expensive the real commit was.

runFlush now records the write cost as a fallback, then extends the
measurement through a rAF registered after the view-sync one: it runs
in the same frame right after the deferred commit, and the rAF
timestamp marks frame start so only in-frame work is counted, not the
vsync wait. A stale callback from before a newer flush is ignored, and
a hidden renderer that never fires rAF keeps the write-cost fallback.
2026-08-03 18:10:18 +05:30
kshitijk4poor dd08277104 test: expect omit_messages in the tile-delegate resume call shape
Two more call-shape-pinning tests (cold tile resume, default-profile
resume) assert session.resume's exact params; the delegate passes
omit_messages: true like every other Desktop resume call site.
Swept all 5 desktop test files that reference session.resume/activate:
380 of 381 files green (the one failure is a pre-existing locale-
dependent number-grouping test that fails identically on clean main).
2026-08-03 13:45:08 +05:30
kshitijk4poor 67fb0d7c2e test: expect omit_messages in the queue-drain resume call shape
Two queue-drain tests added on main pin session.resume's exact params;
the drain path now passes omit_messages: true. Assertion-only update.
2026-08-03 13:45:08 +05:30
Daniel Blank cd41454dfc fix(gateway): let Desktop omit duplicate transcripts on session resume
Salvage of #69926: omit_messages support ported from the PR's
tui_gateway/server.py base onto the post-split methods_session.py
layout. When a Desktop client passes omit_messages=true on
session.resume / session.activate, the RPC returns messages: [] with
messages_omitted: true and an accurate message_count, skipping the
potentially multi-megabyte compression-lineage serialization over the
WebSocket; Desktop hydrates the transcript via the authenticated REST
route in parallel.

The PR's bundled cron-outputs endpoint and codex quiet-timeout bump
were dropped from this salvage as unrelated (invited back separately).
2026-08-03 13:45:08 +05:30
kshitijk4poor e1843c7d08 style: fix the type-import sort position in pet-gallery.test.ts
perfectionist/sort-named-imports orders 'type GatewayRequest' by its
name, so it belongs before loadPetGallery (eslint error, not warning).
2026-08-03 13:41:12 +05:30
kshitij b0089e8bfa perf(desktop): skip store update when pet metadata is unchanged
mergePetInfoMeta now returns the same object reference when all fields
match, and callers skip setPetInfo on reference equality. Without this,
every 15s poll and window-focus refetch fired a nanostores set with a
new-allocated object, triggering a React re-render of FloatingPet even
when nothing changed — a regression from the old samePetRevision guard
which returned without calling setPetInfo.
2026-08-03 13:41:12 +05:30
kshitijk4poor cb0226d4ae docs: update stale samePetRevision comment reference
The helper was extracted and renamed to hasPetSpriteForMeta +
mergePetInfoMeta; the pet.changed comment still cited the old name.
2026-08-03 13:41:12 +05:30
embwl0x 1573829a7c fix(desktop): avoid repeated pet spritesheet fetches 2026-08-03 13:41:12 +05:30
Johnny 51defb9e29 fix(desktop): adapt right pane probe to per-cwd status 2026-08-03 13:39:33 +05:30
Johnny 1051434325 perf(desktop): isolate right pane layout work 2026-08-03 13:39:33 +05:30
kshitij 372494fdf9 style(desktop): eslint --fix import order in sessions-section test
Review follow-up on the #75714 salvage: perfectionist/sort-imports
would fail lint CI; autofixed.
2026-08-03 10:00:20 +05:30
StanleyStetson 416f91d271 fix(test): export VirtualSessionListProps and assert sessions array identity change 2026-08-03 10:00:20 +05:30
StanleyStetson 09ed1ac950 fix(desktop): memoize sidebar flatRows and row renderers to prevent scroll jitter (fixes #73629)
Fix direction inspired by PR #73674 by @drbronson with added Vitest component unit tests.
2026-08-03 10:00:20 +05:30
hermes-seaeye[bot] d79a6f0e3e
fmt(js): `npm run fix` on merge (#77300)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-03 03:44:44 +00:00
Brooklyn Nicholson 9e373b3828 fix(desktop): paste rich text with images as text, not blank attachments
Copying a Discord thread (or any rich-text selection with images) attached
one or more blank thumbnails and dropped the message text entirely.

Two causes. The clipboard's `text/html` was scraped for inline
`<img src="data:…">` regardless of whether the copy carried its own text —
and what Discord ships beside each image embed is a 32x5 blurhash
placeholder, which is exactly the blank attachment. Then, because any image
blob short-circuited the paste handler, the prose that came with it never
reached the composer.

Inline HTML images now only count for an image-only copy, and are ignored
below a thumbnail-sized floor so spacers and trackers don't attach either. A
mixed paste attaches its real images and still inserts its text.

Also registers pasteAndMatchStyle in the Edit menu — Cmd+Shift+V had no menu
entry, so the chord was never translated into an editor command anywhere in
the app.
2026-08-02 22:28:28 -05:00
Brooklyn Nicholson 17bdc99db4 feat(desktop): reload a tab from its right-click menu
Right-click any tab and pick Reload: the pane's content remounts in
place — effects re-run, state resets, measurements are retaken — while
the tab keeps its slot and every other tab is untouched.

A per-pane epoch atom keys the contribution inside the zone body, so
reload never rewrites the layout tree. Both tab menus offer it: the
zone strip menu (tool panels, the file tree, a fresh draft's main tab)
and the session tab menu (tiles + the loaded main tab).
2026-08-02 22:08:09 -05:00
Jason Chai 5b73cfcc98 fix(desktop): bound review workspace scans 2026-08-03 00:08:30 +05:30