590 Commits
Author SHA1 Message Date
Maciej Lisiewski 29e78b124e docs: pnpm commands for the JS workspaces
READMEs, contributor guides, AGENTS.md files, skills and the website (with
the zh-Hans translations) give pnpm commands for the workspaces; the
website itself is still an npm project.
2026-11-06 04:08:45 +01:00
Maciej Lisiewski 7062229090 chore(js): switch the workspaces from npm to pnpm
pnpm 12.15.0 (native executable) installs every JS workspace from one
pnpm-lock.yaml, imported from package-lock.json with the same resolved
versions. website/, the WhatsApp bridge and the photon sidecar stay separate
npm projects.

Configuration moves from .npmrc and the root manifest into
pnpm-workspace.yaml: the 14-day minimumReleaseAge and its excludes,
overrides, allowBuilds (the old allowScripts set, with strictDepBuilds
failing any unlisted build script), engineStrict, sideEffectsCache: false
(in-place rebuilds must not write into the store) and
verifyDepsBeforeRun: false (a `pnpm run` never installs).

The pnpm version is pinned in pm/lock.json and in root devEngines.
pmOnFail: ignore keeps that pin out of pnpm-lock.yaml: a leading pin
document makes osv-scanner read zero packages. scripts/build/node-deps.mjs
enforces the exact version instead, and every lock reader refuses a
multi-document lock. A root preinstall refuses `npm install`; devEngines
onFail is warn so npx (MCP servers, the tsc linter) keeps working inside
the checkout.

User-machine installs run --frozen-lockfile --trust-lockfile (release age is
checked when locking, in CI, not per user). node-deps removes npm-era
node_modules trees before the first pnpm install. Nix builds the store with
fetchPnpmDeps and a hash file that CI checks.
2026-11-06 04:08:24 +01:00
Maciej Lisiewski 1975266038 fix(web): build the compiler preset without mutating it, from the config dir
Two changes that npm's hoisting hid and pnpm's layout exposes:

- reactCompilerPreset()'s rolldown.filter is optional once @vitejs/plugin-react's
  optional type peers resolve (TS18048 on the in-place assignment). Spread the
  preset instead of mutating it.
- Babel resolves preset plugin names from process.cwd(). Builders that run Vite
  from the checkout root (installer, Docker, Nix) only found
  babel-plugin-react-compiler because npm hoisted it there. Pass the config
  dir as babel's cwd.
