Commit Graph

3200 Commits

Author SHA1 Message Date
royzhrxy-glitch a787bfeab9 fix(desktop): restore profile/agent switching in release builds (nanostores 1.4.2)
nanostores 1.4.0-1.4.1 annotate batch() @__NO_SIDE_EFFECTS__. Rollup
(via vite build) honors that and erases a result-unused batch(...) call
as dead code -- callback included. Since d57f94a33/053eb7aab/4e520f085
moved the gateway-switch publication (activate() +
+ ) inside batch(), packaged desktop builds lost the entire
publication: clicking a profile in the rail did nothing at all.

Dev builds and vitest run unminified, so only the packaged app broke.

nanostores 1.4.2 removes the annotation from batch() (it stays on the
creation functions, where it is correct). Bump all three pinned copies
(apps/desktop, apps/bootstrap-installer, ui-tui) and add a regression
test asserting the installed nanostores never re-annotates batch.
2026-08-19 01:58:21 -07:00
brooklyn! 63565fa26b fix(desktop): keep auto-speak silent across the stream-id rewrite
Supersedes #75649, #86637, #87672, #88642.

Fixes #86601
Fixes #87652
Fixes #87823

Co-authored-by: Charmmy <lilShawtty@qq.com>
Co-authored-by: chelsealong <chelsealong@126.com>
Co-authored-by: Olympusbuildz <Olympus.roots@outlook.com>
Co-authored-by: Ricardo Mendes <ricardo.mendes@maiolabs.ai>
2026-08-19 02:56:55 -05:00
brooklyn! 70e818c6e3 Merge pull request #89814 from NousResearch/bb/session-group-icon-fix
fix(desktop): close the blank hole in the Sessions header
2026-08-19 02:49:01 -05:00
brooklyn! ca4a0c4358 Merge pull request #88233 from helix4u/fix/get-windows-recovery-command
fix(desktop): recover missing get-windows binding
2026-08-19 02:46:05 -05:00
Brooklyn Nicholson 0223710d00 test(desktop): compare get-windows installer path via realpath
require.resolve returns the macOS realpath (/private/var/...) while
os.tmpdir() stays on the /var symlink, so a raw join() deepEqual failed
even though the spawn was correct.
2026-08-19 02:39:44 -05:00
brooklyn! b82f8b0654 Merge pull request #89257 from xxxigm/fix/desktop-files-panel-remote-download
fix(desktop): let Files panel download remote backend files
2026-08-19 02:38:30 -05:00
Teknium 2507bc649f fix(desktop): atomic profile-switch publication v2 — fail open, lease mid-dial entries (#89622, #46651)
Re-lands the goals of the reverted atomic-publish series (#89483, reverted
in #89785) without the fail-closed decline path that killed profile clicks,
and fixes the underlying pruner race the original series exposed.

Two changes relative to the restored (pre-series) behavior:

- Publication is atomic and mode-safe (#46651): the switch resolves the
  target's connection descriptor CONCURRENTLY with the socket work and
  publishes $activeGatewayProfile + $connection in one nanostores batch()
  frame, so no request or plugin mode-listener observes the new gateway
  beside the previous profile's descriptor. Unlike the reverted series, a
  failed descriptor lookup fails OPEN — the switch still lands, the previous
  descriptor stays, boot/reconnect resyncs it later, and the failure is
  logged instead of swallowed.
- The live-work pruner can no longer kill a switch mid-dial (#89622's root
  race): ensureGatewayForProfile / ensureGatewayForAgent lease their entry
  (activationLeaseUntil, 30s bound) for the duration of the dial and
  pruneSecondaryGateways spares leased entries. A switch target is not yet
  active, has no live sessions and holds no request lease, so any prune
  recompute during a cold pool spawn used to dispose the dialing entry.
  Bounded lease: an orphaned one self-expires.

The agent door logs a non-landing activation (target removed mid-dial)
instead of resolving silently, but never fails the switch closed.

Live E2E (Electron over CDP, 3 local profiles, cold + warm): 10 sequential
switches, rapid two-click interleave (last click wins), 6-click stress run —
all land, overlay always clears, fresh pool backends spawn on cold switches.

Tests: gateway-activation-prune-lease suite (mid-dial prune survival on both
doors, lease release, lease expiry); the invalidated-registry-identity pin
updated for the concurrent (read-only) descriptor probe.
2026-08-19 00:03:35 -07:00
Brooklyn Nicholson 0e980e8049 fix(desktop): keep mark-all flush with the sessions header actions
The unread check-all was a sibling of the +/filter cluster. The header
is justify-between, so that extra child sat in the middle as a blank
24px hole until hover.
2026-08-19 01:31:11 -05:00
brooklyn! 38f79e25a0 Merge pull request #89798 from NousResearch/bb/tour-demo-remove
Drop the dev-only tour demo
2026-08-19 01:27:34 -05:00
brooklyn! e4771b92a7 Merge pull request #89808 from NousResearch/bb/turn-activity-timer
Time the gaps a working turn spends producing nothing
2026-08-19 01:25:37 -05:00
Brooklyn Nicholson ff8c4159e8 chore(desktop): drop the dev-only tour demo
The demo existed to iterate on the tour UI without spending an agent turn per
look. That work is done and merged, so it is scaffolding now — a floating
button, a hotkey, a palette row and a window hook, none of which anyone needs
again.

Removing it restores the credit-notice demo's original single-disposer wiring
and drops the Compass icon export, which the demo was the only consumer of.
The tour feature itself is untouched.
2026-08-19 01:20:39 -05:00
Brooklyn Nicholson ccaa4872d8 fix(desktop): time the gaps a working turn spends producing nothing
The transcript's activity row went silent for most of an agentic turn. Two
gates were too narrow. It mounted only while the tail bubble's own status was
`running`, so a turn that seals a bubble mid-flight (message.interim) or
finishes one while the agent keeps going left a settled message at the tail and
unmounted the row entirely. And its activity signature was part count plus text
length, which a landing tool result does not change — a result mutates the part
that was already there — so the pause after a call ended was read as more of
the same silence and dated from whenever the call started.

Between them, the app spent whole stretches with the composer's arc border lit
and Stop armed while the thread showed nothing and counted nothing.

The row now follows the same busy signal the composer does, is mounted by the
tail unconditionally, and decides for itself whether the turn owes the user a
line. A named wait (compaction, provider wait, a tool being drafted) says so
immediately; an unnamed gap earns two seconds of quiet first, so a run of quick
calls doesn't strobe. It still defers to the two waits already accounted for
elsewhere: a prompt the user is answering, and a tool call in flight carrying
its own row and timer — anywhere in the message now, not just the last part,
and excluding the silent tools that render nothing to defer to.

Renamed to TurnActivityIndicator / `aui_turn-activity`: it hasn't measured a
stall since it started measuring the whole turn.
2026-08-19 01:19:33 -05:00
Brooklyn Nicholson 7c3c2d112b refactor(desktop): give each chat surface its own turn clock
`turnStartedAt` was only reachable through the global atom, which mirrors
whichever session is currently viewed — the same trap `$busy` and `$messages`
were moved out of. Anything in a tile that measures a turn was therefore
timing the primary chat's.

Put it on SessionView beside the other per-surface signals: the primary falls
back to the draft atom while a new chat has no slice yet, a tile reads its own.
2026-08-19 01:19:21 -05:00
hermes-seaeye[bot] f82f2dbabd fmt(js): npm run fix on merge (#89795)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-19 06:13:16 +00:00
Brooklyn Nicholson f0d0971899 feat(desktop): add a dev-only tour demo
Looking at the tour UI needed an agent turn per iteration. This adds a demo
that walks the Artifacts page — a route step, a late-mounting target,
per-step narration, the return trip — behind three triggers: a floating
button, Ctrl+Shift+X, and a palette row. The button is not redundant; a chord
can be eaten by a menu accelerator or the main process before the renderer
sees it.

DEV-gated and dynamically imported, so it is absent from production builds.
2026-08-19 00:52:55 -05:00
Brooklyn Nicholson 702d7bacc9 feat(desktop): name the app's shared surfaces for tours
Tours address elements by selector, and a positional nth-child path breaks on
the next re-render. These are the durable handles, applied at the shared
primitive rather than per screen: every route overlay's nav and its rows, every
settings field (keyed by its config schema key, so new fields are named for
free), the filter tabs on any search shell page, and artifact cards.

One edit per primitive covers every screen built from it, which keeps the tour
vocabulary small enough to stay accurate.
2026-08-19 00:52:55 -05:00
Brooklyn Nicholson d433a452cc feat(desktop): give tours a themed spotlight and directional motion
The stock driver.js look is replaced with the app's own: unfocused UI fades
toward var(--background) and desaturates, so it recedes in light and dark
without a mode branch. The dimming is masked to a feathered cutout that tracks
driver's eased stage rect each frame, keeping the highlighted element crisp —
a plain backdrop-filter blurs the spotlight too, because it clips to the
element box rather than the cutout.

Popovers enter from wherever they land: left, right, above or below their
target, and a shorter settle for centered narration steps, which have nothing
beside them to measure against. Travel, duration and easing are tokens on
.driver-popover, and the whole thing collapses to a fade under
prefers-reduced-motion.
2026-08-19 00:52:55 -05:00
Brooklyn Nicholson 98bbfbda6e fix(desktop): keep a tour bound to its target across navigation and re-renders
A tour step that moved the app deadlocked: driver.js checks waitForElement
before running any hook, so the step waited for an element on a page the hook
had not opened yet. The move now happens in the step's own onHighlightStarted
— the one hook driver fires however a step is reached — so clicking Next,
pressing an arrow key and calling the API all behave the same.

Steps also re-bind themselves. driver.js holds the highlight as a node
reference and re-measures it, so a poll or refetch that swapped the node left
it measuring a detached element and the spotlight vanished, looking like the
tour closed itself. One observer now covers both cases: the target that has
not mounted yet, and the one that was replaced underneath.

Orphaned overlays are swept before a tour starts, since driver.js can only
tear down what its own instance built.
2026-08-19 00:52:55 -05:00
Brooklyn Nicholson 0d572e063b feat(desktop): answer tour.request from the renderer
Translates the wire payload into a normalized action and dynamic-imports
the engine, keeping driver.js off the boot path. Active session only — a
background turn must never paint an overlay over what the user is looking
at.
2026-08-19 00:48:32 -05:00
Brooklyn Nicholson ec69940537 feat(desktop): run tours inside the preview pane
The guest page is out-of-process, so the first action injects a
self-contained bundle over executeJavaScript — the driver.js IIFE, its
stylesheet, and the engine source — parked on window globals so later
actions reuse the live instance. Injection is idempotent and vanishes with
the page, so a navigation resets the tour.

This is what lets a tour walk through any web app open in the in-app
browser, not just Hermes itself.
2026-08-19 00:47:58 -05:00
Brooklyn Nicholson 018176915b feat(desktop): tour engine — highlight and narrate any DOM
A surface-agnostic walkthrough engine plus the API that drives it.
collectTourTargets scans any document for addressable elements and marks
each selector stable (identity-based, survives a re-render) or positional;
runTourEngine turns one action into a driver.js highlight, a multi-step
tour, or a step change.

Both are written self-contained — no imports, no closures — so the same
source runs in the renderer and, stringified, inside a webview guest page.
Popovers are repainted from the app's own theme tokens, so tours follow
every theme and custom skin.

The named verbs (startTour, showTourStep, nextTourStep, …) are the public
API: the agent tool is one caller, and a feature can ship its own curated
tour through the same entry.
2026-08-19 00:47:58 -05:00
Brooklyn Nicholson 5dc20eb319 build(desktop): add driver.js for guided tours
Pure-ESM, MIT, ~5KB gzipped, no runtime deps. Excluded from optimizeDeps:
it only enters the graph through a dynamic import, so letting the scanner
discover it at first use prebundles the ?raw IIFE as a module (breaking the
raw-text transform) and forces a mid-session page reload.
2026-08-19 00:47:58 -05:00
emozilla 5d3c15aaa7 restore profile switching 2026-08-18 22:44:03 -07:00
hermes-seaeye[bot] 292d0df04d fmt(js): npm run fix on merge (#89759)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-19 05:11:57 +00:00
David Dudok de Wit 9ed738fa0c fix(desktop): stabilize gateway settings interactions 2026-08-18 22:03:25 -07:00
David Dudok de Wit c41ec9c588 refactor(desktop): scope gateway context to Sessions 2026-08-18 22:03:25 -07:00
David Dudok de Wit e999df46fa fix(desktop): keep gateway search shortcut local 2026-08-18 22:03:25 -07:00
David Dudok de Wit 6e9e12075c refactor(desktop): keep gateway details contextual 2026-08-18 22:03:25 -07:00
David Dudok de Wit 2c6dcf91c7 feat(desktop): keep large gateway registries searchable 2026-08-18 22:03:25 -07:00
David Dudok de Wit 0a36fa7fbd fix(desktop): keep remote bot identity visible 2026-08-18 22:03:25 -07:00
David Dudok de Wit 72fc5ab7ca fix(desktop): use gateway terminology in connection UI 2026-08-18 22:03:25 -07:00
David Dudok de Wit 3095ac54db docs(desktop): clarify connection settings copy 2026-08-18 22:03:25 -07:00
David Dudok de Wit 2228bc7d88 feat(desktop): remember the last used source 2026-08-18 22:03:25 -07:00
David Dudok de Wit ccd30f955c feat(desktop): distinguish sources from profiles 2026-08-18 22:03:25 -07:00
David Dudok de Wit 0ab0ed54ff feat(desktop): clarify active and default connections 2026-08-18 22:03:25 -07:00
David Dudok de Wit 4ea516b8d4 docs(desktop): explain multi-source session scoping 2026-08-18 22:03:25 -07:00
David Dudok de Wit fa73b45d34 fix(desktop): isolate state across source switches 2026-08-18 22:03:25 -07:00
David Dudok de Wit 0ca7443824 feat(desktop): switch sources from the Sessions rail 2026-08-18 22:03:25 -07:00
David Dudok de Wit 7245b022db feat(desktop): route REST through registered sources 2026-08-18 22:03:25 -07:00
amanning3390 8e5f55f83b feat(hermes-bots): add a collapsible group activity view
Bot Mode group rooms only showed a single "is thinking…" line while bots
ran, and nothing after a turn settled or failed — no way to see what the
room did without reading the whole transcript.

- Runtime-only, bounded activity feed per room (GROUP_ACTIVITY_LIMIT),
  recording truthful turn events: queued, working, replied, passed,
  timed-out, failed, cancelled, settled, delivered.
- Every event is tagged with the room epoch it belongs to; the view shows
  only the CURRENT run, so a superseding send (or a rename that re-keys
  the room) can never surface stale activity.
- Quiet disclosure in the room header: collapsed by default, the collapsed
  row shows the latest event summary; expanding lists the current run's
  events newest-first with per-state glyphs and tones.
- Never persisted and never hydrated — the transcript stays the only
  durable record, so activity cannot be replayed as history.

Tests: 8 new behavioral + source-contract cases in
tests/group-activity.test.mjs (settled arc, failed turn, supersede/cancel,
epoch filtering, bounded feed, runtime-only guarantee, labels, disclosure
a11y contract).
2026-08-18 22:03:02 -07:00
pierrenode 589bee99cf fix(bot-mode): never resubmit into a still-running stranded group member
runGroupChatRounds' responder selection had no awareness of the stranded/
harvest state the previous round's harvest pass just confirmed. A member
whose turn timed out (marked stranded) but is STILL genuinely running
could be re-selected as a responder in a later round of the same
invocation — resolveGroupResponders has no busy filter, and a member's
watermark is bumped past the stranded timeout regardless of outcome, so a
fresh delta re-qualifies them.

Re-selecting them fires another prompt.submit into their live session.
tui_gateway's _handle_busy_submit treats that as a normal busy mid-turn
prompt: by default it either redirects the live turn in place or, for
older agents, hard-interrupts it and queues the new text as the next
turn. Either way the member's original in-flight work — exactly what the
stranded/harvest mechanism exists to protect — gets abandoned or killed,
undermining the "never lost, just late" guarantee.

Filter responders against the room's current stranded map (freshly
confirmed by this round's own harvest pass) before selecting who speaks.
A member with a live stranded marker is skipped; the next harvest pass
picks their reply up once it actually lands.

Added a regression test exercising runGroupChatRounds end-to-end with a
member confirmed still-busy: without the guard the round loop resubmits
into their session (asserted via prompt.submit call count); with the
guard it never does, and the marker survives untouched. Mutation-verified:
temporarily reverted the filter and confirmed the new test fails
(2 !== 0) fast, without a real wall-clock wait.

Full hermes-bots plugin suite: 246/246 pass (47 files). node --check
clean on both changed files.
2026-08-18 22:02:32 -07:00
Teknium d440891204 test(desktop): update canonical-chat-identity expectation for salvaged PR #89031 2026-08-18 21:52:41 -07:00
marketing2981 ed47d41dc9 fix(desktop): bot navigation no longer forces the all-profiles sidebar on
Every host.openSession call in the Bot Mode plugin omitted
keepAllProfilesScope, so the SDK applied its default and flipped
$showAllProfiles back on whenever the target session belonged to a
different profile than the live gateway (sdk/index.ts:
options.keepAllProfilesScope !== false => setShowAllProfiles(true)).

For anyone running more than one profile this silently undid the sidebar
profile filter: narrow Sessions to one profile, click any other bot, and
the unified all-profiles list came back.

Bot navigation is an explicit context switch into that bot's profile, so
pass keepAllProfilesScope: false at every openSession call site (4 on
current main after the plugin.js refactor consolidated the original 7).

Salvaged from PR #89031 onto current main; includes contributor mapping.
2026-08-18 21:52:41 -07:00
Teknium af3373f7c8 test: follow-up for salvaged PR #89020 — focused-highlight contract includes the !activeGroup guard 2026-08-18 21:52:11 -07:00
Shawn Wang 80f080b6ed fix(bot-mode): highlight selected group chat
Signed-off-by: Shawn Wang <32839114+enwaiax@users.noreply.github.com>
2026-08-18 21:52:11 -07:00
chelsealong 3809f740c6 fix(bot-mode): reseed $selectedBot on register so re-enabling never leaves it stale
nanostores' .listen() never replays the current value the way .subscribe()
does, so the $focusedBotProfile listener in register() only kept
$selectedBot current from the moment it was attached. A disable -> profile
switch -> re-enable cycle (Settings > Plugins) left $selectedBot pointed at
whichever bot was active before the plugin was disabled, so the roster
highlight fallback and Routines scoping could start from a stale bot.

Extract the sync into bindProfileSync(), which reseeds $selectedBot from
the profile store's current value before attaching the listener. This runs
on every register() call, so re-enabling the plugin always starts in sync.

Salvaged from PR #89637 (the pane-precedence portion was superseded on main
by the $focusedBotProfile design; this residual reseed gap remained).
Regression test mimics real nanostores get/listen semantics and fails
without the reseed.

Fixes-residual-of: #89625
2026-08-18 21:51:36 -07:00
David Dudok de Wit 00e9a32454 fix(desktop): keep Bot Mode routines docked 2026-08-18 21:50:56 -07:00
Jay 104ec97595 fix(bot-mode): group room preview shows the bot handle, not @default
`botHandle()` exists so that, per its own comment, "the word 'default'
never surfaces in the UI" — it presents the primary profile as `hermes`.
The roster rows, mention resolution and the group-chat prompt all route
through it. Two preview paths did not, and rendered the raw profile name:

- `GroupRow`'s room preview line built `@${last.from?.name}`, so a group
  room read `@default: …` while the bot answers to `@hermes`.
- `previewKind()` returned the raw captured name from the bot-to-bot
  delivery prefix, so the `🤖 @<name>` badge and its tooltip could show
  `@default` too.

The mismatch is presentation-only, but it reads as a routing bug: the
room says the message came from `@default` while `@default` is not a
handle the mention resolver accepts, so users reasonably conclude
bot-to-bot addressing is broken when it is working correctly.

Both paths now map through `botHandle()`. `GroupRow` passes the matching
member so a bot with a custom handle keeps it; `previewKind` maps the
lowercased sender name, which leaves every non-primary profile unchanged.

Tests: the primary profile resolves to `hermes` and a named profile keeps
its own handle (behavioural, in the existing previewKind suite), plus a
source-shape assertion for the render path matching that file's
convention. Both new assertions fail against the pre-fix source.

Fixes #89484
2026-08-18 21:50:31 -07:00
Teknium cb0fd836ac feat: Bot Mode blob avatars gain 4 new silhouettes via blobatar 2.0.0
blobatar 0.2.0 -> 2.0.0 (gen2). Ten silhouettes instead of six:
capsule, triangle, hexagon and droplet join round, organic, boxy,
nub, cloud and sun. BLOB_KIND_TRAIT repinned to gen2 band centers
(empirically verified against the published package: every pinned
value resolves to its named silhouette across seeds). The avatar
picker derives from BLOB_KINDS, so the new shapes appear there
automatically. Note: gen2 remaps most unpinned seed->face mappings
by design (upstream generation change).
2026-08-18 21:45:29 -07:00
hermes-seaeye[bot] 395c70d616 fmt(js): npm run fix on merge (#89703)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-19 04:05:24 +00:00