Commit Graph

45761 Commits

Author SHA1 Message Date
teknium1
fcd830ee1c feat: Desktop first-run consent answer records setup_completed 2026-09-28 12:43:03 -07:00
teknium1
520f0e9ed5 feat(desktop): first-run shared-metrics consent and Settings › Privacy toggles 2026-09-28 12:43:03 -07:00
teknium1
b685eb90c0 feat: shared_metrics.status/set RPCs for Desktop consent
Desktop had no way to read or change the shared-metrics opt-ins. The new
profile-scoped RPCs read/write telemetry.shared_metrics.{enabled,send} in the
focused profile's config.yaml through the one config writer, keep the setup
wizard's invariant (send is forced off unless collection is on) and reconcile
the consent windows on every change via the wizard's own helper. first_run
answers also record the desktop setup-completed metric (lazy import until the
events module lands).
2026-09-28 12:43:03 -07:00
teknium1
3d62ae2233 feat(telemetry): decision-data docs, consent copy, smoke coverage and install-age for new installs
- Docs: a Decision-data metrics section mapping each metric to the product question it answers;
  skill-load and snapshot paragraphs describe the new public-name and install-age fields.
- setup consent copy names the new data classes (session length, token totals, command and
  catalog names, bucketed setup counts).
- The real-CLI smoke now asserts the session summary, token sums, TTFT bucket and milestones in
  both the SQLite store and the exported, schema-validated package.
- A profile with no sessions yet is a brand-new install (lt_1h), not unknown: the first task's
  milestone and the first snapshot fire before state.db has a session row.
- Two invariant tests: session summaries + once-per-install milestones; token sums per model and
  auxiliary task with private task names collapsed.
2026-09-28 12:43:03 -07:00
teknium1
26e7a9ac35 feat(telemetry): decision-data metrics core: sessions, milestones, tokens, TTFT, snapshot fields, event API
Adds the contract, store and runtime side of the decision-data extension to shared metrics v3:
per-session summaries, once-per-install milestones, token sums per model/provider (primary and
auxiliary by task), time-to-first-token on model routes, bundled skill names on skill loads, and
install age / main provider / terminal backend / messaging platform count / language on the
daily snapshot. Process-level facts (setup, slash commands, compression, model switches,
fallbacks, extension installs) get a call-site API in shared_metrics_events. Every new
dimension is a closed enum, a bucket or a public catalog name.
2026-09-28 12:43:03 -07:00
teknium1
ef35c94cfa test: auxiliary relay route asserts the v3 model-route dimensions 2026-09-28 12:43:03 -07:00
teknium1
b8732b768e feat(telemetry): shared metrics v3 with failure classes, tool usage, platform and daily snapshot
The fleet rollups could say that tasks and tools failed, but not why, on which
model, on which messaging platform, or which features installs actually use.

- model_route rows gain call_role, outcome and error_class (the classifier's
  FailoverReason for the last failed attempt; a success row keeps the error it
  recovered from).
- task_run rows gain platform (built-in gateway platform, plugin, or none);
  task_run.finished gains failure_class from a closed set.
- new hermes.tool.usage.count: tool_name (only names from the static
  toolsets.TOOLSETS snapshot; mcp/plugin otherwise), outcome, error_class.
- new hermes.install.snapshot, latched to once per rolling 24h like
  client.active: memory provider plus bucketed counts of MCP servers,
  plugins, skills, cron jobs and profiles; no names.