2026-11-05 14:08:26 -08:00
Maciej Lisiewski 7ee14aa4ff fix(web): scan the design-system bundle where npm installs it
The @source pointed at web/node_modules/@nous-research/ui/dist, but npm hoists the package to the
root node_modules, so Tailwind silently skipped it and the dashboard lost the utilities only the
bundle uses (--font-sans, .!visible). Tailwind cannot resolve packages, so both locations are
listed; the missing one is skipped.
2026-11-05 14:08:26 -08:00
Teknium 2e933c501e feat(dashboard): Update all and Auto-update on the Plugins page
POST /api/dashboard/agent-plugins/update-all runs the same unattended
update-all as Desktop and /plugins update (nothing accepted on the
user's behalf; widened pins come back needs_review), and
GET/PUT /api/dashboard/plugins/auto-update read and set the profile's
plugins.auto_apply. Both run in the dashboard profile scope off the
event loop. The Plugins page gets the button and the switch; copy in
every dashboard locale. Plugin catalog types move to api-plugins.ts to
keep api.ts under its size cap.
2026-11-04 21:58:12 -08:00
kshitijk4poor a53912d288 test(aux): pin the skills_hub removal by behaviour, not source text
- Drop the assertions that read web/desktop TS sources (tests/AGENTS.md bans
  source-reading tests); the frontends get their own auxTaskRows tests.
- The leftover-block test now proves the block is inert: it survives
  load_config(), the "reset to main" stale-pin nudge ignores it, a direct
  assignment is rejected with 400, and assign-all leaves it untouched.
- config_defaults: rewrap the merged note so the removal and the Compression
  notes sit on separate lines (net -1 line under the file cap).
- integrations/providers: OpenCode session-header list no longer names
  "skills-hub lookups" as an auxiliary call.

Refs #40703
2026-11-05 02:36:29 +05:30
Muhammad AimanandClaude Opus 5.5 8b1e49558b chore(config): remove stale auxiliary.skills_hub LLM slot (#40703)
Skills Hub search/install is deterministic; nothing calls
call_llm(task="skills_hub"). Removes the slot from DEFAULT_CONFIG, the CLI
picker, the dashboard and Desktop task lists (+ their locale strings) and
the docs, as requested in the review of #40710.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 02:36:29 +05:30
Teknium 065ec46234 fix(relay-link): re-review folds: list survives a failed managed check, fresh snapshot before minting, Portal URL from the login
- The account list no longer disappears when only the access-list question
  fails (managed_by 'unknown'); minting still re-asks and refuses.
- Linking snapshots the linked accounts right before minting, so a failed
  earlier load cannot make every existing account read as newly linked.
- The Portal button follows the instance's Portal (operator override, stored
  login, default) through the scoped resolver instead of raw os.environ.
- Managed copy no longer asks the user to link here.
- Tests: the stand-in connector checks the bearer on the access route and
  answers 404 like a connector without it; the managed test also rejects the
  'Link another' button.
2026-11-03 09:45:07 -08:00
Teknium f61682590b fix(relay-link): Portal-managed agents link from the Portal; only a new account reads as linked
- On a Portal-managed agent (connector /manage/access/list answers
  managedBy: portal) a code minted with the gateway bearer carries no owner, so
  its redeem binds no account (gateway-gateway scopeService: a managed agent
  takes only the Portal setting). The gateway now refuses to mint there
  (managed_by_portal) and the Channels card on both surfaces lists the linked
  accounts read-only with a 'Link accounts in Nous Portal' button.
- Success is a new platformUserId in the refreshed list, never an 'inactive'
  code before expiry: 'inactive' also means used by an already-linked account
  or unknown. That case now shows a neutral note; expiry still says expired.
- Desktop keys the link card by connection + profile + platform, so a code,
  poll or account list never carries over to another backend.
2026-11-03 09:45:07 -08:00
Teknium 77d2c9cea7 fix(relay-link): the linked toast fires; reload accounts before closing the code
Closing the code (setLink(null)) re-ran the poll effect and set its cancelled
flag before the account reload returned, so a successful link never showed the
confirmation.
2026-11-03 09:45:07 -08:00
Teknium ca221796e6 fix(relay-link): review fixes: per-platform panel state, re-link reads as linked, bounded poll, named connector refusals
- Desktop: key the link panel by platform so a pending code never follows the
  user to another platform's page.
- A code that goes inactive before it expires was redeemed: a re-link from an
  already-linked account no longer reads as "expired".
- Polling stops after 5 consecutive errors or at expiry and shows the error.
- Connector 401/403/409 map to named, plain-language errors; 5xx/408/429 are
  retryable (503); unlink answers {unlinked: true}; a silent gateway is worded
  for stopped, too-old and timed-out alike.
2026-11-03 09:45:07 -08:00
Teknium bb7dfed848 fix(dashboard): plain Unlink button; the broken-chain icon read as garbled text 2026-11-03 09:45:07 -08:00
Teknium 179c90f547 fix(cloud): Telegram on Team Gateway is set up by linking an account to the shared bot
On a Hermes Cloud instance with Team Gateway, every direct messaging adapter is
switched off at gateway start (config_env._relay) and the connector's shared bot
is the only way Telegram, Discord and Slack reach the agent. The Channels page
and the Desktop messaging page still offered "Create with QR" and bot-token
setup, which make a bot the relay never runs: users finished setup, restarted,
and the bot never answered (#141546, #138101).

The instance now mints the connector's one-time `/link <code>` itself:

- gateway/relay/link.py calls /manage/link, /manage/link/state,
  /manage/link-status and /manage/unlink with the per-gateway bearer, and
  builds the shared bot's DM link from GATEWAY_RELAY_BOT_IDS.
- The per-gateway secret lives only in the gateway process, so the gateway
  answers a new `relay-link` control-socket verb and the dashboard routes
  (/api/messaging/relay*) go through it.
- /api/messaging/platforms reports relay_fronted / relay_exclusive per platform.
- Dashboard Channels and Desktop Messaging show a Team Gateway card (link
  button, QR of the bot, the exact message to send, linked accounts with
  Unlink) and hide the direct setup that cannot work under an exclusive relay.
2026-11-03 09:45:07 -08:00
Krasimir Kralev 955381dd38 fix(dashboard): say when Team Gateway relay turned a channel off 2026-11-03 09:45:07 -08:00
unsupportedpastels 3d9a230d57 fix(dashboard): forward the Vite dev page's socket Origin unchanged
The /api proxy rewrote a WebSocket upgrade's Origin to HERMES_DASHBOARD_URL.
Pointed at a port forward (http://127.0.0.1:8000 forwarded to the backend on
9119), that Origin names neither the backend's bound port nor the Host the
proxy keeps (localhost:5173), so the backend refused the dev page's sockets.

The proxy sets no changeOrigin, so Host stays the page's own authority and
the browser's Origin already names it, which the backend admits as
same-origin. Drop the rewrite and its helper, pin the proxy options, and
pin both header pairs in the backend test.
2026-11-02 13:56:48 -05:00
FroRaut cb837e10a5 fix(dashboard): stop trusting fixed dev-server ports for CORS and WebSockets
_TRUSTED_LOCAL_ORIGIN_PORTS (9119, 5173, 5174, 4174) was trusted on every
backend, including packaged Desktop backends on an ephemeral port. A Vite app
on 5173, or on 5174 when 5173 is taken, could still read the session token
from / and open /api/ws. _is_trusted_local_origin() now accepts only the bound
port and the desktop dev renderer this backend was spawned for
(HERMES_DESKTOP_DEV_SERVER). `hermes dashboard` keeps working on 9119 because
that is its bound port.

The dashboard's Vite dev server was the only client that relied on a fixed
port: the browser's Origin on proxied /api upgrades is Vite's. The /api proxy
now presents upgrades from Vite's own page (Origin host equals Host) with the
backend's origin, and passes every other Origin through for the backend to
refuse, so it does not launder foreign pages the way Vite's rewriteWsOrigin
would. Vite's default CORS grants every localhost port and would hand another
local page the token hermesDevToken() injects into index.html, so it is off.

(cherry picked from commit 66de028a3ce39ec49a32205855dc269d30bf3942)
2026-11-02 13:56:48 -05:00
Gille b26b6c2769 fix(web): isolate PostCSS discovery from ancestor config 2026-10-28 15:15:13 -07:00
Teknium 6da3e5641e fix(web): reload for a failed chunk only into a newer build, once per build
Review on #145357 (@unsupportedpastels):

M1: a failed chunk request reloaded even when nothing was rebuilt (a
network abort, offline). Before reloading, fetch the page's index.html
with cache: no-store and reload only when it names a different built
entry than the running one; skip while offline and in dev. Otherwise
the import rejects as before.

M2: the single tab-wide 5-minute timestamp suppressed recovery for a
later build (A -> B -> C inside the window). The session guard now
records the entry the tab reloaded away from, so a repeat on the same
build (or a reload that lands on it again) is suppressed while a newer
build can still recover.
2026-10-27 20:32:39 -07:00
Teknium 010611bc5e test(web): drive chunk-load recovery through a real vite:preloadError event
The tests called reloadForChunkLoadError directly, so a broken listener in
startChunkLoadRecovery would stay green. startChunkLoadRecovery now takes the
storage and reload seams, and the tests dispatch the event itself: one reload
then suppression, chunk CSS / Safari / Firefox messages, and throwing storage.
Renaming the listened event turns 4 of the new cases red.
2026-10-27 20:32:39 -07:00
Teknium 640b2c21d3 Port from paperclipai/paperclip#17349: reload once when a lazy chunk fails to load
A web UI tab (Webapp / gateway web UI / dashboard) opened before
`hermes update` keeps referencing the old hashed page chunks. The update
rebuilds web_dist with emptyOutDir, so the first page the stale tab opens
404s on its chunk; there is no error boundary, so React unmounts the whole
app and the tab goes blank until the user reloads by hand.

Listen for vite:preloadError (fired for every failed lazy import and chunk
CSS preload) and reload once; index.html is served no-store, so the reload
picks up the new build. A sessionStorage timestamp limits it to one reload
per 5 minutes per tab, and only build chunks under /assets/ qualify, so a
broken plugin bundle never triggers it.
2026-10-27 20:32:39 -07:00
Teknium 428809549a fix(memory): never call an unsaved provider active; refresh readiness after setup
Review on #145574:
- The Settings summary labelled the unsaved memory.provider draft "Active
  memory provider" even when GET /api/memory named another one. The draft
  now shows as "Selected memory provider" (not active yet, naming what
  sessions keep using), and the active row and recall copy come only from
  status.active. A config autosave that touches `memory` refetches.
- Readiness refreshes after an inline credential save, a full-config modal
  save and OAuth completion ($memoryStatusRevision); a failed status read
  shows "Memory status unavailable" with Retry instead of dropping the badge.
- The built-in row reflects memory.memory_enabled / user_profile_enabled in
  Settings ("On by default" where the config isn't loaded), and recall is
  claimed only for a ready provider. Web dashboard copy drops "always on".
- isExternalMemoryProvider matches the backend's full sentinel set,
  including `default`.

Refs #144648 (WA-22)
Refs #145175
2026-10-27 20:32:14 -07:00
Teknium f70610a4a0 refactor(memory): keep the Memory cards out of the page functions' complexity
The dashboard Memory card moves into its own MemorySection and the
Settings slot decides its own visibility, so SystemPage and
ConfigSettingsInner end no larger than on main (code-health ratchet).

Refs #144648 (WA-22)
2026-10-27 20:32:14 -07:00
Teknium 7a0908c3b1 fix(memory): show the active provider and the always-on built-in layer on every Memory page
An external memory provider is additive: MEMORY.md / USER.md stay loaded
in every session. The Memory surfaces did not say so, and Desktop/Webapp
Settings > Memory & Context showed only the provider select plus the
built-in toggles, so a working provider read as "not in effect".

- Desktop + Webapp: new MemoryLayersSummary (Settings > Memory & Context >
  Persistent memory, and Command Center > Maintenance) names the active
  provider with its readiness from GET /api/memory (or "None - built-in
  memory only") and labels MEMORY.md / USER.md as the built-in layer,
  "Always on". On Settings it follows the live memory.provider draft.
- Dashboard System page: "Active memory provider" and a "Built-in layer -
  always on" label with the additive note, through web i18n.
- MemoryStatusResponse now types the per-provider status the server
  already returns; getMemoryStatus takes the Settings scope profile.

No endpoint change: GET /api/memory already returns active + status.

Refs #144648 (WA-22)
Refs #145546
Fixes #145175
2026-10-27 20:32:14 -07:00
Austin Pickett 681346296c fix(web): write every page header slot from a layout effect
A passive-effect cleanup on the outgoing page runs after the incoming page's
layout effects, so leaving Sessions (the landing route) or Files still erased
the next page's header action, e.g. Channels' Restart gateway. Move the
remaining passive header writers (Sessions, Files, Plugins and Chat's title)
to useLayoutEffect, document the rule on usePageHeader, and cover leaving
Files and Sessions.
2026-10-27 14:27:04 -04:00
GilleandWompaJango e8b6c9b6ad fix(web): preserve page header actions across route commits
Reset per-page header slots during the route render, before child layout
effects populate them. The parent layout effect previously erased the
Channels restart action on direct load and replacement slots on navigation.

Adapt the provider-reset portion of #45652. The separate hidden ChatPage
writer was already addressed by #72234. Add real Channels and route lifecycle
coverage, including delayed mounting and clearing every previous slot.

Co-authored-by: WompaJango <WompaJango@protonmail.com>
2026-10-27 14:27:04 -04:00
Austin Pickettandunsupportedpastels 82d7e2dba7 fix(skills): match a ClawHub install by source, not through a bare slug alias
The installed map listed each ClawHub lock row under its bare slug as well
as clawhub/<slug>. A bare id carries no source, so with @alice/foo
installed, a `foo` from any other source would also read installed.

HubLockFile.by_identifier now keys a ClawHub row by its lock id and the
source-qualified clawhub/<slug> card id only. Desktop cards install that
id, so they need no change. The dashboard's hub search and featured list
carry a ClawHub skill's bare slug, so web/src/lib/skill-hub-installed.ts
looks a hit up under its own id, then under the id a catalog card would
install for it (skillCatalogInstallIdentifier), which differs only for
ClawHub.

Co-authored-by: unsupportedpastels <theoldwizard123@pm.me>
2026-10-27 10:56:48 -04:00
Maciej Lisiewski 05b83f632f Merge pull request #143719 from NousResearch/bulk-update-js
chore(deps): bump npm dependencies to latest minor/patch releases
2026-10-25 12:54:29 +01:00
Bear Huddlestonandemozilla b5eb5ee7a8 feat(webapp): carry the browser-hosted Desktop's server pieces from #93508
Carried from BearHuddleston's #93508 onto main. The Webapp relay and the
profile-incarnation fence stay out: carried code follows main's
named-profile rule and refuses a missing or deleted profile home.

- Webapp routes: one-use launch tickets, window handoff, and gateway
  WebSocket tickets minted for the signed-in browser
- Browser surface policy and the SPA document policy
- A host terminal for the browser that survives reconnects
- Staged browser uploads and file tickets; file.attach takes a staged upload
- hermes sessions adopt
- Webapp bundle build helpers and the renderer freshness check
- Regenerated gateway contracts

Co-authored-by: emozilla <emozilla@nousresearch.com>
2026-10-25 03:22:14 -07:00
Maciej Lisiewski eb57df3823 chore(deps): keep three at 0.180.0
@nous-research/ui declares peer three ^0.180.0 (>=0.180.0 <0.181.0), so the 0.186.1 pin in web
left a second three: 0.180.0 hoisted for @nous-research/ui and 0.186.1 under web/node_modules,
both bundled into the dashboard. Back to the version the UI kit accepts until it widens its peer.
2026-10-23 23:16:06 +02:00
Maciej Lisiewski 41298d65a8 chore(deps): bump vite to 8.3.4
vite: 8.2.4 -> 8.3.4 (pins edited: 3; also: {})
2026-10-23 22:00:12 +02:00
Maciej Lisiewski 458b9dbafd chore(deps): bump react and react-dom to 19.3.0
react+react-dom: 19.2.7 -> 19.3.0 (pins edited: 9; also: {})
2026-10-23 22:00:12 +02:00
Maciej Lisiewski 1201def24e chore(deps): bump three to 0.186.1
three: 0.180.0 -> 0.186.1 (pins edited: 1; also: {})
2026-10-23 22:00:12 +02:00
Maciej Lisiewski 5b90561bfc chore(deps): bump tailwind-merge to 3.7.0
tailwind-merge: 3.6.0 -> 3.7.0 (pins edited: 3; also: {})
2026-10-23 22:00:12 +02:00
Maciej Lisiewski 5107c494c5 chore(deps): bump react-router to 8.4.0
react-router: 8.3.0 -> 8.4.0 (pins edited: 2; also: {})
2026-10-23 22:00:12 +02:00
Maciej Lisiewski 0433c0934e chore(deps): bump motion to 12.43.0
motion: 12.42.2 -> 12.43.0 (pins edited: 2; also: {'framer-motion': "['12.42.2']->['12.43.0']"})
2026-10-23 22:00:12 +02:00
Maciej Lisiewski 73ff19fe1d chore(deps): bump eslint-plugin-react-refresh to 0.5.7
eslint-plugin-react-refresh: 0.5.3 -> 0.5.7 (pins edited: 1; also: {})
2026-10-23 22:00:12 +02:00
Maciej Lisiewski 19374ee0c9 chore(deps): bump @types/react-dom to 19.3.0
@types/react-dom: 19.2.3 -> 19.3.0 (pins edited: 3; also: {})
2026-10-23 22:00:12 +02:00
Maciej Lisiewski 67407d6cf5 chore(deps): bump @types/react to 19.3.0
@types/react: 19.2.17 -> 19.3.0 (pins edited: 4; also: {})
2026-10-23 22:00:12 +02:00
Maciej Lisiewski e52e39ee20 chore(deps): bump @types/node to 22.20.5
@types/node: 22.20.1 -> 22.20.5 (pins edited: 3; also: {})
2026-10-23 22:00:12 +02:00
Maciej Lisiewski 26745fc118 chore(deps): bump @rolldown/plugin-babel to 0.2.4
@rolldown/plugin-babel: 0.2.3 -> 0.2.4 (pins edited: 2; also: {'picomatch': "['4.0.5']->['4.0.7']"})
2026-10-23 22:00:12 +02:00
Maciej Lisiewski 5316aaea2d chore(deps): bump @react-three/fiber to 9.8.1
@react-three/fiber: 9.6.1 -> 9.8.1 (pins edited: 1; also: {'its-fine': "['2.0.0']->['2.1.1']"})
2026-10-23 22:00:12 +02:00
Maciej Lisiewski 4980098a78 chore(deps): bump @nous-research/ui to 0.19.1
@nous-research/ui: 0.18.2 -> 0.19.1 (pins edited: 3; also: {})
2026-10-23 22:00:12 +02:00
Maciej Lisiewski c9987be6b6 chore(deps): bump @babel/core to 8.0.7
@babel/core: 8.0.1 -> 8.0.7 (pins edited: 2; also: {'@babel/code-frame': "['8.0.0', '8.0.0']->['8.0.6', '8.0.6']", '@babel/compat-data': "['8.0.0', '8.0.0']->['8.0.5', '8.0.5']", '@babel/generator': "['8.0.0', '8.0.0']->['8.0.6', '8.0.6']", '@babel/helper-compilation-targets': "['8.0.0', '8.0.0']->['8.0.7', '8.0.7']", '@babel/helper-globals': "['8.0.0', '8.0.0']->['8.0.6', '8.0.6']", '@babel/helper-string-parser': "['8.0.0', '8.0.0']->['8.0.6', '8.0.6']", '@babel/helper-validator-identifier': "['8.0.4', '8.0.4']->['8.0.6', '8.0.6']", '@babel/helpers': "['8.0.0', '8.0.0']->['8.0.7', '8.0.7']", '@babel/parser': "['8.0.4', '8.0.4']->['8.0.7', '8.0.7']", '@babel/traverse': "['8.0.4', '8.0.4']->['8.0.7', '8.0.7']", '@babel/types': "['8.0.4', '8.0.4']->['8.0.6', '8.0.6']", '@jridgewell/sourcemap-codec': "['1.5.5']->['1.6.0']"})
2026-10-23 22:00:11 +02:00
Teknium 32fc5b878c Merge origin/main into window-plans
Resolves hermes_cli/auth_model_picker.py against 7a8569baa9 (the picker
takes a title, public as prompt_model_selection): keeps this branch's
description_lines()/menu_title() split, with menu_title(title=...) and the
title= parameter on _prompt_model_selection carried over from main.
2026-10-22 14:43:44 -07:00
Maciej Lisiewski c82bdd01ae chore(deps): bump vitest to 4.1.12
Latest 4.1.x patch: mocker hardening for manual and redirect mocks
(vitest-dev/vitest#11580 backport to v4). Also bumps @vitest/mocker.

vitest 4.1.12 is 7 days old, below the min-release-age=14d guard, so
.npmrc gains a dated exclude entry (remove when > 2wks old).
2026-10-22 21:20:10 +02:00
rob-maron 6adca16a77 Merge branch 'NousResearch:main' into window-plans 2026-10-21 23:07:58 -04:00
Maciej Lisiewski ba9534c157 fix(deps): bump vite 8.2.0 → 8.2.4 (high, GHSA-587h-7wmj-h5pm)
vite 8.2.4 bundles launch-editor fixes for GHSA-587h-7wmj-h5pm,
GHSA-r825-w8mm-5pxw and GHSA-698v-4r3x-3vcv. Bumped in all three
workspaces that pin it (web, apps/desktop, apps/bootstrap-installer),
leaving one hoisted copy.

Forced by vite 8.2.4's ranges:
- rolldown ~1.2.4: 1.2.1 → 1.2.13 (@oxc-project/types =0.153.0)
- postcss ^8.5.26: root override 8.5.23 → 8.5.29; website lock moved
  to the same 8.5.29

The .npmrc age-gate excludes for vite/rolldown/@rolldown/*/@oxc-project
were marked "remove once vite 8.2.0 is > 2wks old" (it is 82 days) and
let a same-day rolldown 1.2.15 into the lock, so they are removed; every
version above is past the 14-day gate. The website postcss exclude
("remove when 8.5.23 is > 2wks old") is removed for the same reason.
2026-10-21 21:14:06 +02:00
Gille 13282fdac4 fix(dashboard): allow safe WhatsApp re-pairing after logout (#142439)
* fix(dashboard): allow safe WhatsApp re-pairing after logout

* fix(whatsapp): retain pairing reservations through credential flush
2026-10-21 10:08:33 -04:00
emozilla d7c48548a9 feat(shared-metrics): offer two consent answers, drop collect-only
The first-run offer on Desktop, the dashboard and the terminal had a
third answer ("Keep on this computer" / "Collect locally only" / "Local
only") that collected stats nobody reads: no surface shows them to the
user, and data from before an opt-in is never sent. Every surface now
offers "Share with Nous" or "No thanks", and each answer sets `enabled`
and `send` together. Desktop Settings > Privacy and `hermes setup
telemetry` become one on/off choice to match.

Profiles that already chose collect-only keep that answer and are not
asked again: they saw the question and declined to send. Nothing is sent
for them; the Desktop switch reads off and the `hermes tools` row says
"collecting locally".

Copy that described collection as a separate local choice is rewritten
in every locale that carries it (de, es, fr, ja, ru, zh, zh-hant, ar).
2026-10-21 00:32:21 -04:00
rob-maron 0e29e8a25a fix tests 2026-10-20 22:18:49 -04:00