Commit Graph

4745 Commits

Author SHA1 Message Date
teknium1 3272fb35aa docs: profile-scope invariant in AGENTS.md — one process serves many profiles; out-of-turn code binds its scope
Root AGENTS.md § Code Shape Rules replaces "module-level constants are fine — they cache after
_apply_profile_override() sets HERMES_HOME" (true for `hermes -p x <cmd>`, inverted under the
multiplex gateway and the Desktop/dashboard `serve` backend, where os.environ holds the LAUNCH
profile) with the invariant: a profile = home + secret scope + terminal scope, bound per profile
ACTIVITY, and every execution point with no turn on the stack binds it explicitly. Names the real
seams: gateway/run.py::_profile_runtime_scope, tui_gateway @_profile_scoped +
_session_profile_runtime_scope (+ _profile_runtime_scope_tokens, launch_profile_policy ->
set_multiplex_active), cron/scheduler_provider.py::_profile_cron_scope,
gateway/run_agent_cache.py::_run_release_in_profile_scope, tools/environments/local.py::
served_profile_child_env, agent/memory_provider.py::spawn_context_thread. Adds a routing-table row
for profiles / multiplex / secret scope.

Area AGENTS.md paragraphs, one per seam, for gateway/ (activity-not-turn binding, hooks per
profile, adapter YAML never reaches os.environ, unserved shared-ingress reported via
_note_unserved_secondary_platform + needs_attention at the single writer), tui_gateway/ (RPC
binding is home AND secret AND terminal; HOME-only is half-bound; teardown chokepoint), cron/
(per-home tick lock, ticker scope incl. pre-loop code, kanban notifier routing, worker liveness by
(pid, worker_started_at) fingerprint, descendant fence as a path), hermes_cli/ (DEFAULT_CONFIG
key <-> reader parity, service-install matrix, -p vs multiplex home binding), tools/ (check_fn
reads through get_secret and is cached per hermes_home_key, one env builder per spawn, MCP trust
per profile), plugins/ (lifecycle hooks are bound by the caller; never cache the home from
initialize()), apps/desktop/src/ (pooled serve per (connection, profile); remote topologies),
agent/ (end-of-session flush is caller-bound; set_multiplex_active gates fail-closed).

