Commit Graph

2857 Commits

Author SHA1 Message Date
Teknium 9c58a78a7d feat(desktop): Capabilities-wide profile scoping + one-click hub installs on the Skills tab
Extends the Capabilities "Configuring:" profile selector (#86548) from
Tools/MCP to the WHOLE view — Skills, Tools, MCP, and Browse Hub now all
read and write the same selected profile — and brings Bot Mode's
one-click Skills Hub picker into the main Capabilities -> Skills tab.

Scope widening:
- skills/index.tsx: the selector renders once above whichever tab is
  active. Skills list, toggles, bulk ops, editor, and archive are scoped
  via the trailing-profile pattern; the skills RQ key gains the scope key.
  Toolsets analytics (usage badges) load per scope. SkillsHub and the
  hub picker are keyed/remounted per scope. Scope changes drop the open
  editor/archive dialog and in-flight analytics (same hazards as an
  app-wide profile switch); an app profile switch clears the override.
- hermes.ts: getSkills, setSkillEnabled, get/edit/deleteLearningNode,
  getUsageAnalytics, and all seven skills-hub fetchers take the optional
  trailing profile? (omitting preserves exact app-wide behavior).
- store/hub-actions.ts: runHubAction threads profile through spawn and
  getActionStatus polling so install/uninstall/update and their logs run
  against the scoped backend.
- hub.tsx: sources/search/preview queries keyed+scoped per profile;
  install/uninstall/update/scan route to the scoped profile.
- archive-skill-confirm-dialog.tsx: optional profile prop.

One-click hub installs (from Hermes-Bot-Mode):
- skills/embedded-hub-picker.tsx: collapsible, resizable iframe of the
  live Skills Hub (hermes-agent.nousresearch.com/docs/skills?embed=picker)
  on the Skills tab. Origin-checked hermes-skill-pick postMessages route
  through the standard hub action pipeline (background action, tailed
  log, optimistic flip, Skills list + slash-completion invalidation),
  scoped to the selected profile.
- i18n: skills.hub.picker* keys (en + zh; others fall back).

Tests: index.test.tsx — new case asserts picking a profile on the Skills
tab refetches skills scoped to it and routes toggles there (6/6);
toolset-config-panel 28/28. Full typecheck (3 tsconfigs) + eslint clean.
2026-08-15 16:21:56 -07:00
hermes-seaeye[bot] 17c7b0bebd fmt(js): npm run fix on merge (#87296)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 22:15:27 +00:00
Teknium 3e8ab06107 fix(desktop): never send ordinal-only truncation — resolve durable row ids by content
Client half of #87059. The gateway now fails ordinal-only truncation
closed for durable sessions (#87150), which turned the mis-aimed cut into
a visible edit-resend error for any bubble without a bound rowId (edit
after an interrupted turn, unstamped resume). Make the Desktop always
produce a durable address or degrade safely:

- runRewindSubmit: when a truncation request lacks a durable address,
  resolve the target's row id by exact content against session.history
  (which ships row_id per persisted row). Resolution is
  exact-or-nothing: a unique text match wins; ambiguity is accepted only
  when the target is provably the newest persisted turn (the
  edit-after-interrupt shape). Anything else degrades to a PLAIN
  resubmit — never a guessed cut. The client ordinal is dropped either
  way (its space can diverge from the gateway's — the #87059 root).
- planReload/planRestore: degrade failed turns to a plain resubmit
  (extends the #86623 pattern to regenerate/restore) and carry the
  turn's persisted sourceText as the content key.
- rebindSurvivorRowIds: iterate the same failed-turn-aware ordinal
  space as the truncate math.
- session-tile-actions: reload goes through the shared runRewindSubmit
  primitive instead of a raw prompt.submit, so the tile surface gets the
  same discipline.
2026-08-15 15:09:11 -07:00
vondelomlo c2a50a8662 fix(desktop): skip failed turns in the backend-facing user ordinal space
A user turn whose submit failed keeps its optimistic bubble but never
reached the gateway, so counting it makes every later
truncate_before_user_ordinal overshoot the backend index (refused 4018,
regenerate dead for the rest of the session). Skip failed turns in the
one shared visible-user ordinal space (visibleUserMessageIndices) used by
truncate ordinals, ordinal->index resolution, and survivor-rowId
rebinding.

Based on #41275 by @vondelomlo, relocated onto the split
use-prompt-actions/ modules and widened from visibleUserOrdinal to the
shared index helper.
2026-08-15 15:09:11 -07:00
hermes-seaeye[bot] 0c50bdbdea fmt(js): npm run fix on merge (#87257)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 20:44:31 +00:00
hermes-seaeye[bot] 9c7f92bf93 fmt(js): npm run fix on merge (#87251)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 20:38:02 +00:00
Yingliang Zhang eec4d5ec19 style: order deep-parent fixture import before siblings (perfectionist/sort-imports) 2026-08-15 13:35:32 -07:00
Yingliang Zhang 9f78a0d37f fix(desktop): preserve turn-elapsed timer across session switches
Rebased onto latest origin/main. Resolved conflicts in:
- use-session-actions.test.tsx: kept both HEAD's image-attachment test
  and PR's turn-clock restoration test (orthogonal features)
- use-session-actions/index.ts, gateway-event.ts, server.py,
  test_tui_gateway_server.py, test_protocol.py: adapted to HEAD's
  refactored structure while preserving PR's turn-origin tracking
2026-08-15 13:35:32 -07:00
Tommy00748 93a9b2318f feat(desktop): show per-turn wall-clock duration in the transcript
Each assistant reply now carries a small time badge below the message text
showing how long its turn took (message.start -> message.complete), so
users can gauge task latency at a glance without hovering.

The duration is computed renderer-side from the per-session turnStartedAt
timestamp the app already tracks and stamped onto the ChatMessage at
completion (successful and failed turns alike). It is not persisted
backend-side, so messages hydrated from history have no badge — matching
how reasoning-block durations already behave.

Also adds the assistant.thread.turnDuration i18n key across all five
locale files.
2026-08-15 13:31:35 -07:00
Teknium 4b583e4476 fix(desktop): discriminate backend-confirmed turns with turnLive so the settle gate survives submit-time clock seeding
Follow-up hardening for the #74163 salvage: the no-payload settle gate used
turnStartedAt as "backend reported the turn live", but since the turn clock is
now optimistically seeded at submit (#86923), that signal is ambiguous.
Introduce ClientSessionState.turnLive, set on message.start, the running=true
session.info edge, and resume-onto-running paths; cleared by every settle.
The pre-start bail now gates on turnLive so a running=false heartbeat in the
submit gap still keeps the spinner up, while a genuinely started turn that
dies without a payload settles and unbricks the session.
2026-08-15 13:27:08 -07:00
briandevans 3e46389e40 fix(desktop): settle a turn that ends with no assistant payload
A turn that finishes without ever producing an assistant payload never
reaches message.complete, so session.info with running=false is the only
event that can release it. The busy=false branch bailed out of the state
update whenever awaitingResponse was still set and no payload had been
seen, so awaitingResponse and busy stayed latched until the app was
restarted.

That is not a cosmetic indicator. The per-session busy flag is
authoritative for isTargetSessionBusy, so submitPrompt and the slash
dispatcher silently returned false: the user typed, pressed Enter, and
nothing happened, with no error. Per-session state does not self-heal on
a session switch, so the session was effectively bricked. It reproduces
on a gateway crash mid-stream, a provider error before the first delta,
and an agent-build failure.

The bail still has a real job: submit arms busy/awaitingResponse
optimistically, so a running=false heartbeat landing in the gap before
the turn spins up is a pre-start report, not a finished turn, and
settling on it would drop the spinner and re-open the send guard
mid-flight. Gate the bail on turnStartedAt, which is stamped only once
the backend reports the turn live and cleared by every settle: null means
no turn was ever reported running, so keep waiting; non-null means the
turn started and is now reported finished, so settle.

On recovery, catch up the surfaces the missing message.complete would
have refreshed. The sidebar refresh stays unscoped so a background
session's working dot clears without the user opening it, and it fires on
the recovery edge only because the unchanged-state guard short-circuits
every later heartbeat. The transcript hydrate is scoped to the active
session so an idle background session does not cost a REST call.
2026-08-15 13:27:08 -07:00
hermes-seaeye[bot] 45af7a71fc fmt(js): npm run fix on merge (#86935)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 11:39:10 +00:00
Teknium bda83a4737 style: satisfy desktop eslint (curly braces, import order) 2026-08-15 04:33:47 -07:00
Teknium ed4f91c4eb fix(update): widen stale-lock self-heal to banner and desktop; align with compare-API status
Follow-up on the cherry-picked gitlock work (#80501 by @RGerrish, covering
the #75133 / #75168 wedge first reported and fixed by @RelaxJonh):

- Drop the PR's ancestor-check halves in banner.py, update-count.ts and
  main.ts: superseded by the compare-API status recovery that landed in
  #86257/#86331 (ahead_by == 0 already reports local-ahead as up to date).
  The salvaged update_cmd.py check path keeps main's compare-API structure
  instead of the PR's tip-SHA-plus-ancestry print.
- Keep and wire clear_stale_git_locks() at the remaining wedge sites the
  original PR targeted: hermes update apply, hermes update --check, and the
  passive banner check.
- Add the desktop counterpart (electron/gitlock.ts) so checkUpdates() heals
  the same wedge instead of reporting fetch-failed forever; mirrored
  age + git-process guards; vitest coverage.

E2E verified: real --depth 1 clone with an aged .git/shallow.lock reproduces
"Unable to create '.git/shallow.lock': File exists"; clear_stale_git_locks
removes it and the fetch succeeds; a fresh lock (in-flight fetch) is
preserved.
2026-08-15 04:33:47 -07:00
Teknium f70277bc70 fix(desktop): arm the turn progress timer at submit instead of waiting for message.start
The progress box's timer (turnStartedAt) was only seeded by the backend's
message.start event, so the submit RPC -> gateway accept -> WS round trip
(seconds under load) showed no timer at all. Seed the per-session clock in
seedOptimistic at Enter-time; message.start now keeps an existing seed
(?? Date.now()) so backend-originated turns still arm there, the active-
session mirror reuses the seeded value instead of snapping to accept-time,
and the abort/failure paths retire the seed with the turn. Adds a
console.debug submit->accept latency probe at message.start.
2026-08-15 04:22:54 -07:00
hermes-seaeye[bot] 3d5f450781 fmt(js): npm run fix on merge (#86915)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 11:02:12 +00:00
Teknium 3671529e9d feat(sdk): export route-decoupled McpTab + ToolsetConfigPanel for plugins (#86896)
* feat(sdk): export route-decoupled McpTab + ToolsetConfigPanel for plugins

Runtime plugins can only import from @hermes/plugin-sdk, but the real
Capabilities components (the full per-toolset config panel and the full MCP
tab with OAuth/API-key setup) were never exported there — so a plugin could
only reimplement bare checkbox lists. Export both, route-decoupled so they
render safely outside the Settings react-router context:

- toolset-config-panel.tsx: useOptionalNavigate() wraps useNavigate in try/catch
  (returns null with no router); the 'manage keys' deep link becomes a no-op
  when embedded outside Settings. In-Settings behavior unchanged.
- use-deep-link-highlight.ts: useOptionalSearchParams() degrades to inert params
  with no router (shared by 4 in-Settings callers incl. McpTab; identical there).
- sdk/index.ts: export { McpTab }, export { ToolsetConfigPanel }, export type
  HermesGateway, and host.getGateway() returning the live $gateway instance
  (McpTab takes a HermesGateway prop; plugins had no way to get the instance).

Both components are already profile-aware (profile?: null|string, #86548), so a
plugin can scope them to a specific bot profile. tsc: 0 errors (unchanged from
baseline). Enables Hermes-Bot-Mode to show the real Tools+MCP config in the bot
editor instead of checkbox stand-ins.

* fix(lint): sort the new SDK exports into perfectionist/sort-exports order

CI check:lint failed — the capabilities exports were grouped by comment instead
of interleaved into the file's path-sorted export list. Place them at their
natural-ascending positions: ToolsetConfigPanel (@/app/settings) after @/app/routes,
McpTab (@/app/skills) after @/app/shell/*, HermesGateway (@/hermes) after
@/contrib/types. Verified 0 adjacent-unsorted export pairs.

---------

Co-authored-by: Teknium <teknium1@users.noreply.github.com>
2026-08-15 03:55:51 -07:00
Teknium aff19d0251 feat(desktop): multi-source agents end-to-end — sockets, roster, SDK, fan-out updates
Phases 3-5 of the multi-connection campaign in one PR (per Teknium), on top
of the registry (#86679) and composite-key backend routing (#86839). Agents
from every registered connection are now usable side by side.

Renderer socket registry (phase 3):
- backendScopeKey moves to apps/shared (@hermes/shared) so main-process pool
  keys and renderer socket keys derive from ONE rule; the electron module
  keeps a byte-identical twin (tsconfig project boundaries) pinned by a
  cross-copy contract test.
- store/gateway secondaries are scope-keyed: entries carry (connectionId,
  profile); registry-scoped entries dial through getConnectionFor +
  getGatewayWsUrlFor (fresh per-connect OAuth tickets against the right
  host); events keep the bare profile plus a connectionId tag; touch/idle
  keepalive uses the scope key; pruning keeps entries whose PROFILE has live
  work. New ensureGatewayForAgent/openGatewayForAgent fall through to the
  profile path for local/null sources — single-source behavior byte-identical.

Union roster + plugin SDK (phases 3+4, the Bot Mode door):
- hermes:agents:roster enumerates every connection's /api/profiles
  concurrently (eager REST, lazy sockets; unreachable sources report per-row;
  undialed ssh boxes stay connect-on-demand) and flattens through
  buildAgentRoster — the @name-device duplicate-handle rule applied once
  across all sources, pure + tested.
- SDK: host.connections(), host.agents(), host.warmAgent(),
  host.ensureAgent() — feature-detected so plugins degrade cleanly on older
  Desktop builds.

Fan-out updates (phase 5):
- hermes:connections:update-all dispatches hermes update to every eligible
  source in parallel: local via the app's own applyUpdates pipeline,
  remote/ssh via the backend's own POST /api/hermes/update; cloud skipped as
  platform-managed (updateEligibility, pure + tested); per-connection result
  rows so one dead box can't wedge the batch. Settings → Connections gains
  the "Update all instances" button (shown with 2+ connections).

Also: getJsonForBackend/postJsonForBackend helpers with the token/OAuth-cookie
auth split; docs section updated from "staged rollout" to live behavior.

Tests: +4 pure cases (cross-copy contract, roster handles, unreachable
sources, update eligibility); FULL desktop suite 5115 passed; tsc renderer +
electron + shared clean; eslint clean.
2026-08-15 03:11:32 -07:00
hermes-seaeye[bot] 5b05e51f2e fmt(js): npm run fix on merge (#86876)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 09:33:47 +00:00
Chen Jin 0d1828294a fix(desktop): re-target find scope on a keep-alive surface flip (#81726)
The scoped find captures the foreground chat surface at bar-open time and
marks it `data-find-root`. A keep-alive tab flip — clicking another session
tab in the same stack — hides that surface with `data-pane-hidden` and
activates an unmarked one, without a pathname change, so the FindBar (which
only closes on a route change) stays open. Every subsequent query then
resolved no visible `data-find-root`, reporting 0/0 while the surface on
screen contained the text; Cmd+G / Cmd+Shift+G became no-ops.

currentFindScope now re-targets when every marked root is hidden: it
re-resolves to the foreground chat surface, clears the old surface's
highlights and marker (so they do not resurface when its tab is revisited),
stamps `data-find-root` on the new surface, and re-arms the re-render
watcher there. The normal case is unchanged — the scope stays on the
captured surface while it remains visible. Also restores the missing
trailing newline in store/find-in-page.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 02:28:07 -07:00
Chen Jin 041cbb5e48 style(desktop): add missing trailing newlines to find-in-page files (#81726)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 02:28:07 -07:00
Chen Jin c856c5f6db fix(desktop): keep find-in-page highlights consistent across React re-renders (#81726)
The scoped find walker wraps transcript text nodes in <mark> elements that
React does not own. Assistant responses stream through markdown-text.tsx,
which rebuilds the markdown DOM on every delta, and a new message is
appended whenever the assistant answers — so a re-render of a changed
region detaches the marks we inserted, dropping the user's highlights while
the bar stays open.

Watch the captured scope with a MutationObserver and re-wrap only when an
unmarked occurrence of the active query actually reappears. The observer is
gated behind a re-entrancy flag while the walker is mutating, coalesced to
one re-apply per microtask, torn down when the bar closes or the query
clears, and restores the active ordinal so a mid-stream re-render doesn't
reset the user's place to match #1. An append that adds no matching text is
a no-op; re-wrapping only fires when highlights genuinely went stale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 02:28:07 -07:00
Chen Jin 4e86caa1e2 fix(desktop): re-wrap on findNext when stale marks no longer cover all matches (#81778)
The fast path trusted that marks matching the query case-insensitively
implied the set still covered every match. A stale mark from an earlier
query (raced re-wrap, external DOM writes) can pass that per-mark
comparison while live occurrences stay unwrapped — stepping then walks
old highlights and the new matches never light up. Gate the fast path on
a read-only coverage check (same skip rules as the walker) so any
unmarked occurrence forces a re-wrap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 02:28:07 -07:00
Chen Jin 0ea7349336 fix(desktop): case-insensitive fast-path in find-in-page stepping (#81778)
Triage found the findNext fast path compared each existing `<mark>`'s
textContent byte-for-byte against the typed query. Since highlightMatches
stores the ORIGINAL-CASE source slice, the first differently-cased match
('Hermes' for 'hermes', sentence-initial capitals, ALL-CAPS) failed the
check, forcing a full re-wrap on every Enter/⌘G: fresh `<mark>` elements
lose `data-find-active`, `previousActive` resolves null, and the active
ordinal stays 1 forever (backward steps land on the last match every
time).

Compare case-insensitively. Regression test pins the differently-cased
stepping behavior: same marks survive, count stays 2, ordinal advances
to 2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 02:28:07 -07:00
Chen Jin 7ebef79c65 fix(desktop): resume sibling traversal after a fully-consumed text node match
Team review (#81778) found the walker terminated sibling traversal when a
text node was entirely consumed by a match: `current = textNode ? ... :
null` — after replaceChild detaches the original node, its `.nextSibling`
reads null, so `<div>needle<span>needle</span></div>` searching "needle"
matched only the first occurrence. Any JSX bare-text + element sibling
pattern (tool rows, attachment rows) could silently drop later matches.

Capture the text node's own `nextSibling` BEFORE the first replaceChild
and resume the outer walker from it on full consumption. The `after`
split path is unaffected (the trailing text node is re-scanned).

Regression test pins `<div>needle<span>needle</span></div><p>needle</p>`
→ 3 matches. 17/17 scope tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 02:28:07 -07:00
Chen Jin ab3b7394b1 fix(desktop): scope Ctrl+F find to the current chat view (#81726)
Pressing Cmd/Ctrl+F in Hermes Desktop searched the entire webContents,
so every keep-alive chat surface matched — a user in chat A got hits from
chat B, every background tile, and every other pane rendering a transcript.
That's not what users expect from in-page find; it's a global search, and
it deserves its own shortcut.

Replace `webContents.findInPage` (whole-document, unscoped) with a
renderer-side DOM walker that captures the active chat surface at
bar-open time and only walks text nodes inside that subtree. The Electron
bridge is retained for secondary session windows (each window still
searches its own webContents); the primary window no longer drives the
bridge.

Scope decision. "Current view" is the foreground `[data-chat-surface]`
filtered through the existing pane-visibility helper, so an inactive
keep-alive tab can't accidentally answer the lookup. The scope is
captured once when the bar opens, not re-resolved on every keystroke,
so a mid-search route change can't silently re-home the highlights — the
FindBar's route-change cleanup closes the bar first.

Walker. Walks text nodes in document order, splits nodes that span a
match boundary, wraps each match in `<mark class="find-hit">`, and tracks
the active match by a single `data-find-active` attribute. Step uses the
existing marks when the query has not changed (no DOM churn) and re-
wraps when it has. Closing the bar unwraps every mark and normalizes the
parent text, restoring the original DOM byte-for-byte.

Tests. New `find-in-page-scope.test.ts` covers the walker in isolation:
multi-match wrapping, case-insensitivity, cross-node splits, script/style
filtering, no-self-match against the search overlay, scope retarget on
surface swap, and unwrap-on-release. Rewrote the FindBar store/component
tests to assert against the real DOM (marks + counts) instead of the
bridge mock — keeping the bridge mock only for the listener-refcount
cases that still cover the secondary-window path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 02:28:07 -07:00
Jony ca8a47e276 fix(desktop): keep find bar out of its results 2026-08-15 02:20:23 -07:00
Teknium e0c73bfece Merge pull request #86847 from NousResearch/salvage/72959-titlebar-tool-count
fix(desktop): count all five static titlebar buttons; find-bar overlay guard (salvage #72959)
2026-08-15 02:14:49 -07:00
Teknium d8536709b7 fix(desktop): re-measure find-bar offset when the files pane toggles or resizes
The salvaged measurement effect only listened to window `resize`, so
opening, closing, or drag-resizing the files pane while the find bar was
up left the bar in a stale position (either still covering the pane's
header or floating mid-window after the pane closed).

Harden the measurement path:

- ResizeObserver on the aside tracks drag-resizes of the pane rail.
- A body-scoped childList MutationObserver catches the pane mounting or
  unmounting mid-find-session and retargets the ResizeObserver when the
  aside's identity changes (pane reopened, panes flipped).
- All triggers coalesce into a single rAF-batched measure; everything
  (observers, listener, pending frame) tears down when the bar closes.

Also extends find-bar.test.tsx with three positioning tests: default
right-4 when the pane is closed, parked left of an open pane, and
re-measuring when the pane opens/closes while the bar is up.
2026-08-15 02:06:37 -07:00
songoten eaef39df18 fix(desktop): position find bar clear of the files pane when open
The find bar is fixed to the top-right of the window, which overlays the
right sidebar's header and first file rows whenever the Files pane is
open. Measure the pane's live rect and park the bar just left of it;
fall back to the previous right-4 position when the pane is closed.

Closes #0
2026-08-15 02:06:37 -07:00
Teknium 279ec2f75b style(desktop): blank line before statement in open-find-bar release (lint) 2026-08-15 02:05:42 -07:00
Chen Jin f5e531ded0 fix(desktop): gate the Ctrl/Cmd+F main-process hook to Linux only (#81727)
The before-input-event handler was registered on every platform, so on macOS
and Windows — where the renderer's rebindable view.findInPage keybind already
owns Ctrl/Cmd+F — the chord became un-rebindable and would double-open when a
user remapped or cleared it. Restrict the install to process.platform ===
'linux' (the only platform #81727 affects); mac/Windows keep the renderer's
own keybind registry path.

Also corrects the docstring: the prior claim that 'GNOME Files owns Ctrl+F at
the windowing layer' is not accurate (Nautilus does not install global grabs).
The interception layer varies by distro/desktop; the fix sidesteps it by
acting at before-input-event regardless of cause, which is what actually
matters. The handler function itself stays platform-agnostic and injectable
for tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 02:05:42 -07:00
Chen Jin 75f0602301 test(desktop): cover the real macOS Cmd+F chord in the main-process handler (#81727)
IS_MAC was baked from process.platform at import time, so the unit test
titled "Cmd+F (macOS primary accelerator)" could never hit the meta (Cmd)
branch — it actually sent Ctrl and only exercised the literal-Ctrl-on-macOS
fallback. Make the platform detector an injectable default parameter and:
  - assert meta+Cmd on macOS opens the FindBar (the real Cmd path),
  - pin the dual-channel design (literal Ctrl also accepted on macOS),
  - cross-check that meta alone does NOT open on Linux/Windows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 02:05:42 -07:00
Chen Jin 2d51f6d022 style(desktop): use top-level BrowserWindow type import in find-in-page tests
`as unknown as import('electron').BrowserWindow` trips
@typescript-eslint/consistent-type-imports ("import() type annotations are
forbidden"). Use the top-level `import type { BrowserWindow }` instead,
which the lint gate accepts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 02:05:42 -07:00
Chen Jin d3193da3b1 fix(desktop): claim Ctrl/Cmd+F in main process so Pop!_OS / GNOME doesn't eat it
On Pop!_OS / GNOME-based Linux distros the GTK compositor grabs Ctrl+F at
the windowing layer (GNOME Files owns it) before the renderer's keydown
listener can fire, so the renderer's own `view.findInPage` keybind is
silently dead even though the binding is registered (#81727).

Claim the chord in the main process via `before-input-event` — that runs
strictly before the compositor shortcut can grab it. The renderer's
find-in-page pipeline still owns the FindBar UI and the actual search; we
just guarantee the press reaches it. The pre-existing `view.findInPage`
keybind stays as the renderer-side fallback for environments where the
compositor doesn't intercept.

Accept the platform's primary accelerator (Cmd on macOS, Ctrl elsewhere)
AND literal Ctrl on macOS so a non-macOS layout still works.

27/27 unit tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 02:05:42 -07:00
Teknium 883b57cbd8 feat(desktop): route backends by (connection, profile) — registry-scoped pool keys
Phase 2 of the multi-connection campaign (#86679 shipped the registry).
The Electron backend pool can now serve agents from ANY registered
connection concurrently, keyed by composite (connection, profile) scopes.

- connection-registry.ts: backendScopeKey(connectionId, profile) — the
  single home of the composite-key rule. Local/empty connection ids keep
  the BARE profile key, so every legacy pool entry, reaper log line, and
  touch call is byte-identical for single-source users; non-local
  connections get `conn:<id>::<profile>`, which cannot collide with a
  plain profile name. backendScopePrefix() matches the keys a connection
  owns (teardown on remove).
- main.ts ensureRegistryBackend(connectionId, profile): resolves a backend
  against the v2 registry. local kind delegates to ensureBackend()
  untouched; remote/cloud dial the entry's own URL/auth (descriptor carries
  profile + connectionId + sharedRemote for per-request ?profile= scoping);
  ssh bootstraps a tunnel scoped to the composite key, with the served
  dashboard token persisted back onto the REGISTRY entry (not v1
  connection.json). Pool entries reuse the existing LRU/idle-reaper/touch
  lifecycle.
- hermes:connections:remove now stops every pooled backend + ssh scope the
  removed connection owns.
- New IPC hermes:connection:for + preload getConnectionFor + renderer
  types (connectionId/sharedRemote on HermesConnection). No renderer
  behavior change yet — the multi-source roster/socket switchover is PR 3.

Tests: +2 backendScopeKey contract cases (28 total in the registry suite);
electron + settings projects 1357 passed; both tsc configs and eslint clean.
2026-08-15 02:04:30 -07:00
tyfpro d059239aa4 fix(desktop): space out find-bar buttons and anchor below titlebar 2026-08-15 02:04:10 -07:00
Teknium fc8ebff6d8 feat(mcp): unify the desktop MCP suggestion directory into the catalog
The desktop app carried its own hardcoded list of 17 vendor MCP endpoints
(apps/desktop/src/lib/mcp-directory.ts) powering the composer suggestion
pills — a second PR-reviewed vendor list, overlapping and drifting from the
Nous-approved MCP catalog (optional-mcps/).

This makes the catalog the single source of truth:

- manifest schema: optional `suggest:` block (keywords + hosts), parsed,
  validated, and normalized in mcp_catalog.py
- 15 new URL-only hosted-remote catalog entries (atlassian, sentry, datadog,
  notion, stripe, vercel, supabase, netlify, hugging_face, asana, intercom,
  airtable, webflow, paypal, square); figma + linear manifests gain suggest
  blocks
- GET /api/mcp/catalog now serves the suggest metadata
- desktop suggestion provider builds its match index from the catalog;
  the static directory remains only as a compatibility rung for older
  backends without suggest metadata
- setup card source line prefers the catalog entry's transport URL

GitHub stays out of the catalog on purpose: its hosted MCP rejects generic
DCR and the bundled github/* skills (gh CLI) are the stronger integration.
New desktop `github` suggestion provider offers the github-auth skill
instead — gated on a new cached GET /api/git/gh-auth probe so already-
authenticated users never see the pill.
2026-08-15 02:03:24 -07:00
Teknium 001e2b0b85 test(desktop): add archiveSelectedSession to keybind harness deps
KeybindRuntimeDeps grew archiveSelectedSession on main after #72959 was
opened; the salvaged keybind-gate harness needs the new field to typecheck.
2026-08-15 01:56:31 -07:00
David Metcalfe 903250772f style(desktop): satisfy perfectionist import ordering in find-bar PR files
- use-keybinds.ts: sort @/app/routes after @/app/chat/close-tab and the
  right-sidebar imports (natural-asc)
- find-bar.test.tsx: sort @/app/hooks/use-keybinds before @/components and
  @/i18n imports; KeybindRuntimeDeps before useKeybinds in named imports
2026-08-15 01:56:31 -07:00
David Metcalfe 16a1ad6ef4 fix(desktop): count the right-sidebar toggle in the controller's titlebar slot too 2026-08-15 01:56:31 -07:00
David Metcalfe d0ba405ab7 test(desktop): cover the view.findInPage overlay keybind gate 2026-08-15 01:56:31 -07:00
David Metcalfe a1b24c52f7 fix(desktop): count the right-sidebar toggle in the titlebar tools width 2026-08-15 01:56:31 -07:00
David Metcalfe 4e8e991d61 fix(desktop): find bar positioning, overlay guard, and keybind gate
Three fixes for the find-in-page bar (Ctrl+F):

1. Position: replace fixed right-4 with calc() that accounts for the
   titlebar tool cluster width, preventing visual overlap with the
   layout/haptics/keybinds/settings icons. Uses existing CSS vars from
   wiring.tsx (--titlebar-tools-right, --titlebar-tools-width).

   right-[calc(var(--titlebar-tools-right,0.75rem)+var(--titlebar-tools-width,0px)+0.5rem)]

2. Overlay guard: hide the find bar on full-screen overlay routes
   (agents, command-center, cron, profiles, settings, starmap,
   webhooks), matching the titlebar controls pattern. Prevents the
   bar from rendering behind overlays at z-50 and avoids collision
   with overlay-specific search surfaces (e.g. Settings search #69025).

3. Keybind gate: suppress the view.findInPage keybind on overlay
   routes so Ctrl+F doesn't mutate store state with no visible effect.
   Defense-in-depth alongside the component guard.

Adds regression test asserting the find bar does not render on /settings.
2026-08-15 01:56:31 -07:00
Benjamin Ang ad42ecfc06 fix(desktop): stream remote media without renderer credentials 2026-08-15 01:53:34 -07:00
Teknium 5ed4506f42 fix(desktop): FindBar no longer overlaps native window controls
The ⌘F/Ctrl+F find bar positions itself at
top-[calc(var(--titlebar-height,0px)+0.5rem)], but it mounts at the
overlay root in ContribWiring, outside any subtree that defines
--titlebar-height. The 0px fallback parked the bar inside the 34px
titlebar strip, underneath the native min/max/close window-controls
overlay on Windows/Linux (two X buttons side by side, close button
half-covered).

Fix: use the real titlebar height (34px) as the fallback, matching the
established pattern in floating-hud.ts and notifications.tsx. The bar
now floats just below the titlebar band, clear of the window controls.
2026-08-15 01:46:51 -07:00
hermes-seaeye[bot] f0c222c73d fmt(js): npm run fix on merge (#86820)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 08:27:29 +00:00
Teknium eb4f72f914 test: satisfy new unread props at remaining sibling fixture (session-row Tip test) 2026-08-15 01:21:40 -07:00
Teknium 7f921871e7 test: pass onToggleUnread in wheel-overscroll VirtualSessionList fixture (cross-PR prop addition) 2026-08-15 01:21:40 -07:00
Teknium 0ceb739abc style: eslint --fix on salvaged unread-dot files 2026-08-15 01:21:40 -07:00