- package schema v3; v2-shaped pending counters still validate and drain.
- consent copy and developer docs list the new categories; the relay smoke
  asserts v3 shapes and disables title generation (its third model request
  broke the smoke's request-count check on main).
2026-09-28 12:43:03 -07:00
Gille
8127b377a2 fix(desktop): satisfy lint for refresh regression 2026-09-28 14:42:44 -05:00
Gille
37942a69ce fix(desktop): retain completed tool cards across transcript refresh 2026-09-28 14:42:44 -05:00
Teknium
7c799a6565 feat(vercel): fresh sandboxes use a managed image (universal:latest); runtime presets deprecated, migration 49 (#126741)
* feat(vercel): start fresh sandboxes from a managed image instead of the deprecated runtime

Vercel deprecated Sandbox runtimes (node24/node22/python3.13) in Aug 2026 in favour of
images, and rejects runtime+image together and runtime with a snapshot source. New
terminal.vercel_image (default vercel/sandbox/universal:latest, Node 24 + Python 3.14)
picks the image for fresh sandboxes; a pinned terminal.vercel_runtime still works, wins
over the image and logs a deprecation warning; snapshot restores send neither.

Setup wizard prompts for the image, dashboard exposes both keys, status/config show the
effective choice, TERMINAL_VERCEL_IMAGE bridges config to the tool like its siblings.

* feat(config): migration 49 drops the seeded node24 Vercel runtime pin

Every pre-49 config.yaml carries terminal.vercel_runtime: node24 (the template default) and
the setup wizard mirrored it into .env as TERMINAL_VERCEL_RUNTIME. Both are the default
copied, not a choice, so the migration drops them and fresh sandboxes follow vercel_image;
node22 / python3.13 pins are the user's and survive. Persisted sandboxes are unaffected:
a snapshot restore never sends a runtime or an image.
2026-09-28 12:33:01 -07:00
Teknium
3f871425af fix(modal): persistent sandbox snapshots no longer expire after 30 days (modal 1.5.5, ttl=None) (#126740)
* fix(modal): keep persistent-sandbox snapshots past the SDK's 30-day TTL

modal>=1.5 gives Sandbox.snapshot_filesystem() a default ttl of 30 days, so an idle
persistent Modal sandbox silently lost its filesystem and restarted from the base image.
Pass ttl=None (retain until deleted) and bump the modal extra from 1.3.4 (no ttl
parameter; legacy RPC) to 1.5.5 so the kwarg exists on every install.

* fix(modal): drop the dead modal.Mount credential-mount block

modal.Mount left the public API in modal 1.0, so _modal.Mount.from_local_file raised
AttributeError into the surrounding except on every sandbox start and the block never
mounted anything. The FileSyncManager created right after already uploads the same
credential, skills and cache files (iter_sync_files), so delete the duplicate; the test
fake stops exporting a Mount the real SDK does not have.

* chore: retrigger CI (zero-job dispatch failure, auto-heal)
2026-09-28 12:31:03 -07:00
Brooklyn Nicholson
2de175e8a7 feat(desktop): put model-picker pricing behind a setting, off by default
Prices on every row are noise until you're comparing cost. Settings →
Appearance → General → Model Pricing turns them on; the choice persists.
2026-09-28 14:27:59 -05:00
Brooklyn Nicholson
a9d14d0ac1 feat(desktop): surface cached-read rates in the model catalog menu
Issue #63125 asks for the cached vs uncached read rate at a glance in
the model picker. The backend already ships it: _apply_pricing fills
ModelPricing.cache from input_cache_read, and the CLI picker renders it
as a third column — the catalog menu was the only surface dropping it.

- ModelPrice appends the cached rate (dim, `·$0.16`) next to the
  uncached input/output pair; its hover title names it, and the row's
  tooltip gains the cached-read line. Uncached read stays the inline
  `input` rate, so the cached-vs-uncached comparison reads at a glance.
- The hardcoded "free" label and English-only tooltip move to
  shell.modelMenu copy (free / cacheRead / priceTitle) so the row
  localizes; fr/de/es are completeness-checked, so all locales that
  carry the modelMenu block get the new keys.
- A collapsed `-fast` family now falls back to the fast sibling's price
  when the base id is unpriced (the sharing the comment already
  claimed — review point on #84831).

Refs #63125
2026-09-28 14:27:59 -05:00
Brooklyn Nicholson
81e28566c5 fix(desktop): harden catalog ModelPrice against partial pricing payloads
Review follow-ups on #84831:

- Treat null input/output as unknown: render nothing rather than
  "null/null" when a provider reports a non-free model without rates
  (_apply_pricing ships "" for unknown, but the wire type is
  string | null, so guard both).
- Only show the sale tag for a positive discount_percent, so a negative
  value can't render as a double-minus.
- Fallback title uses "—" for a missing half instead of "undefined".
- New test: no price span at all when the provider carries no pricing
  for the model.
2026-09-28 14:27:59 -05:00
Thiago Chu
67bc567657 feat(desktop): render per-model pricing in the model catalog menu
The model.options payload has carried per-model ModelPricing for providers
that support live pricing (Nous Portal included) since the picker requested
it, but the catalog menu never displayed it. Add a compact price label to
each model row: input/output $/Mtok, a green "free" label for free-tier
models, and a sale tag when the portal reports a discounted list price.

Verified against a live payload probe: the nous provider ships 35 pricing
entries (e.g. anthropic/claude-sonnet-5 -> $1.60/$8.00, 20% discount).
2026-09-28 14:27:59 -05:00
teknium1
b6fe961783 test(logging): rollover chown-back asserts the previous file's owner, not the process gid (macOS tmp dirs are gid 0) 2026-09-28 12:23:25 -07:00
teknium1
a2cdaa7388 fix(review): run TTS warm/release command hooks under the caller's secret scope (#120481)
The warm_command/release_command hook thread was a bare threading.Thread, so under
the multiplexer resolve_passthrough_value() saw no secret scope, raised
UnscopedSecretError, and the best-effort hook swallowed it at debug level: every
command provider with a non-empty env_passthrough silently never warmed or released.
Bind the thread with ctx_bound (copy_context().run), the same seam the keep-warm
timer already uses. Regression test: the hook thread resolves the bound profile's
passthrough value.

Docs (review minor): the multiplex isolation table now names per-profile slash-command
gating and the fail-closed empty-admin policy for a served profile with no cached config.
2026-09-28 12:23:25 -07:00
teknium1
b6a1be64f7 fix(env): skip a launch-home dotenv load inside a foreign routed turn (#125530)
The #125530 guard let any load whose TARGET was the launch home through while a
routed override was active. Inside a foreign profile's turn that load ends in
_reapply_terminal_config_bridge(launch), whose read_raw_config()/load_config_readonly()
follow the override, so the ROUTED profile's terminal.cwd was bridged into the shared
TERMINAL_CWD and the launch profile's next turn and cron job ran in the other tenant's
workdir (tests/e2e/core/tenancy/test_two_tenant_gateway.py red on CI). Require both the
target and the active home to be the launch home; the launch profile's own scoped bodies
(override naming the launch home) still load, a routed profile's .env still never does.

tests/agent/test_skip_background_review.py: the fixture pointed HERMES_HOME at a named
profile dir that never existed and built an AIAgent inside it; that only worked because
setup_logging() used run_agent's import-time home. It now logs under the ACTIVE home, and
mkdir_under_hermes_home() refuses to materialize a missing named profile by design
(load_config()'s ensure_hermes_home() and SessionDB refuse the same home, #94590), so the
test creates the profile dir. No assertion changed.
2026-09-28 12:23:25 -07:00
teknium1
e317f5205e fix(env): inline the launch-home check; prove A->B->A on two temp homes (#125530, salvage #125572)
The salvaged test wrote the live process home's .env; the replacement uses two tmp homes (launch loads, routed does not, launch loads again) under set_multiplex_active(True).
2026-09-28 12:23:25 -07:00
teknium1
9462098499 fix(gateway): resolve slash gating from the bot-owning profile in slash_access (#121705, salvage #121722)
policy_for_runner_source(runner, source) lives in the topical module instead of a 50-line mixin helper. Authorization follows the transport (bot-owning) profile the identity already names as authorization_home: a turn routed to profile B through the launch bot is gated by the launch bot's allow_admin_from, and a bot configured only in B is gated by B's. A served profile with no cached config fails closed (floor only). Drops the salvage's "launch gated + routed ungated => fail closed" rule: each profile's own config is authoritative for its own bots.
2026-09-28 12:23:25 -07:00
teknium1
b05f767b07 fix(logging): inline the rollover owner restore into doRollover (#120151, salvage #124764)
Same behaviour as the salvaged helpers (stat before, chown as root + chmod after, best-effort) without two module-level helpers; tests pin the chown hand-back and the refused-chown path.
2026-09-28 12:23:25 -07:00
teknium1
8affd9aeaa fix(agent): trim the #126005 salvage to the one-line active-home fix (#125974)
setup_logging(hermes_home=get_hermes_home()) is the whole fix; the FileNotFoundError fallback chain guarded a home that is being served (it exists), and the two extra tests re-tested it. Keep the A->B routing invariant and the no-bare-handler guard.
2026-09-28 12:23:25 -07:00
Yuan Li
71e89162c2 fix(env): still load the launch home's .env under multiplex routing (#125530)
The multiplex guard in load_hermes_dotenv() skipped the dotenv load whenever
a routed profile-home override was active AND multiplexing was on, even when
the load's target home IS the process launch home. Under the desktop/serve
backend the launch profile's own scoped bodies bind an override naming the
launch home (tui_gateway.model_switch._profile_runtime_scope_tokens(None)),
so its .env never loaded: 'No .env file found. Using system environment
variables.' and launch-profile-only credentials (e.g. an OPENROUTER_API_KEY
backing fallback_providers) were invisible to the process, silently breaking
failover.

Restrict the skip to loads whose target is a FOREIGN (routed) profile home,
using the same override-immune launch-home comparator _reapply_terminal_config_bridge
already uses in this file. The routed profile's .env is still never copied
into the shared os.environ (isolation intent of 1aa62ceb45 preserved).

(cherry picked from commit 0c71a329f48195a5c242d09ca6e92bb1c2842640)
2026-09-28 12:23:25 -07:00
Mohamad Kanso
d067d2fdc3 fix(gateway): use routed profile for slash access checks
(cherry picked from commit 306b35662586b7427a7bfe6855e75c2795fbaa6e)
2026-09-28 12:23:25 -07:00
WYSTAF
2ba97b01d4 fix(logging): preserve log ownership across a rollover
The stdlib rollover renames `agent.log` and opens a fresh one, so the
replacement is owned by whichever process wrote the record that crossed
`maxBytes`. With a rotating handler per profile, that is usually the
long-lived gateway, so a profile's log that a worker uid owned becomes
root-owned — and every later worker open of that path fails EACCES,
permanently, from what should be a routine rotation (#120151).

The failure surfaces as a model-connection error and is parked by
`check_respawn_guard` on a branch with no cooldown, so it never retries
even after ownership is repaired.

Snapshot the file's owner and mode before the rollover and hand them back
to the new file afterwards. Only root may chown to another uid, so for an
unprivileged process this is a no-op — it never changed the owner to begin
with — and on Windows, which has neither chown nor a meaningful uid, the
snapshot reads as "nothing to preserve". Every failure path is best-effort:
a log that cannot be chowned is still a working log.

(cherry picked from commit 379b099807146af67b7d655a4685c22336e05fab)
2026-09-28 12:23:25 -07:00
Tranquil-Flow
3fdb3cb84c fix(agent): log to the active profile home, not the import-time freeze (#125974)
A Desktop serve backend builds agents for several profiles inside
set_hermes_home_override(); _setup_logging passed run_agent's
import-time _hermes_home freeze, so setup_logging saw a home it already
served, never adopted the profile, and every profile's records landed in
the launch profile's agent.log. Pass the active-scope home instead.

A home that cannot host logs (missing/tombstoned named profile —
mkdir_under_hermes_home refuses to materialize it on purpose) falls back
to the process launch home, then disables file logging with a stderr
notice: logging setup must never take agent construction down.

(cherry picked from commit 76bcb71bae400e764fb0e04c28e610a3d1b5d4ca)
2026-09-28 12:23:25 -07:00
Yuan Li
e0cd1aba5d test(gateway): launch-home dotenv loads under multiplex — red/green regression for #125530
Contract: under multiplex, a load whose target IS the process launch home
must still populate os.environ from the launch home's .env (its scoped bodies
bind an override naming it), while a FOREIGN routed home's .env is still
never copied into the shared process environment.

(cherry picked from commit dfba1487d9e5444da5803c1e186f16caf1b3b956)
2026-09-28 12:23:25 -07:00
John Paul Soliva
1df4af417f fix(tts): resolve command-provider env_passthrough through the profile secret scope
run_command_provider forwarded declared env_passthrough keys from os.environ,
which under the multiplexer is the LAUNCH profile's .env. A served profile's
command TTS/STT subprocess (voice-note transcription, speech, warm/release
hooks) got the launch profile's credential and never its own. Resolve each key
with resolve_passthrough_value, as the terminal and code-execution spawns do.

(cherry picked from commit 22a299d5f562e1d174fa5d37590cd46361a1383a)
2026-09-28 12:23:25 -07:00
teknium1
379f14b553 test(local-runtime): racing-callers fake publishes a modern state record
65ff3ad353 made a bare legacy {pid} record non-adoptable (a live PID alone is not
evidence of the managed router) and moved the other tests in this file onto
_write_current_process_state, but left the racing-callers fake publishing the legacy
shape, so the second caller could never adopt the first's state and main's Run tests
lane went red on every PR rebased past it (main's own CI could not start at the time).
Publish the modern record the real supervisor writes.
2026-09-28 12:18:58 -07:00
teknium1
2f0b102ce3 fix(review): /api/profiles/active fails closed on current; document the hub-action secret scope
Review finding 2: `_or_default` in `get_active_profile_endpoint` answered
"default" for `current` when `get_active_profile_name()` raised — exactly the
value that lets the SPA's `shouldAdoptActiveProfile` retarget a dashboard to
the sticky active profile. A dashboard that cannot name its own home must not
read as the machine dashboard, so the failure fallback for `current` is now
"custom" (the same adoption-refusing answer the function itself gives an
unresolvable home). The empty case keeps `or "default"`, and `active` keeps
"default" on failure: "custom" there would itself trigger adoption of a
non-existent profile when `current == "default"`.

Review finding 1 (docs half): hub actions targeting `default` from the
machine dashboard now take the same scrubbed, HERMES_HOME-pinned environment
as every named-profile action. Kept on purpose (one env contract per hub
target); the user-visible rule — children run with the target profile's own
.env and secret sources, not the dashboard process environment — is now in
web-dashboard.md. The PR body carries the explicit behaviour-change note.
2026-09-28 12:18:58 -07:00
teknium1
146b5f8e76 test(dashboard): the default-home gateway restart argv names -p default
The #120688 fix makes _own_profile_selector name the default home as the
literal "default" selector and _profile_cli_args emit ["-p", "default"]
for it, so every unnamed lifecycle verb spawned from the default home
carries an explicit selector (test_gateway_unnamed_default_home_verbs).
Two existing tests pinned the old bare argv on the shared spawn site
(_restart_gateway_after -> _spawn_gateway_restart -> _gateway_subcommand):

- test_dashboard_admin_endpoints::test_enable_platform_starts_gateway_restart
  (POST /api/webhooks/enable auto-restart): expected argv is now
  ["-p", "default", "gateway", "restart"]; the test's subject (the save
  is authoritative, restart_started/pid reported) is unchanged.
- test_web_server::test_telegram_onboarding_apply_reports_restart_failure_after_save
  (Telegram onboarding apply auto-restart): same argv update inside the
  failing-spawn stub; the test's subject (a failed spawn is reported
  after the save, not raised) is unchanged.

`hermes -p default gateway restart` is accepted by hermes_cli/gateway.py:
_apply_profile_override pins HERMES_HOME to the root (skipping the
sticky active_profile), profile_lifecycle and
_guard_named_profile_under_multiplexer both pass "default" through, and
the CLI's own refusal text prescribes exactly that argv as the host
remedy. No other test in tests/ asserts a bare ["gateway", ...] argv
produced by these spawn sites (the remaining hits are mocks, hand-built
argv, or [-2:] suffix checks that still hold).
2026-09-28 12:18:58 -07:00
teknium1
1909674ebc chore(contributors): map ipriyaaanshu fork email 2026-09-28 12:18:58 -07:00
ipriyaaanshu
064d16981b fix(dashboard): a named-profile dashboard keeps its own profile instead of the sticky active one (#96712, salvage #96736)
shouldAdoptActiveProfile() aligned the switcher to the machine-global active_profile
whenever it differed from the serving profile, so a dashboard hosted by a named profile
(hermes -p demo serve) silently retargeted its banner, chat TUI env and model to another
agent on every load. Only the default-hosted machine dashboard aligns now; "custom"
(unnamed HERMES_HOME) never does.

Co-authored-by: ipriyaaanshu <ipriyaaanshu@users.noreply.github.com>
2026-09-28 12:18:58 -07:00
Adolanium
8c90004ec1 fix(dashboard): apply ?profile= from links that also change the path
Profiles > Manage skills & tools goes to /skills?profile=X. The path and the param change in the same navigation, so the effect that adopts the URL param and the effect that writes state back to the URL both ran with the old state. The write-back won, and the page stayed on the old profile with the URL rewritten to match it.

A changed ?profile= is now adopted during render, before effects run, so the URL sync effect only ever sees the new state.
2026-09-28 12:18:58 -07:00
teknium1
a3ad3c0c58 test(dashboard): the Nous recommended-default test uses the hermetic home fixture (#124283, salvage #124284)
The salvaged fixture set HERMES_HOME under tmp_path and ran the TestClient lifespan, which
read the checkout manifest and tripped the home-I/O guard; the unknown-profile case folds
into the one invariant test (the non-Nous branch is covered by test_web_unknown_profile_404).
2026-09-28 12:18:58 -07:00
John Paul Soliva
f78c1dbfcd fix(dashboard): the Nous recommended default honours ?profile=
GET /api/model/recommended-default?provider=nous returned before the
route entered the profile scope, so ?profile=<name> got the launch
profile's Nous tier, Portal URL and recommendation caches, and an
unknown profile answered 200 instead of the scope's 404. The Nous
branch now runs under _config_profile_scope like the other providers
and lets the scope's HTTPException through.
2026-09-28 12:18:58 -07:00
John Paul Soliva
9ad7156324 fix(dashboard): cron delivery targets, blueprints, recommended default, voice config, official skills and debug share answer an unknown ?profile= with 404
Each route's catch-all or fallback swallowed the profile scope's 404: the
GETs answered 200 with default or empty data (local-only targets, static
deliver options, an empty model, relay voice mode, no installed marks) and
POST /api/ops/debug-share answered 500. HTTPException now passes through
ahead of each catch-all, as in the rest of this change; for the hub skills
catalog the pass-through sits in _installed_hub_identifiers, so the search
and sources routes that share it keep their existing 404.

The unknown-profile test gains a row per GET route; the endpoint list in
web-dashboard.md names them.
2026-09-28 12:18:58 -07:00
John Paul Soliva
c2e4772b13 docs(web-dashboard): list learning graph and plugins hub as profile-scoped 2026-09-28 12:18:58 -07:00
John Paul Soliva
f793ccad2d fix(dashboard): an unknown ?profile= answers 404 on PUT /api/env, the learning graph and the plugins hub, not 500 2026-09-28 12:18:58 -07:00
John Paul Soliva
cc5cb8f963 fix(dashboard): the Codex saver takes the profile scope before the session lock
_profile_scope holds _SKILLS_PROFILE_LOCK for its whole body. The Nous,
xAI and MiniMax savers enter it and then take _oauth_sessions_lock; the
Codex saver took the session lock first and then the scope. Two sign-ins
finishing at once could each hold one lock and wait on the other. Enter
the scope first in the Codex saver too, so there is one lock order.
2026-09-28 12:18:58 -07:00
John Paul Soliva
08caaceb35 fix(dashboard): xAI and MiniMax sign-ins save into the profile they started in, never after a cancel
The dashboard device-code pollers looked the owning profile up by session id
only after the provider answered. A cancel (DELETE) or the 15-minute session
sweep had removed the registry entry by then, so the lookup returned None and
the tokens were saved into the dashboard's launch profile; the xAI and MiniMax
pollers also never re-checked for a cancel, so a cancelled login still saved
and made xai-oauth the default profile's active provider. The Codex path had
the same bug (IA-01).

All three pollers (Nous, MiniMax, xAI) now take the profile from their own
session dict, and MiniMax and xAI check "cancelled" in the same critical
section as the save, like Nous and Codex. The by-id lookup helper is gone.
2026-09-28 12:18:58 -07:00
Sahil Vishnalya
684fd0e36e fix(gateway): an explicit --profile default names the default profile (#100817, salvage #100888)
gateway/status.py::_command_line_belongs_to_profile and
hermes_cli/gateway.py::_matches_current_profile rejected any -p/--profile flag on the
default-home branch, so a default gateway started with an explicit --profile default
(hand-written launchd plist mirroring the named-profile shape) was reported stopped and
could not be matched for stop/restart. Only a flag naming ANOTHER profile disqualifies.

Co-authored-by: Sahilvishnaliya <Sahilvishnaliya@users.noreply.github.com>
2026-09-28 12:18:58 -07:00
teknium1
a335431b5d test(dashboard): trim the -p default salvage to two invariant tests (#120688, salvage #120689)
Keep the hub-install selector test (argv + HERMES_HOME pin on a pooled named-profile
serve) and the unnamed-verb test; drop the case-variant/ambient change-detectors and
the token-scrub assertion, which no longer holds since host_gateway_child_env() became
the env builder for -p default gateway verbs.
2026-09-28 12:18:58 -07:00
beardthelion
de031da774 fix(dashboard): name -p default for unnamed gateway verbs on the default home
An unnamed gateway lifecycle verb from a dashboard running on the default
home spawned a selector-less child (hermes gateway restart). The child
re-read the sticky active_profile and could restart another profile's
gateway, and it took the unscrubbed env branch so the default profile's
token survived into the child. _own_profile_selector now resolves the
default home to the literal default selector instead of None, so every
lifecycle argv carries -p and the named-target env scrub applies.

Adds tests/hermes_cli/test_gateway_unnamed_default_home_verbs.py covering
the pinned argv, the child home resolution under a sticky active_profile,
and the token scrub.
2026-09-28 12:18:58 -07:00
beardthelion
9bdecbbf57 fix(dashboard): keep -p default in hub-action argv so actions hit the default profile
_profile_cli_args folded "default" into the no-override early return
alongside "current", so a hub action scoped to the default profile
spawned selector-less argv. The child hermes process then resolved the
ambient HERMES_HOME (the launch profile under a pooled -p X serve, or
the sticky active_profile) and installed/toggled into the wrong
profile; it also skipped the named-profile credential scrub.

"default" is a real named target everywhere else (profile list,
_resolve_profile_dir, -p default already emitted for multiplexed
restarts), so only "" and "current" now mean ambient.
2026-09-28 12:18:58 -07:00
Brooklyn Nicholson
8423c4775c fix(desktop): make the full-changelog copy a small icon on the What's new row 2026-09-28 14:11:59 -05:00
Brooklyn Nicholson
6f2a01fce7 fix(desktop): complete locales and reuse CopyButton for full changelog copy
fr/de/es overlays are completeness-checked against the en key set, so the
new updates.copyFullLog key must ship there or catalog-completeness fails;
ar/ru carry the neighbouring moreChanges key, so they translate too.
Swap the hand-rolled copied-state button for the shared CopyButton
primitive (haptics, aria-label, error path, unmount cleanup included).
2026-09-28 14:11:59 -05:00
Weidows
542fd83d7b fix(desktop): address review feedback for full changelog copy
- Restore bounded -n 40 git log in readCommitLog (shallow-safe)
- Extract formatFullChangelogText to commit-changelog.ts for testability
- Fix breaking commit format: type(scope)!: not type!(scope):
- Add copyFullLog to ja.ts and zh-hant.ts
- Add unit tests for formatter (conventional, scoped, breaking, fallback)
2026-09-28 14:11:59 -05:00
Weidows
c395693c85 feat(desktop): add 'Copy full changelog' button to updates overlay
The update overlay previously showed at most 6 commit subjects from the
changelog, with no way to access the full list or copy it for translation.

Changes:
- Increase git log limit from 40 to all commits (remove -n 40)
- Add 'Copy full changelog' button that copies all commits as formatted
  text (type(scope): subject - author) to clipboard
- Use parseCommitHeader to reconstruct full convention commit format
  including scope and author
- Add i18n keys: en 'Copy full changelog', zh '复制完整更新日志'

Closes #48654
2026-09-28 14:11:59 -05:00
Brooklyn Nicholson
cb07391753 fix(desktop): drop SSH host refocus effect, assert custom input is typeable
The first-pick bug was the Select's controlled value already being the
Custom sentinel, which the value swap fixes on its own; the Input's
autoFocus handles focus once it mounts, so the rAF refocus effect is not
needed. Extend the regression test to type into the field and to check
that clearing it and blurring returns to the dropdown.
2026-09-28 14:00:18 -05:00