Corrects the statements the multiplex model made wrong, in the same PR: root module-constant
sentence; hermes_cli "sets HERMES_HOME before any import" (+ cli-internals.md);
ADDING_A_PLATFORM.md §2 raw os.getenv loader (now an _ENV_STEPS row through config.py::_getenv)
and §4 platform_env_map in gateway/run.py (now _PLATFORM_ALLOWLIST_ENV in pairing.py + registry
allowed_users_env); platform_registry.py "may set os.environ (guard with not os.getenv)";
cron/AGENTS.md hardcoded ~/.hermes/cron/.tick.lock; gateway-internals.md agent:main as THE key
format, ~/.hermes/hooks/, single-profile `gateway stop`, plus a new "Multiplexed profiles"
section; tools/AGENTS.md os.getenv check_fn sample; "installed per turn" wording; "one temp
HERMES_HOME" E2E wording; multi-profile-gateways.md intro lists system units, Windows tasks, s6
and the Desktop backend.
2026-09-15 10:59:22 -07:00
brooklyn! 673d33095b fix(desktop): keep code geometry stable while highlighting loads 2026-09-15 12:33:45 -05:00
brooklyn! 3ccad12c0a fix(desktop): preserve reading intent across history and pane changes
Co-authored-by: wukangcheng1994 <160389295+wukangcheng1994@users.noreply.github.com>
Co-authored-by: Zhuzewen <zhuzw@seer-robotics.ai>
Co-authored-by: KoNit-K <124019182+KoNit-K@users.noreply.github.com>
Co-authored-by: nftpoetrist <264138787+nftpoetrist@users.noreply.github.com>
Co-authored-by: aydnOktay <xaydinoktay@gmail.com>
Co-authored-by: FalconOrtiz <falcon.ortiz11@gmail.com>
Co-authored-by: Ahmett101 <ahmet.tunc@gmail.com>
Co-authored-by: Val Alexander <val@opencoven.ai>
Co-authored-by: networthexplained <networthexplained@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 12:33:45 -05:00
brooklyn! 76f9bf948b fix(desktop): retain scoped transcript paint through background hydration
Co-authored-by: Benjamin Brumbaugh <benbrumbaugh@gmail.com>
Co-authored-by: Daisuke Suzuki <dai.suzuki.829@gmail.com>
Co-authored-by: xrbs00 <178640517+xrbs00@users.noreply.github.com>
2026-09-15 12:33:45 -05:00
teknium1 69fd61b0ef fix(desktop): anchor the first-paint backfill so long sessions stop lurching (#99920)
Switching to a long tool-heavy session still painted the tail, then jumped a
full viewport when the first backfill step committed, on current main.

Root cause: anchorBeforePrepend() skipped while the load was unsettled, but the
settle loop hands a bottom-pinned load back at the FIRST-PAINT height, before
the backfill transition commits. Every backfill step therefore ran unanchored;
the prepend grew scrollHeight by thousands of px with scrollTop untouched and
the view sat near the top until use-stick-to-bottom's ResizeObserver re-pinned
it frames later (measured CLS 0.6-1.0 per switch, layout-shift entries of 0.61).

Fix:
- anchor a bottom-pinned load even while unsettled (shouldAnchorBeforePrepend);
  an unsettled offset restore still never anchors, the settle loop owns it
- record the anchor's scrollHeight and consume it only on a taller tree, so a
  commit that does not grow content (transcript arriving under the first-paint
  budget, a store-window refresh) cannot spend the anchor as a no-op
- clear any anchor on a cold switch, not only on a key change: the stale rows
  shown under the new key are collapsed by the swap and the anchor with them
- do not arm the backfill while the transcript is empty or nothing is hidden

Live A/B (isolated Desktop, Xvfb, CDP layout-shift capture, 3 rounds x 2 long
sessions): main CLS 0.19-0.65 with one-frame 3-4k px scroll jumps every switch;
fixed CLS 0 on every warm switch and no scroll jump (cold first open of a
session keeps only the sub-0.1 first-paint shifts). Show-earlier and remembered
offset restore controls unchanged.
2026-09-15 08:51:14 -07:00
teknium1 043632e283 feat(desktop): hideable profile rail with a statusbar profile dropdown stand-in
For people who run profiles as bots, the colored profile strip at the sidebar
foot duplicates the sessions list (community request). Add a persisted
`hermes.desktop.profileRailVisible` preference (on by default) toggled from the
Sessions view menu ("Profile rail"), the shell right-click menu, ⌘K
("Toggle profile rail") and an unbound `view.toggleProfileRail` keybind.

While the rail is hidden the statusbar grows a `ProfileSwitcher` dropdown
beside the gateway switcher ("This device ⌄ · Profiles ⌄") offering the same
choices the rail does: this gateway's profiles, All profiles, every other
gateway's agents in fleet mode, New / Import / Manage. It also answers the
`profile.create` hotkey the rail used to own, so no door is lost.
2026-09-15 08:50:18 -07:00
teknium1 6bc3628ce8 fix(desktop): drag gateway/profile groups by their header, not only the hidden handle
The Gateway & profile sidebar sections carried dnd-kit listeners on the lead
glyph alone, and that glyph only reveals its grabber on hover — so a press on
the row's name or empty space did nothing and users fell back to the ⋯ menu's
Move up / Move down. Bind the sortable to the whole header (same shape as a
project row), with the handle and the ⋯/caret cluster keeping their own
gestures; a sub-threshold press on the label is still the fold click.

Live: Playwright pointer drag on the header label reorders sections (before:
unchanged, after: reordered); handle-drag still works. Test drives dnd-kit's
keyboard sensor on the label and is red on the old binding.
2026-09-15 08:50:18 -07:00
kshitijk4poor 658f319147 fix(free-tier): setup.ready carries the failure block flat, the shape setup.status already spreads
`SetupRecord.as_payload()` serialised the record verbatim, so the broadcast
nested `failure: {...}` while `setup.status` spread the same four keys flat.
A client keyed on `error_code` saw it on one surface and not the other. Flatten
it in `as_payload`, declare the three optional keys on `SetupReadyPayload`, and
regenerate the TS/OpenRPC contract.
2026-09-15 20:44:42 +05:30
Robin Fernandes 1034215ae8 docs(free-tier): drop the rehearsal page and its references
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 20:44:42 +05:30
Robin Fernandes a241f42fc8 copy(free-tier): "it's free" without "keeps the free model" — signed-in free models are not the same model
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 20:44:42 +05:30
Robin Fernandes 16def7b8cc fix(free-tier): the desktop renders a free-tier refusal as its own card, not an OAuth re-login
A welcome-tier 403 classifies as auth_permanent, so the desktop's error
surface mapped it to "Your Nous Portal sign-in expired" with a Nous Portal
re-login button — the chat sentence never reached the user. Terminal results
on the free route now carry a structured free_tier block (kind + the chat
sentence); agent/error_surface.py turns it into a free_tier_<kind> code on
the provider layer with the sentence as `message`. The desktop gives those
codes their own titles, shows the backend sentence as the body, and offers
"Sign in with a Nous account" (the free-tier dialog) instead of the OAuth
re-login, with Retry only where a later send can succeed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 20:44:42 +05:30
Robin Fernandes d89cacc25f chore(free-tier): keep the rehearsal server out of the repo; the docs page explains the stand-in instead
The fault-injecting server served one-off manual rehearsal only and would
drift silently from the real services; the doc now says how to point the
desktop at any local stand-in (the three env overrides) and what such a
stand-in has to speak. The dev-only HERMES_EXTRA_WELCOME_HOSTS override stays,
pinned by a test in test_anon_failure_modes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 20:44:42 +05:30
Robin Fernandes 59fad62a40 fix(free-tier): review follow-ups — read the classifier's context, never replace a locked identity, re-inventory on retry
Correctness
- The welcome-tier recovery hooks (model_not_free move, wrong-host heal) and
  the long-wait rate-limit check read the turn's extract_api_error_context()
  dict, which never carries welcome_refusal / welcome_route. They now read
  classified.error_context, where _nous_welcome_tier parks them; the guard
  records the classifier's reset_at. Tests drive the real classifier and the
  real extractor so the two-context boundary is exercised.
- The connector path caught every AnonCredentialDead and re-minted; a locked
  account (anon_account_locked) is now retired without replacement, matching
  the inference resolver.
- A background bootstrap retry reused the boot-time provider inventory; it
  re-inventories, so a provider connected during the cooldown keeps
  inference.
- The desktop's setup.ready listener only refreshes an untouched picker
  (oauth mode, no local endpoint, idle flow) and re-checks after the
  readiness round, so an API-key form opened meanwhile is never dismissed.
- /__log on the rehearsal server sent its response while holding the state
  lock that _send re-acquires; the log is copied out first.

Reductions
- One shared FakePortal / install_portal (tests/hermes_cli/anon_portal.py)
  behind both free-tier fixtures, with a single httpx.Client transport seam.
- The rehearsal server's static inference answers are a table; dead
  scaffolding (REAL_PAID_URL, claim_codes, the no-op dead_once branch,
  extra_headers) removed.
- Setup-notice copy is a code-to-key map; its test uses real codes (the old
  loop built nonexistent ones and only exercised the fallback).
- The ineffective FreeTierErrorCode union is gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 20:44:42 +05:30
Robin Fernandes 51e39af967 feat(free-tier): ruled behaviour for every welcome-api failure, with friendly copy and a fault-injecting rehearsal server
The free tier depends on the account service (NAS) and the welcome inference
host, and Hermes had no honest answer for most of the ways either can refuse
or fail: the NAS codes it matched were never sent, the tier-dark 403 carried
no message to match, a single boot-time blip disabled minting for the whole
process, and a structured rate-limit refusal never reached the cross-session
guard, so the "sign in for a bigger allowance" prompt was dead code.

Backend
- anon_auth: classify what NAS actually sends (404 not_found, 503
  temporarily_disabled, 429 + Retry-After, 428 pow_*, 403 account_locked)
  into one ANON_* code each, carrying retry_after / retryable on AuthError.
- Replace the process-lifetime mint memo with a per-profile cooldown that
  honours the server's wait, climbs a short ladder when the service is
  unreachable, never retries terminal codes, and yields to the user's own
  retry (force=True).
- Bootstrap record carries error_code / retryable / retry_after; a bounded
  background loop retries transient failures and re-announces setup.ready.
  setup.status and free_tier.status expose the block; free_tier.provision is
  the forced retry.
- Inference: a generic 403 from a welcome host is the tier refusing (keyed on
  the route); model_not_free moves onto the gateway's alternate once;
  anon_on_paid_host re-reads the route once; a long rate_limited refusal
  trips the cross-session guard; a locked account is retired but never
  replaced; terminal copy on the free route is one plain sentence.
- Sign-in: Failed keeps the service's code and wait; account_busy is
  retryable; the OAuth poll reports retryable / retry_after.
- All user-facing copy rewritten for first-time users: never "the free
  service is off" (what is unavailable is using Hermes without signing in,
  and signing in is free), no jargon, spoken waits.

Desktop
- A setup-failure notice above the provider picker: one sentence per code,
  a retry when the backend says one can work, the sign-in pointer only when
  the account service answered at all. The overlay re-checks readiness on
  setup.ready so a background success dismisses it.
- Sign-in dialog gains busy / unreachable / unavailable screens.

Rehearsal
- scripts/free_tier_fault_server.py stands in for both services with the
  real wire contract and a CORS-open scenario switch; HERMES_EXTRA_WELCOME_HOSTS
  (dev-only, env-only) lets the route rules treat it as the welcome host.
  Walkthrough in website/docs/developer-guide/free-tier-fault-rehearsal.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 20:44:42 +05:30
teknium1 15ebf7ec90 test(desktop): drop the never-assigned descriptor-profile knob from the connections mock
The salvaged switch-back regression only ever exercised the profile-less
primary shape; the mock's string branch had no writer. Publish the
profile-less local descriptor unconditionally and say why.
2026-09-15 06:32:53 -07:00
Konstantin Khlopkov 825324fdc9 fix(desktop): restore the last-used profile when the primary local descriptor is profile-less
The local primary backend (startHermes) publishes its descriptor without a
profile field, unlike the pooled/forced-local child path. Downstream, a
profile-less descriptor normalizes to 'default', so on a switch back to the
local source the store either refused to remember the source/profile pair or
killed the commit in targetIsActive() — landing the window on 'default'
every time (#110819).

- The primary local descriptor now carries the profile it booted with, and
  the unshared primary branch of ensureBackend() keeps the requested key on
  the descriptor instead of dropping it.
- selectConnection() tolerates a profile-less descriptor on the source it is
  landing: the activation already published the route that was asked for, so
  a missing profile no longer strands the switch.
2026-09-15 06:32:53 -07:00
teknium1 d735097376 fix(desktop): keep createdBy=learn through the starmap share code
The share-code codec encodes createdBy through a fixed table that only
knew none/agent/user, so idxOf returned 0 for 'learn' and a learned
skill round-tripped as createdBy: null. The 2-bit field has a free slot,
so 'learn' now occupies it, and metaBadges labels agent- and
learn-created skills alike as 'learned' — matching the Python side,
where both values count as learning signals.

Review finding: learn nodes lost createdBy in share-code round-trip.
2026-09-15 05:38:31 -07:00
teknium1 ee6fb0ed22 fix: clear the relay roster per sole connection, not once per below-two regime
rosterCleared was a single boolean latched the first time the peer set fell
below two connections. When the sole connection a was replaced by c between
ticks (still length 1) the flag stayed set, so c's gateway never received
the empty-roster push and kept the stale roster. Track the id of the
connection that got the clear instead and push whenever the sole
connection's id differs; reset once two or more connections relay again.

Review finding: routes [a] → tick → routes [c] → tick pushed no roster clear to c.
2026-09-15 05:19:12 -07:00
teknium1 c02a269351 fix(desktop): an empty relay route list does not spend the one roster clear
relayConnections() returns [] before the registry loads (and whenever the
host bridge is missing). The below-two clear must not latch on that empty
list, or the single connection that arrives on the next tick never gets its
roster cleared. Gate the clear on exactly one connection; formats the
salvaged test file.
2026-09-15 05:19:12 -07:00
John Paul Soliva 8b091e539c fix(desktop): the relay clears the remaining gateways' remote roster when the peer set drops below two
syncRelayRosters returned early with fewer than two connections, so a machine
removed from the registry stayed in every remaining gateway's
bot_relay/roster.json — still listed in each bot's system-prompt roster and
still a message_agent target — until a second connection appeared again.
Push the now-empty roster once when the set shrinks below two (and once at
start with a single connection, for a roster left behind by an earlier peer);
union pushes resume when a peer returns.
2026-09-15 05:19:12 -07:00
teknium1 33cd423d56 fix: remote probe watchdog kills the probe's process group, not just its child
The watchdog SIGKILLed only the direct child of the wrapper. A remote
`hermes` launcher that runs the CLI without exec leaves the hung grandchild
alive after its parent dies — exactly the broken-launcher class from
#110478 — so the probe still orphaned a process on the remote. Enabling
job control (`set -m`) around the probe start puts it in its own process
group; the watchdog now kills `-$__htp` first and the direct pid as the
fallback for shells that cannot turn job control on without a tty (dash),
where behaviour is unchanged.

Review finding: watchdog SIGKILLs only its direct child; a non-exec launcher leaves the hung grandchild orphaned.
2026-09-15 05:18:28 -07:00
teknium1 b06071c0c9 test(desktop): make the remote-watchdog probe test host-safe
The live shell leg skipped nothing on Windows (no POSIX sh) and its orphan
sweep matched ANY `sleep 30` on the host, so a busy dev box or a parallel
test failed it. Skip on win32 like the other shell-shape tests here, and
sleep a per-run unique duration so the sweep can only see our own child.
The lockfile-skew guard now rejects a kill of any literal pid instead of
only `kill -9`, so the probe watchdog's own `kill -9 $__htp` no longer
weakens it.
2026-09-15 05:18:28 -07:00
Kevin Rajan 5724cb3002 fix(desktop): kill hung remote SSH probes via a POSIX remote watchdog
runSsh SIGKILLs the LOCAL ssh child on timeout, but the remote command keeps
running as an orphan (ppid=1). A hung remote CLI (e.g. a wedged
`hermes --version`) therefore accumulates orphans on every timed-out probe.

Wrap the two remote Hermes CLI probes (--version and the serve --help
ownership probe) in withRemoteTimeout(), a pure-POSIX remote watchdog
(macOS remotes have no GNU `timeout`): the probe runs as the watchdog's
direct child and is kill -9'd remotely after 15s, before the local
20s exec timeout fires. The sleeper's stdio is detached so its orphaned
sleep cannot hold the ssh channel open on the healthy path.

Fixes #110478
2026-09-15 05:18:28 -07:00
teknium1 bc59c39cfd docs(desktop): point the copy-control inset comment at the real scroller
The salvaged comment cited tool/fallback.tsx, which does not exist; name ExpandableBlock/CodeCardBody (the .scrollbar-overlay scroller) and log-tail.tsx (the 12px sibling) instead.
2026-09-15 05:15:42 -07:00
David Metcalfe 6e04bf8e59 fix(desktop): keep the code-block copy control clear of the scrollbar
The fenced code card's scroller (CodeCardBody / ExpandableBlock) carries
`.scrollbar-overlay`, which hands the card's right edge back to the
platform's ~15-17px scrollbar lane. The hover copy control sat at a 6px
inset with a 10px icon, inside that lane and on top of the bar itself.
Inset it 16px and use the 12px icon the other corner copy controls use
(log-tail.tsx).

Salvaged from #110548 without its className change-detector test; the fix
is pure CSS (Tailwind inset/icon size) and is verified by a static trace
against the .scrollbar-overlay scroller.
2026-09-15 05:15:42 -07:00
KoNit-K fccdcdaea8 fix(desktop): expose Codex compression auto-raise 2026-09-15 05:14:59 -07:00
KoNit-K ec5c975902 fix(desktop): refetch vault sources after closed-to-open remount
Per-query staleTime: 0 so a fresh installed:false cache still refetches when the Passwords page remounts disabled and the gateway opens later.
2026-09-15 05:14:15 -07:00
KoNit-K c89a4404fd fix(desktop): refresh vault source detection on mount 2026-09-15 05:14:15 -07:00
DavidMetcalfe 6201a8236f fix(desktop): drop unsaved credential edits when the settings target changes
The shared Settings "Applies to" target re-fetches `vars`, but the in-flight
edit and revealed maps are keyed by var name alone and were not reset with it.
A value typed while targeting one profile therefore survived a switch to
another, where the still-live Save button wrote it into the profile then being
targeted — the credential landed in the wrong profile.
2026-09-15 05:13:32 -07:00
teknium1 7c47e5517b test(desktop): trim the live-draft helper coverage to two invariants
Keep the case the fix exists for (editor holds text the mirror has not
seen yet) and the pre-mount fallback; the whitespace, stale-non-empty and
cleared-editor cases all reduce to "the helper returns composerPlainText
of the editor", which the first test already pins.
2026-09-15 05:12:33 -07:00
DavidMetcalfe 7679556d90 test(desktop): tighten the live-draft race coverage and dedupe the sibling reads
Tighten the empty-editor case against a stale non-empty mirror, add JSDOM body cleanup for editorWith, and route the bare-Enter and Cmd/Ctrl+Enter live reads through liveComposerDraft.
2026-09-15 05:12:33 -07:00
DavidMetcalfe bc62b270ed fix(desktop): read the live composer draft for the recall guard
draftRef is a once-per-frame mirror, so an ArrowUp in the same frame as a keystroke or paste saw the pre-keystroke text and let the sent-message recall overwrite what the user just wrote.
2026-09-15 05:12:33 -07:00
teknium1 e5d57c7fc5 refactor(desktop): workspace lanes read the show-all preference at the leaf
The salvaged fix threaded `showAllSessions` through three components
(sessions-section → entered-content → RepoFlatSection → workspace-group) as
a prop. The repo's TypeScript rule is that a leaf subscribes to the shared
atom instead of state being threaded through intermediaries, and
overview-row.tsx already reads `$sidebarShowAllSessions` that way. Drop
the prop plumbing and let SidebarWorkspaceGroup `useStore` the atom, which
also covers the bare `groups.map(SidebarWorkspaceGroup)` branch in
sessions-section.tsx that the prop version left paged.

The regression test flips the atom instead of re-rendering with a prop.
Still red against origin/main source, green here.
2026-09-15 05:11:46 -07:00
KoNit-K 38a4e909c6 fix(desktop): honor show all sessions in projects 2026-09-15 05:11:46 -07:00
teknium1 9b5f5a0c84 fix(desktop): local-runtime job poller no longer throws after the test env tears down
The 700ms re-poll in local-runtime-jobs.ts reached for `window.setTimeout`;
when a test left a running job in the store, the timer fired after vitest had
torn down jsdom and threw `ReferenceError: window is not defined` as an
unhandled error, failing the whole check:test:ui lane on main even though all
7808 tests passed. Use the global timer functions (identical in the renderer)
and drain the running-job state after each local-models-settings test so no
poll is armed across test boundaries.
2026-09-15 04:50:07 -07:00
teknium1 9c689bee1a fix(bot-mode): an empty member seat surfaces an error instead of swallowing the group send
Ported from the pre-TSX PR onto the current module layout. main's
group-chat-view.tsx already restores the composer draft when
sendToGroupChat returns null (feat(desktop): retain Bot group drafts by
room, b42d8279ed), so the draft-loss half of the original fix is
FIXED_ON_MAIN and not re-applied here. What remained: sendToGroupChat in
group-rounds.ts still folded "no text" and "no members" into one silent
`return null`, so a fully typed message into a room whose roster had not
hydrated (or a legacy room record without member descriptors) was
rejected with no thread, no log entry and no error.

The guards are split: empty content stays silent, an empty member seat
raises host.notify with a Bot Mode i18n string (en/ja/zh/zh-hant). The
wording no longer promises a retry will help (review: a legacy room with
no member descriptors never recovers by retrying) and points at the two
real remedies.

The original source-regex .mjs tests are dropped per review; the
contract is pinned by a behavioural vitest in group-rounds.test.ts that
drives sendToGroupChat through the scripted room harness: empty members
→ null + one error toast + no log entry; blank text → null, no toast.
2026-09-15 04:14:09 -07:00
teknium1 81e89ad1c1 test(desktop): kanban block-loop title assertion follows the plain-language copy 2026-09-15 03:50:00 -07:00
teknium1 66878996dd fix(ux): plain-language, actionable user-facing messages (desktop-tui)
Squashed integration of the user-facing message audit for this surface set.
Full per-finding receipts: /tmp/ux-audit/lanes/*-receipt.md (campaign artifacts).
2026-09-15 03:46:45 -07:00
teknium1 ca5822b5a5 test(desktop): mark the loud scope note semantically instead of by class
Asserting the loud variant via the Tailwind `font-medium` class couples
the test to styling: any restyle of the accent breaks it without the
behaviour changing. Give the note role="status" (it announces which
profile edits land in) and a `data-scope-loud` flag for the non-default
variant, and assert those. Also fixes the padding-line lint warning in
settings-scope.test.ts.
2026-09-15 03:42:20 -07:00
teknium1 d85aaa3049 test(desktop): stub the new settings-scope atoms in the ConfigSettings test
config-settings.test.tsx mocks @/store/settings-scope with a hand-written
subset of exports; SettingsProfileScope now also reads
$settingsScopeProfile and $settingsScopeEditsNonDefault, so the partial
mock threw at render time in CI (JS & TS checks red).
2026-09-15 03:42:20 -07:00
teknium1 d7d9209795 fix(desktop): derive the loud scope note from the shared settings-scope store
Rewrite of the original component-local `editingNonDefault` computation
onto the existing scope architecture (owner's ask: "use the same
existing architecture" as the Capabilities/toolset scope handling).

- store/settings-scope.ts gains `$settingsScopeEditsNonDefault`, a
  computed over `$settingsScopeProfile` × `$profiles`, so "the settings
  pages are editing a non-default profile" is a store fact any surface
  can subscribe to, not a per-component recomputation.
- profile-scope.tsx reads `$settingsScopeProfile` for the selected chip
  (instead of re-deriving `override ?? active`) and the new selector for
  the loud/quiet note. User-visible outcome is unchanged: accented note
  for any non-default target, override or not; quiet note for an explicit
  override onto the default; nothing when following the active default.
- Review: an unloaded roster (no `is_default` entry yet) used to suppress
  the note exactly at the landing moment where a bot may already be the
  active profile. The selector now assumes the root profile's canonical
  key as the default, so an unknown default fails loud, not quiet. The
  four-cell truth table is documented at the JSX conditional.
- settings-scope.test.ts pins the selector across active-profile,
  override and unloaded-roster inputs; the component tests from the
  original PR are kept as-is and still pass.
2026-09-15 03:42:20 -07:00
Teknium b047b5db4c fix(desktop): settings pages state loudly when they edit a non-default profile's config
After any Bot Mode chat the active gateway profile is the bot's, so the
settings scope silently followed it — edits landed in
profiles/<bot>/config.yaml with only a faint chip tint as the tell
(#89190/#89162/#89597 report class, live-repro'd: Max Agent Steps written
to scout's config). The applies-to note now renders for ANY non-default
target, override or not, accented; default-profile editing stays quiet.
2026-09-15 03:42:20 -07:00
teknium1 8b9df066e2 test(gateway): trim block-loop wording coverage to two invariants; CLI keeps the needs_input distinction
The salvaged parametrized set collapsed to one neutral case (transient) plus
the needs_input positive control. hermes kanban block now mirrors the
notifier: 'needs a human decision' only when the block was typed
needs_input, 'orchestration attention needed' otherwise.
2026-09-15 03:42:00 -07:00
KoNit-K 5c970d9745 fix(kanban): neutral block-loop wording on the CLI, Desktop toast, wake text and docs
The sibling surfaces of the gateway ping rendered the same false claim:
`hermes kanban block` said "needs a human decision", the Desktop toast title
said "needs a decision", the wake status line (locales/*.yaml
gateway.kanban.wake.block_loop_detected) said "needs a decision" and the
docs described the triage route as "for a human decision". A repeated-block
circuit breaker only establishes that orchestration attention is needed.

Surface sweep from PR #111131 (notifier/test hunks dropped in favour of the
typed-kind formatter from PR #111132).
2026-09-15 03:42:00 -07:00
teknium1 0468acdce6 fix(desktop): key voice-fields autosave on the profile scope string
Parents (profile-config.tsx) build the `profile` scope as a fresh object
literal on every render, so `useMemo(..., [profile])` produced a new
cache writer each time and the autosave effect, which depends on both,
tore down and re-armed its 550ms timer on every unrelated re-render.
Key both on profileScopeKey(profile) — the same string the query cache
already uses as the scope's identity — so only a real scope change
re-arms them.
2026-09-15 03:41:04 -07:00
teknium1 ab1ef0c88e test(desktop): pin that a scoped Capabilities TTS panel saves into its scope
Review on the PR: the load-bearing direction (profile B's scope
forwarded into saveHermesConfigRecord) was only covered by the live
E2E; the unit test asserted just the unscoped default. Renders the panel
with profile={profile:'scout', connectionId:'gw-2'} and asserts the
autosave carries exactly that scope. The @/hermes full-replacement mock
gains profileScopeKey, which use-config-record reaches when a scope is
present. Sabotage (drop the profile prop from <VoiceProviderFields>)
fails this test with `expected undefined to deeply equal {profile:
'scout', …}`.
2026-09-15 03:41:04 -07:00
Teknium bdac5c8e2e fix(desktop): Capabilities TTS voice fields write the scoped profile's config, not the active one
ToolsetConfigPanel threads its profile scope into every fetch but rendered
VoiceProviderFields without it, and the fields were hard-wired unscoped —
configuring profile B's TTS from the Capabilities selector read and
autosaved profile A's whole config record. New capability-scoped
saveHermesConfigRecord (symmetric with getHermesConfigRecord), profile
prop threaded through, per-scope cache write-through. Unscoped callers
(Settings → Voice) unchanged.
2026-09-15 03:41:04 -07:00
teknium1 e860b8e4e4 fix(context): compute-host /context and session.context_breakdown carry the per-file manifest; report blocked files
Why: the tui_gateway live formatter (`_format_live_context_output`, used when
the session runs on a compute host) renders its own summary and never got the
"Context files" block, and `session.context_breakdown` had no structured rows,
so Desktop's popover could not show them. The formatter now appends
render_context_file_lines() with the session cwd bound (the RPC thread has no
session context, so the discovery walk would key on the backend's cwd), and
the RPC payload gains a `context_files` list (contract + generated TS/OpenRPC
+ Desktop type). The docs sentence is scoped to the surfaces that render it.

A file whose content _scan_context_content replaces with a BLOCKED marker was
reported "loaded"; the manifest now runs the same scan and reports `blocked`.
The module docstring names the frontmatter-strip / chain-cap approximations
and drops the product-name attribution (credit stays in the PR body).
2026-09-15 03:37:49 -07:00
kshitijk4poor e112da7578 docs(desktop): preflight comments describe the OAuth-only branch; scope assertion via objectContaining 2026-09-15 16:07:43 +05:30
kshitijk4poor 2821cb2d8c refactor(desktop): one profile-order sort for the active strip and the at-rest groups
sortByProfileOrder moves to a pure lib module with a key selector so
buildRestGroups sorts its named squares directly, replacing the collator
sort that the component then re-sorted with a different comparator. The
fleet rail test no longer needs importOriginal (and three store mocks) to
reach the helper.
2026-09-15 16:07:43 +05:30