Commit graph

1065 commits

Author SHA1 Message Date
nousbot-eng
7fd419e5e6
fmt(js): npm run fix on merge (#66890)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-18 11:21:28 +00:00
Tranquil-Flow
c7eb0cd22c fix(desktop): preserve dirty inline edits on blur 2026-07-18 04:15:22 -07:00
teknium1
ddd34a98f3 fix(desktop): harden Windows sandbox fallback against false-positive sandbox loss
Follow-up to the salvaged #66803 (@HexLab98):

- Two-strike boot marker: a single mid-boot abort (task-manager kill,
  power loss) no longer disables the sandbox — only a second consecutive
  abort, or a signature-confirmed GPU/renderer STATUS_BREAKPOINT death,
  engages --no-sandbox.
- Version-scoped stickiness: the fallback marker records the app version
  and re-probes the sandbox once after an update (new Electron or
  installer ACL repair may have fixed the host) instead of degrading
  forever. A failed re-probe returns straight to fallback.
- Launch-time icacls repair now runs only when the marker shows a prior
  aborted boot (icacls /T recurses the whole install tree — healthy
  launches skip it; the installer grants the ACE at install time), and
  targets the install dir only. The userData grant is dropped: granting
  S-1-15-2-2 RX on userData would expose Hermes sessions/config to every
  AppContainer app on the machine.
- Renderer crash-loop recovery (same class as #56726, credit @Sahil-SS9
  in PR #57414): a Windows renderer crash loop bearing the breakpoint
  exit code gets the same one-shot --no-sandbox relaunch instead of a
  dead window; unrelated crash loops keep the sandbox.
- Manual --no-sandbox launches are honored but never made sticky.

Tests: 15/15 windows-sandbox-fallback vitest; full desktop electron
suite 432 passed / 1 skipped.
2026-07-18 02:26:19 -07:00
HexLab98
2b0c4d69ea test(desktop): cover Windows sandbox fallback marker and ACL helpers 2026-07-18 02:26:19 -07:00
HexLab98
e53d2dfccb fix(desktop): recover Windows GPU sandbox 0x80000003 startup crashes
Grant ALL APPLICATION PACKAGES RX on the unpacked app and stick a boot
marker so fatal Chromium sandbox deaths relaunch with --no-sandbox
(#38216).
2026-07-18 02:26:19 -07:00
nousbot-eng
c48d53413a
fmt(js): npm run fix on merge (#66741)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-18 05:16:25 +00:00
brooklyn!
65f1c94d2d
Merge pull request #66737 from NousResearch/bb/tooltip-focus-open
fix(desktop): stop tooltips re-opening when a menu/dialog restores focus to its trigger
2026-07-18 01:09:25 -04:00
Brooklyn Nicholson
7f69494c3a fix(desktop): stop tooltips re-opening when a menu/dialog restores focus to its trigger
Picking a model from the composer model pill left the pill's tooltip
stuck open over the fresh selection: Radix Tooltip opens on ANY trigger
focus (its isPointerDownRef guard only covers a pointerdown on the
trigger itself), and Radix menus/dialogs restore focus to their trigger
on close — so every mouse-driven pick ended with a phantom tip. Same
pattern on every Tip-wrapped trigger that opens an overlay.

Gate the focus-open to KEYBOARD focus: the trigger's own onFocus runs
before Radix's composed handler and calls preventDefault() unless the
trigger matches :focus-visible — composeEventHandlers skips onOpen for
defaultPrevented events. Chromium keeps focus-visible modality across
the menu round-trip, so a mouse pick's focus restore no longer opens
the tip, while Tab-focus still shows it (a11y unchanged). Fails open if
:focus-visible is unsupported.

Tests cover the three branches (suppress on non-keyboard focus, keep on
keyboard focus, fail open on selector error); chat/shell suites green.
2026-07-18 01:04:24 -04:00
Brooklyn Nicholson
da805810d4 fix(desktop): restore exec bit on node-pty spawn-helper for dev terminals
node-pty's published npm tarball ships the POSIX `spawn-helper` with mode
0644 (no exec bit). node-pty `posix_spawnp`s that helper on macOS/Linux, so a
non-executable copy fails every embedded-terminal spawn with
`Error: posix_spawnp failed.`. Packaged builds are unaffected because
stage-native-deps.mjs chmods the staged copy, but the dev flow
(`npm run dev` -> `electron .`) resolves node-pty straight from node_modules,
which nothing chmods -- so the first terminal in dev always dies.

Restore the exec bit once, lazily, right before the first spawn, via a small
DI-testable helper. Idempotent: already-executable copies (packaged builds)
are left untouched, and stat/chmod failures are collected and logged rather
than thrown so terminal startup never breaks.
2026-07-18 01:00:48 -04:00
brooklyn!
77a33111c7
Merge pull request #66470 from NousResearch/bb/picker-dialog-latency
perf: fast model picker + dialogs — config-load hot path, model.options off the reader thread, off-screen turns skip rendering
2026-07-18 00:58:29 -04:00
nousbot-eng
7f78046e5d
fmt(js): npm run fix on merge (#66731)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-18 04:56:56 +00:00
UnathiCodex
9803b2fb89
fix(desktop): preserve numeric and display LaTeX (#66173) 2026-07-17 19:02:18 -04:00
David Metcalfe
e7a8b374b7
fix(desktop): expose Local / custom endpoint in Providers API-keys tab (#62818)
The onboarding overlay already contains a 'Local / custom endpoint' card
that writes model.provider:custom + base_url + api_key, but no reachable
Desktop GUI path opens it for a fresh add. The composer model pill falls
back to the gateway menu panel (Edit Models…), and Settings → Providers →
API keys is env-var-driven and never lists a custom endpoint — so users
following their instincts cannot add an OpenAI-compatible endpoint (Zyphra,
vLLM, Ollama, …) from the GUI.

Add a 'Local / custom endpoint' row to the API-keys tab that calls
startManualLocalEndpoint(), landing the overlay directly on the existing
custom-endpoint form. Reuses the tested onboarding flow; no new UI surface.

Regression test in providers-settings.test.tsx asserts the row renders and
opens the custom-endpoint flow.

Fixes #62817

Co-authored-by: David Metcalfe <80915+DavidMetcalfe@users.noreply.github.com>
2026-07-17 18:58:37 -04:00
HexLab
38233b61c2
fix(desktop): trust Windows system CAs for remote gateways (#66304)
* fix(desktop): trust Windows system CAs for remote gateways

Load Windows-trusted roots into Node's default TLS context before Desktop probes remote backends, while preserving bundled and extra CAs.

* test(desktop): cover Windows system CA installation

Verify existing trust roots survive the merge and that unsupported or unavailable stores fail open without changing TLS defaults.
2026-07-17 18:55:34 -04:00
nousbot-eng
d9ee342414
fmt(js): npm run fix on merge (#66527)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-17 21:02:05 +00:00
Teknium
597615ade4
fix(ci): make tests, workflows, and attribution reliable under load (#66373)
* feat(attribution): conflict-free contributor mappings via contributors/emails/ directory

The AUTHOR_MAP dict in scripts/release.py was a merge-conflict magnet:
every concurrent salvage PR appended entries to the same lines of the
same file, so parallel PRs re-conflicted on every merge to main.

New system: one file per email under contributors/emails/ — filename is
the commit-author email, first non-comment line is the GitHub login.
File additions never conflict, so any number of PRs can add mappings
concurrently.

- scripts/release.py: AUTHOR_MAP is now LEGACY_AUTHOR_MAP (frozen)
  merged with the directory at import time (directory wins). All
  existing consumers (resolve_author, contributor_audit.py) unchanged.
- scripts/add_contributor.py: idempotent CLI to add a mapping; refuses
  conflicting reassignments (incl. against the legacy map), validates
  email/login shapes.
- contributor-check.yml: attribution gate now accepts a mapping file OR
  a legacy entry; failure message prints the exact add_contributor
  command. Also auto-resolves bare <login>@users.noreply.github.com
  emails is intentionally NOT added (kept id+login form only, matching
  previous behavior).
- contributor_audit.py: guidance now points at add_contributor.py.
- tests/scripts/test_contributor_map.py: 12 tests covering loader,
  merge precedence, CLI idempotency/conflict/validation, subprocess E2E.

* feat(ci): one-shot per-file flake retry in the parallel test runner

A failing test FILE is re-run once in a fresh subprocess. Pass-on-retry
counts as green but is loudly reported in a '⚠ FLAKY' summary section
(with both attempts' output preserved) so the flake gets fixed instead
of eating a full-run rerun. Deterministic failures fail both attempts —
regressions cannot be laundered green.

- --file-retries N / HERMES_TEST_FILE_RETRIES (default 1, 0 disables)
- E2E verified: simulated first-run-fail flake goes green with banner;
  deterministic failure still exits 1; retries=0 restores old behavior.

This converts the dominant CI failure mode (one timing-sensitive test
flaking a 4600-test shard, requiring a manual 10-minute rerun and an
agent triage loop) into a self-healing retry that costs one file's
runtime.

* test(approval): loosen wall-clock perf bounds 0.15s -> 2.0s

These guard against catastrophic regex backtracking (seconds-to-minutes
class), but 0.15s is within scheduler-stall noise on loaded shared CI
runners — test_max_accepted_separator_free_input_is_fast failed a CI
shard this week on runner load alone. 2.0s still catches the regression
class with zero flake surface.

* fix(ci): job timeouts everywhere + retries on all network installs

Reliability pass over every workflow:
- timeout-minutes on all 21 jobs that lacked one (a hung job previously
  burned the 6-hour default runner budget)
- ./.github/actions/retry wrapped around every network-fetching install
  that lacked it: pip installs (deploy-site, skills-index), npm ci
  (deploy-site website, upload_to_pypi web + ui-tui), uv sync (docker
  test deps). Deterministic build steps (npm run build) deliberately
  NOT retried — split into separate steps so a real build failure fails
  fast instead of retrying 3x.

* docs(agents): document the file-retry flake policy

* fix(ci): curl retries on deploy hook + skills-index probe

* fix(ci): kill the remaining transient-failure classes in workflows + Dockerfile

From the workflow reliability audit:
- tests.yml: duration-cache restore had NO restore-keys while saves use
  run_id-suffixed keys — the cache never matched once, so LPT slicing
  always ran blind and unbalanced slices pushed heavy files toward the
  per-file timeout. One-line restore-keys fixes slice balancing.
- Label gates (lint ci-reviewed, supply-chain mcp-catalog-reviewed):
  'gh pr view || true' turned an API blip into 'label absent' → false
  BLOCKING failure. Now 3x retry, and API failure is reported as an API
  failure instead of a missing label.
- detect-changes action: compare API retried before failing open (was
  silently running all lanes on any blip).
- uv-lockfile-check: 'uv lock --check' resolves against PyPI — retried
  so registry blips don't read as 'lockfile stale'.
- docker.yml merge job: imagetools create retried (Docker Hub eventual
  consistency on just-pushed digests).
- Dockerfile: apt-get Acquire::Retries=3; s6-overlay ADDs converted to
  curl --retry 3 (ADD cannot retry; checksums still enforced); npm
  --fetch-retries=5; playwright chromium fetch retried 3x.
- Advisory artifact uploads (per-slice durations, ci-timings report)
  get continue-on-error so an artifact-service blip can't fail a green
  test slice.

* fix(tests): kill the two root-cause flakes — leaking pre-warm timer + env-dependent provider list

- test_tui_gateway_server.py: session.create / non-eager session.resume
  arm a 50ms threading.Timer (_schedule_agent_build) that outlives its
  test and fires into the NEXT test's _make_agent mock, racily
  corrupting captured state (the recurring session_resume shard
  failures). Replaced the per-test whack-a-mole stub with a module-wide
  autouse fixture; the 3 worker-lifecycle tests that genuinely need the
  deferred build opt back in via @pytest.mark.real_agent_prewarm (new
  marker in pyproject).
- test_api_key_providers.py: PROVIDER_ENV_VARS is now derived from the
  live PROVIDER_REGISTRY instead of a hand-list that had drifted
  (missing HF_TOKEN / DEEPINFRA_API_KEY) — resolve_provider('auto')
  tests failed on any machine with HF_TOKEN exported. E2E-verified with
  HF_TOKEN/DEEPINFRA_API_KEY set: 42/42 pass.

* test: de-flake 30 timing-sensitive test files for loaded CI runners

Root-cause fixes from the flake audit (session-DB mining + repo sweep):

Event-based sync instead of sleep-sync:
- title_generator: mock sets threading.Event, wait(10) replaces
  sleep(0.3) hoping the daemon thread got scheduled
- docker zombie_reaping / profile_gateway: poll-for-state helpers
  replace fixed 1-3s sleeps (s6 transitions + SIGCHLD reaping are async)
- process_registry tree test: select()-bounded readline replaces an
  unbounded blocking read (parent wedge now fails THIS test with a clear
  message instead of an opaque rc=124 file kill); SIGTERM grace 1s->2s
  (the 1s partition window mid-interpreter-startup is how a child PID
  escaped the live-system guard in CI)

Timeout raises (loaded 8-way-sliced runners see ~5s scheduling floors;
all of these complete in ms-to-1s when healthy so the raises cost
nothing on green runs):
- subprocess/thread waits <= 2s raised to 10-15s across mcp_tool,
  mcp_circuit_breaker, mcp_reconnect_retry_reset, mcp_parked_self_probe,
  mcp_cancelled_error_propagation, registry, clarify_gateway, interrupt,
  voice_cli_integration, docker_environment, session_store_lock_io,
  planned_stop_watcher, cli_interrupt_subagent, thread_scoped_output
  (joins now also assert not is_alive() so stragglers fail loudly)
- wall-clock discrimination ceilings loosened where the guarded hang is
  10x larger: local_background_child_hang 4s->10s, interrupt_cleanup
  setup 5s->20s + pgid-exit 30s->60s, mcp_stability grandchild spinup
  5s->15s, protocol/gil-starvation fast-handler 0.5s->2s,
  iso_certify_seam 1.5s->5s, wait_for_mcp_discovery 0.1s->1s
- narrow assertion windows widened: honcho first-turn wait 0.4..0.65 ->
  0.25..2.0 (property is bounded-not-hung, not an exact wall-clock);
  compression fork-lock TTL 1s->3s (12 refresh chances per lease);
  compression-lock expiry margins symmetric (ttl 0.05->0.5, sleep 1.0)
- telegram hung-DNS bound 1.0->1.4 (fake hang is 1.5s — must stay under)

* fix(tests): repair indentation from de-flake batch edit

* fix(tests): harden env isolation and replace remaining sleep-sync races

The full 42k-test run and complete npm check surfaced three more classes:

- Environment isolation: local ~/.honcho defaultHost and SSH_* variables
  leaked into Python/TUI tests. Pin the default Honcho host in the
  hermetic fixture, isolate the one fallback test from ~/.honcho, and
  blank SSH_* around terminalSetup tests. This flipped 20 false failures
  back to deterministic behavior on developer machines.
- Background-thread sleep-sync: Honcho async writer tests patched
  time.sleep globally, then busy-polled with that same mocked sleep. Under
  full-suite load the poller could starve the writer. Each test now waits
  on an Event emitted by the exact flush/retry transition; 30/30 passed
  under 15-way contention.
- Desktop streaming: the test slept 80ms and assumed a 500ms timer could
  not fire before its assertion. A loaded runner descheduled the test for
  >500ms and both chunks arrived. Producer controls now gate second-chunk
  and completion transitions explicitly.

Also make file-retry observability complete: a self-healed flaky file now
prints BOTH attempts' full output in the FLAKY summary. Two behavioral
runner tests prove pass-on-retry is green+loud+traceback-preserving, while
a deterministic failure remains red.

* refactor(ci): use gh bot pat, better retries

refactor(ci): use retry action for PR label fetch
the retry action now captures stdout as a step output, so it can serve
double duty: retry + output capture for commands like 'gh pr view' whose
result must be consumed by later steps.

Retry action gains:
- 'stdout' output (heredoc-delimited to preserve newlines)
- tee to temp file so stdout still streams to the job log
- step id 'retry' for output reference

Both lint.yml and supply-chain-audit.yml now use the retry action
directly with 'command: gh pr view ...' and read
steps.<id>.outputs.stdout.

ci: use AUTOFIX_BOT_PAT for all gh CLI / GitHub API auth

Replace secrets.GITHUB_TOKEN and github.token with
secrets.AUTOFIX_BOT_PAT across all workflows and composite actions
that use the gh CLI or GitHub API. The PAT has consistent permissions
across fork PRs (where GITHUB_TOKEN is read-only), avoids API rate
limit sharing with the default token, and is already used by
js-autofix.yml for the same reasons.

19 sites swapped across 9 files:
- lint.yml (3): label fetch, comment post/edit, comment update
- supply-chain-audit.yml (5): scan, critical comment, unbounded dep
  comment, label fetch, mcp-catalog comment
- lockfile-diff.yml (1): PR comment post/update
- skills-index-freshness.yml (1): issue creation on degraded probe
- skills-index.yml (2): index build, trigger deploy workflow
- upload_to_pypi.yml (2): release view poll, release upload
- ci.yml (1): timings report
- deploy-site.yml (2): skills index crawl
- detect-changes/action.yml (1): compare API call

---------

Co-authored-by: ethernet <arilotter@gmail.com>
2026-07-17 20:55:24 +00:00
nousbot-eng
8702e6a6cb
fmt(js): npm run fix on merge (#66505)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-17 20:27:12 +00:00
nousbot-eng
7f76fc040a
Merge pull request #64576 from joelbrilliant/fix/desktop-update-stream-output
fix(update): stream update child output to the live log (PYTHONUNBUFFERED)
2026-07-17 16:20:39 -04:00
ethernet
11d36232c0
fix(desktop): stop button sends interrupt to wrong session + stale events re-arm busy (#66485)
Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
2026-07-17 19:44:10 +00:00
Brooklyn Nicholson
9b8b054c2d perf: fast model picker + dialogs — config-load hot path, model.options off the reader thread, off-screen turns skip rendering
Third profiling round (after #66033 / #66347), targeting the composer
model picker and dialog opens (worktree dialog etc.), measured over CDP
on real 1000+-message sessions.

Backend — model.options took 4.8s cold / 1.8s warm per call, and the
desktop model pill/picker blocks on it every open:

- agent/credential_pool: _load_config_safe uses load_config_readonly().
  Every consumer only reads, and the per-call deepcopy was the dominant
  cost — list_authenticated_providers calls load_pool() per provider
  row, and each load_pool loaded (and deep-copied) the full config
  again via get_pool_strategy.
- hermes_cli/config: memoize ensure_hermes_home() per home path. It
  runs inside the config lock on EVERY load_config(), paying ~14
  mkdir/chmod syscalls per call. The fast path still re-checks that the
  home dir exists, so a deleted home is recreated as before; profile
  switches hit the new path and re-run. Tests cover both.
- tui_gateway/server: add model.options to _LONG_HANDLERS. It measured
  seconds inline on the WS reader thread — while it ran, prompt.submit
  and session.interrupt sat unread (same class as #21123).

Together: model.options RPC 4825/1842ms → 426/230ms (measured on the
live desktop backend); build_models_payload in isolation 6.2s → 0.97s
cold, 0.27s warm.

Desktop — every Radix dialog/popover open forced a whole-document style
recalc (Presence reads getComputedStyle on mount), which on a
1300-message transcript cost ~650-730ms per open (CPU profile:
getAnimationName 483ms self). The worktree dialog (⌘⇧B) paid it on
every single open:

- thread/list: content-visibility:auto + contain-intrinsic-size on the
  per-turn group wrappers. Off-screen turns now skip style recalc,
  layout, and paint entirely; never-rendered turns hold a placeholder
  height (auto: remembered real size once rendered) so scrollbar and
  anchoring stay stable. Verified over CDP: worktree dialog open 656-
  730ms → ~200ms on the same session; stick-to-bottom pin, scroll-to-
  top rendering, and sticky human bubbles all intact.

Also: profile-session-switch harness accepts CDP_HTTP (Chrome tends to
squat on 9222).

Verification:
- scripts/run_tests.sh: config, credential-pool, inventory,
  model-switch routing, tui_gateway protocol, profiles suites green
  (test_profiles has one pre-existing failure on main, unrelated);
  new tests for the ensure_hermes_home memo.
- apps/desktop: tsc clean, eslint/prettier clean, thread + session
  suites green (326 tests).
- E2E over CDP on the live app: numbers above, plus scroll/pin sanity.
2026-07-17 15:01:54 -04:00
nousbot-eng
bcea5371c8
fmt(js): npm run fix on merge (#66465)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-17 18:59:51 +00:00
nousbot-eng
29dac61d77
fmt(js): npm run fix on merge (#66460)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-17 18:53:03 +00:00
brooklyn!
cf52edbb59
Merge pull request #66454 from NousResearch/ethie/session-status-sync
refactor(desktop): derive working/attention session sets from $sessionStates
2026-07-17 14:46:23 -04:00
nousbot-eng
29e3983fa8
fmt(js): npm run fix on merge (#66457)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-17 18:44:45 +00:00
brooklyn!
9930c2b47f
Merge pull request #66034 from NousResearch/bb/review-store-tests
test(desktop): cover the review store
2026-07-17 14:37:55 -04:00
brooklyn!
3e7c563ddd
Merge pull request #66449 from NousResearch/audit/desktop-model-picker
fix(desktop): session-scope fast mode, surface profile ownership + pinned model override
2026-07-17 14:36:21 -04:00
brooklyn!
270486226c
Merge pull request #66347 from NousResearch/bb/profile-switch-prewarm
perf(desktop): pre-warm profile backends and gateway sockets on hover intent
2026-07-17 14:34:53 -04:00
ethernet
a75a8eda72 refactor(desktop): derive working/attention session sets from $sessionStates
$workingSessionIds and $attentionSessionIds were independently maintained
atoms that updateSessionState had to manually keep in sync with the session
cache (paired setSessionWorking/setSessionAttention calls, plus a rotation
special-case in ensureSessionState). Make them computed() projections of
$sessionStates instead, so the data flow is one-directional:
gateway event → cache → $sessionStates → computed views.

Transition side-effects (watchdog arm/disarm, settle grace, unread marker,
compression id rotation signal) move into handleTransition, fired from
publishSessionState by diffing previous vs next — one choke point instead
of per-callsite bookkeeping. The watchdog's force-clear reaches the cache
through setWatchdogClearFn rather than a listener set.

Also:
- clearAllSessionStates disarms all watchdog timers and drops settle-grace
  entries so a gateway switch can't leak stale timers or keep-set rows
- dropSessionState disarms the dropped runtime's watchdog timer
- watchdog tests now exercise the real timer→callback wiring instead of
  manually simulating the clear
2026-07-17 14:33:38 -04:00
Brooklyn Nicholson
ba542338ea fix(desktop): session-scope fast mode, surface profile ownership + pinned model override
Model-picker audit follow-through — closes the remaining pieces of the
"switch one session, switches everywhere / can't tell whose session this
is" report class:

- tui_gateway: `config.set key=fast` with a session no longer writes the
  global agent.service_tier to config.yaml (sibling of the earlier
  `reasoning` scoping fix). It pins create_service_tier_override
  ("priority" / "" for explicit normal) so lazy builds and rebuilds keep
  the choice; the desktop's per-model presets were rewriting the global
  tier on every model pick. Fast-support validation now checks a draft's
  picked model, and `config.get key=fast` reads the pre-build pin.
- desktop: owning-profile tag (initial chip + tooltip/aria label) on
  pinned rows and search results in the All-profiles sidebar, and on the
  chat header once a second profile exists (#66003).
- desktop: composer model pill shows a pin dot + tooltip when a manual
  sticky pick is overriding the Settings default for new chats (#62055).

Closes #66003. Addresses #62055.
2026-07-17 14:31:10 -04:00
nousbot-eng
75b300f13a
fmt(js): npm run fix on merge (#66445)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-17 18:26:36 +00:00
brooklyn!
2f00cca49a
feat(desktop): promote Fireworks AI to #2 in onboarding provider picker (#66432)
Mirror CANONICAL_PROVIDERS so Fireworks sits directly under Nous Portal
(always visible) ahead of OpenRouter across onboarding, Settings → Providers,
and the API-key catalog.
2026-07-17 14:19:57 -04:00
Brooklyn Nicholson
81a140266f perf(desktop): pre-warm opens the gateway socket too, not just the spawn
Answering the review question on the PR table — why a hovered-cold
switch still showed ~440ms click → WS open: getConnection-only
pre-warming left the WS connect chain to the click, and its microtask
continuation can only run after the click's fresh-draft React flush
(unmounting a large open transcript costs ~300-400ms of render work),
so the socket didn't even START connecting until the flush finished.

Add openGatewayForProfile: the same spawn + connect chain as a real
switch, minus activation — so the hover leaves the profile's socket
fully OPEN and the click's ensureGatewayForProfile just activates it
(no ws:new after the click at all; measured ws open at hover+136ms on
a warm backend). No scheduleReconnect on failure: a hover is
speculative, so a dead backend must not start a background retry loop
— the real switch owns retry and error UX. Pruning semantics are
unchanged: a hover-opened socket for an idle profile is dropped by the
next pruneSecondaryGateways recompute, which just returns the click to
the previous behavior.

Tests updated: pre-warm asserts openGatewayForProfile is called and
that activation (ensureGatewayForProfile) is NOT.
2026-07-17 13:11:05 -04:00
nousbot-eng
0bf44d557f
fmt(js): npm run fix on merge (#66348)
Some checks are pending
CI / Detect affected areas (push) Waiting to run
CI / Python tests (push) Blocked by required conditions
CI / Python lints (push) Blocked by required conditions
CI / JS & TS checks (push) Blocked by required conditions
CI / Docs Site (push) Blocked by required conditions
CI / Deny unrelated histories (push) Blocked by required conditions
CI / Check contributors (push) Blocked by required conditions
CI / Check uv.lock (push) Blocked by required conditions
CI / package-lock.json diff (push) Blocked by required conditions
CI / Lint Docker scripts (push) Blocked by required conditions
CI / Build&Test Docker image (push) Blocked by required conditions
CI / Supply-chain scan (push) Blocked by required conditions
CI / OSV scan (push) Waiting to run
CI / All required checks pass (push) Blocked by required conditions
CI / CI timing report (push) Blocked by required conditions
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Waiting to run
auto-fix lint issues & formatting / Apply patch (push) Blocked by required conditions
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-17 14:30:29 +00:00
Brooklyn Nicholson
e0390c0f70 perf(desktop): pre-warm profile pool backends on hover intent
A cold profile switch pays the full pool-backend spawn — Python boot,
port announcement, readiness probe, token adoption — before the
profile's gateway can even open. Measured with the new CDP harness
(scripts/measure-profile-switch.mjs, same family as
profile-session-switch.mjs): click → WS open is ~2.5-2.9s on a cold
profile, ~3-3.6s to a settled sidebar; a warm profile settles in
~0.5-0.8s. The pointer entering a profile square telegraphs the switch
hundreds of ms before the click lands, so start the spawn then.

- store/profile: prewarmProfileBackend(name) — fires the existing
  hermesDesktop.getConnection IPC, which is idempotent (ensureBackend
  returns the pooled connectionPromise), so the real switch joins the
  in-flight spawn instead of starting it. Skips the active gateway
  profile, throttles per profile (60s) so drive-by hovers can't spam
  spawn attempts, and swallows failures — error UX belongs to the real
  switch. No new IPC surface; the pool's existing LRU cap + idle reaper
  still bound resource use, and the LRU guard never evicts a
  keepalive-fresh backend for a hover spawn.
- sidebar/use-profile-prewarm: pointerenter/pointerleave handlers with
  a 120ms dwell so sweeping the pointer across the rail or a
  mixed-profile session list doesn't spawn a backend per element
  crossed.
- Wired at the three switch surfaces: rail ProfileSquare, the condensed
  ProfileDropdown items (extracted ProfileDropdownItem so each row owns
  its dwell timer), and SidebarSessionRow (covers cross-profile resumes
  from the all-profiles view; same-profile rows no-op inside the guard).

Measured E2E over CDP: synthetic hover on a cold profile square spawns
its backend in the background; the subsequent click settles in ~519ms
vs ~3.0-3.6s unhovered — and any hover shorter than the spawn still
shaves its dwell off the click's wait.

Verification: apps/desktop `npx tsc --noEmit` clean; full
`npx vitest run` 212 files / 1777 passed (new prewarm guard/throttle
tests in store/profile.test.ts); eslint + prettier clean.
2026-07-17 10:23:49 -04:00
Teknium
ebd737f4d9 fix(mcp): close hosted OAuth lifecycle gaps 2026-07-17 04:50:47 -07:00
Teknium
6045529724 fix(mcp): harden hosted OAuth across profiles and clients 2026-07-17 04:50:47 -07:00
Gille
531e5763e8
fix(desktop): hide Windows updater console during handoff (#66040)
* fix(desktop): hide Windows updater console (#56884)

* test(desktop): cover hidden updater handoffs behaviorally

---------

Co-authored-by: Kyssta <218078013+kyssta-exe@users.noreply.github.com>
2026-07-16 22:50:40 -04:00
brooklyn!
c856f36459
perf(desktop): kill the layout-thrash cascade on session switch (#66033)
Follow-up to #65890 (router transitions off) and #65898 (structural
compare + first-paint budget): profiling the switch path on real 1000+-
message sessions with a new CDP harness showed the remaining freeze is
NOT markdown rendering — it's a forced-reflow cascade from mount-time
layout reads interleaved with style writes across the transcript's
layout effects, plus the first-paint budget cut landing too late to
stop the full-budget commit.

Measured on the two largest local sessions (996 and 1363 messages),
main-thread longtask totals per switch: warm 2450ms -> 557ms and
1158ms -> 194ms; first paint 1690ms -> 444ms. Harness:
scripts/profile-session-switch.mjs (same CDP family as
profile-real-stream.mjs).

- use-resize-observer: drop the synchronous initial callback and ride
  the observer's spec-guaranteed first delivery instead (same frame,
  after layout, before paint). The sync call ran while the commit's
  layout was dirty, so every size read in a callback forced a full
  reflow — with one instance per user bubble (measureClamp read
  scrollHeight, then WROTE --human-msg-full, re-dirtying layout for the
  next bubble), the switch commit thrashed for over a second. Inside RO
  timing the same reads are free. Composer metrics (2x
  getBoundingClientRect + documentElement style writes) rides the same
  fix.
- Same class, same fix at the remaining call sites profiling surfaced:
  ExpandableBlock and TerminalOutput (dozens per tool-heavy transcript)
  now measure/pin via RO initial delivery; the tool-window and
  thinking-preview pins drop their sync pin() call; the thread
  timeline's initial active-tick compute joins its existing
  scroll-time rAF batching so back-to-back transcript updates coalesce.
- thread/list: cut the render budget in the RENDER phase (state-from-
  props adjustment) instead of the post-commit layout effect. The
  effect-time cut was too late — on a warm switch React first built and
  committed the full 300-part tree, then re-rendered at 60, then bumped
  back to 300, so the expensive commit still happened (and on a cold
  switch the bump rAF usually fired while the transcript was still
  empty, so the prefetched messages rendered at full budget anyway).
  The render-phase cut restarts the component before any child renders;
  a second trigger handles the cold path where messages land later
  under the same sessionKey.
- thread/list: backfill 60 -> 300 inside startTransition so the older
  turns' markdown+shiki render is interruptible background work instead
  of a synchronous freeze one frame after the switch paints. Functional
  Math.max so an urgent "Show earlier" click can't be rebased back down.
- composer focus: skip the rAF/timeout focus retries when the element
  is already focused — focus() runs the full focusing steps (forcing
  layout) even on the active element, ~585ms per switch on a large
  dirty DOM.
- Replace the tautological render-budget test (it re-declared the
  constants locally and asserted 60 < 300) with behavior tests of the
  now-exported buildGroups + firstVisibleGroupIndex.

Verification: apps/desktop `npx tsc --noEmit` clean; full
`npx vitest run` 210 files / 1763 passed; manual CDP check confirms the
deferred backfill commits the full transcript, stays pinned to bottom,
and "Show earlier" still pages.
2026-07-16 22:46:08 -04:00
Brooklyn Nicholson
f6edcb3d76 test(desktop): cover the review store (refresh, selection, mutations, ship flow) 2026-07-16 21:51:16 -04:00
Brooklyn Nicholson
dc58758a4b perf(desktop): kill the layout-thrash cascade on session switch
Follow-up to #65890 (router transitions off) and #65898 (structural
compare + first-paint budget): profiling the switch path on real 1000+-
message sessions with a new CDP harness showed the remaining freeze is
NOT markdown rendering — it's a forced-reflow cascade from mount-time
layout reads interleaved with style writes across the transcript's
layout effects, plus the first-paint budget cut landing too late to
stop the full-budget commit.

Measured on the two largest local sessions (996 and 1363 messages),
main-thread longtask totals per switch: warm 2450ms -> 557ms and
1158ms -> 194ms; first paint 1690ms -> 444ms. Harness:
scripts/profile-session-switch.mjs (same CDP family as
profile-real-stream.mjs).

- use-resize-observer: drop the synchronous initial callback and ride
  the observer's spec-guaranteed first delivery instead (same frame,
  after layout, before paint). The sync call ran while the commit's
  layout was dirty, so every size read in a callback forced a full
  reflow — with one instance per user bubble (measureClamp read
  scrollHeight, then WROTE --human-msg-full, re-dirtying layout for the
  next bubble), the switch commit thrashed for over a second. Inside RO
  timing the same reads are free. Composer metrics (2x
  getBoundingClientRect + documentElement style writes) rides the same
  fix.
- Same class, same fix at the remaining call sites profiling surfaced:
  ExpandableBlock and TerminalOutput (dozens per tool-heavy transcript)
  now measure/pin via RO initial delivery; the tool-window and
  thinking-preview pins drop their sync pin() call; the thread
  timeline's initial active-tick compute joins its existing
  scroll-time rAF batching so back-to-back transcript updates coalesce.
- thread/list: cut the render budget in the RENDER phase (state-from-
  props adjustment) instead of the post-commit layout effect. The
  effect-time cut was too late — on a warm switch React first built and
  committed the full 300-part tree, then re-rendered at 60, then bumped
  back to 300, so the expensive commit still happened (and on a cold
  switch the bump rAF usually fired while the transcript was still
  empty, so the prefetched messages rendered at full budget anyway).
  The render-phase cut restarts the component before any child renders;
  a second trigger handles the cold path where messages land later
  under the same sessionKey.
- thread/list: backfill 60 -> 300 inside startTransition so the older
  turns' markdown+shiki render is interruptible background work instead
  of a synchronous freeze one frame after the switch paints. Functional
  Math.max so an urgent "Show earlier" click can't be rebased back down.
- composer focus: skip the rAF/timeout focus retries when the element
  is already focused — focus() runs the full focusing steps (forcing
  layout) even on the active element, ~585ms per switch on a large
  dirty DOM.
- Replace the tautological render-budget test (it re-declared the
  constants locally and asserted 60 < 300) with behavior tests of the
  now-exported buildGroups + firstVisibleGroupIndex.

Verification: apps/desktop `npx tsc --noEmit` clean; full
`npx vitest run` 210 files / 1763 passed; manual CDP check confirms the
deferred backfill commits the full transcript, stays pinned to bottom,
and "Show earlier" still pages.
2026-07-16 21:48:39 -04:00
nousbot-eng
56e2ba5e79
fmt(js): npm run fix on merge (#66013)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-17 00:58:06 +00:00
nousbot-eng
36bf3c2673
fmt(js): npm run fix on merge (#66010)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-17 00:51:29 +00:00
HexLab
3f199f5c51
fix(desktop): don't latch remote backend boot failures so remote gateway reconnect recovers (#65756) 2026-07-16 20:45:43 -04:00
brooklyn!
432fca55a7
fix(desktop): drain queued prompts for background sessions (#66001)
Co-authored-by: Jakub Wolniewicz <4850809+frizikk@users.noreply.github.com>
2026-07-16 20:39:55 -04:00
nousbot-eng
10b6d1a910
fmt(js): npm run fix on merge (#65986)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-16 23:19:03 +00:00
ethernet
39a93dc633
fix(desktop): follow compression's stored-id rotation to prevent thread reload (#65984)
Auto-compression ends the SessionDB session and forks a continuation,
rotating the stored session id. The gateway emits `session.info` with
the new `stored_session_id`, and the desktop's cache entry was updated
via `ensureSessionState` — but the URL route and `$selectedStoredSessionId`
never followed the rotation.

On the next send, `getRuntimeIdForStoredSession(oldStoredId)` returned
null (the cache entry's `storedSessionId` no longer matched the old id),
so `routedSessionNeedsResume` evaluated true, triggering a full
`session.resume` + REST transcript prefetch — the whole thread reloaded.

Fix: a new `$activeSessionStoredId` atom is set in `ensureSessionState`
when the active session's stored id changes. A `useEffect` in
`use-session-actions` subscribes to it and re-anchors the route +
selection (`setSelectedStoredSessionId` + `navigate(replace: true)`),
and cleans up the stale stored→runtime mapping.

`replace: true` because it's the same conversation — compression is
transparent to the user, so back-button stays correct.
2026-07-16 23:12:55 +00:00
nousbot-eng
75467998f9
fmt(js): npm run fix on merge (#65974)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-16 22:32:53 +00:00
ethernet
f08b1f3445
feat(desktop): button tooltip keybind hints + keybinds settings tab + unified worktree dialog (#65204)
* feat(desktop): add useKeybindHint hook and TipKeybindLabel

Add a shared hook that reads the current keybind combo for an action
id from the $bindings store (rebindable) or KEYBIND_READONLY (fixed),
returning a formatted string or null when unbound.

Add TipKeybindLabel — a convenience component that auto-reads both
its label (from i18n) and keybind combo from the action registry.
Pass only actionId for the common case; pass text to override when
the tooltip is context-dependent.

* fix(desktop): replace native title= on buttons with themed Tip

Migrate all <button>/<Button> elements using the native HTML title=
attribute to the instant, themed <Tip> component. Native tooltips are
unstyled, delayed (~500ms OS default), and visually inconsistent with
the app's instant themed tooltips.

Also adds <Tip> wrappers to icon-only buttons that were missing
tooltips entirely (dialog close, search clear, overlay close,
master-detail pane controls, keybind panel rebind/reset buttons).

Adds an enforcement test (no-native-title.test.ts) that scans all
.tsx files for <button>/<Button> with title= and fails if any are
found. Updates DESIGN.md with the icon-only button tooltip rule and
keybind hint guidance.

* feat(desktop): wire keybind hints into button tooltips

Add actionId to TitlebarTool, StatusbarItem, and SidebarNavItem so
their tooltips show the current keybind combo via TipKeybindLabel.
Fix the hardcoded NEW_SESSION_KBD in the sidebar to read from
$bindings so it stays live on rebind.

Wired surfaces:
- Titlebar: sidebar toggle, flip panes, keybinds, settings
- Statusbar: terminal toggle (view.showTerminal)
- Status stack: open agents button (nav.agents)
- Sidebar nav: new session, skills, messaging, artifacts

* feat(desktop): move keybind panel to settings tab with search filter

Move the keyboard shortcuts panel from a Radix Dialog into a proper
Settings tab (/settings?tab=keybinds). The ⌘/ shortcut and titlebar
keyboard button now navigate to this settings tab instead of toggling
a dialog. Adds a search filter to filter shortcuts by label.

- New: src/app/settings/keybind-settings.tsx (extracted from keybind-panel.tsx)
- Delete: src/app/shell/keybind-panel.tsx (dialog wrapper removed)
- Remove: $keybindPanelOpen atom and toggle/open/close functions
- Add: IconKeyboard to lib/icons.ts
- i18n: keybinds.search + settings.nav.keybinds (en, zh, zh-hant, ja)

* refactor(desktop): unify worktree dialog into shared WorktreeDialog

Extract the worktree creation dialog from StartWorkButton (sidebar) into a
shared WorktreeDialog component. Both the sidebar's StartWorkButton and the
composer's CodingStatusRow now use the same dialog, eliminating the duplicated
UI.

The shared dialog keeps the sidebar version's full feature set:
- BaseBranchPicker (filterable base branch combobox)
- Convert mode (check out an existing branch into a worktree)
- Sanitized branch name input

The coding row passes repoPath (from cwd) and onOpenWorktree (which carries
the composer draft to the new session) so the unified dialog works everywhere
there's a repo, not just inside an entered project.
2026-07-16 18:26:21 -04:00
nousbot-eng
f1315ae91e
fmt(js): npm run fix on merge (#65971)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-16 22:26:09 +00:00
ethernet
0f05aaa2bf
perf(desktop): make session switching snappy on large transcripts (#65898)
Switching between chat sessions in the desktop app froze for up to ~1–2s on
large transcripts. Profiling the switch path surfaced three main-thread
blockers, fixed here minimally and without changing behavior.

1. JSON.stringify deep-compare (worst case). chatMessagesEquivalent compared
   message parts with JSON.stringify(a) === JSON.stringify(b) on every switch.
   On image-/large-blob-bearing transcripts this serialized every part twice
   and cost well over a second. Replaced with a structural compare that never
   stringifies: array-level identity fast-path, per-part reference fast-path,
   then type-aware field comparison. The compare's only consumer asks "did the
   transcript change, should I setMessages?", so it is deliberately
   conservative — a false-negative just causes one extra idempotent
   setMessages, while a false-positive (the unsafe direction) is avoided.

2. Scroll-settle loop. thread-list ran a requestAnimationFrame settle loop up
   to 90 frames (or 5 stable frames) on every sessionKey change, each frame
   forcing a synchronous layout read + write — racing the markdown paint for
   up to ~1.5s. A normal synchronous switch stabilizes within a couple
   frames, so the ceiling is now 2 stable frames / 15 max.

3. Synchronous first paint of up to 300 parts. On switch, thread-list reset
   the render budget to the full RENDER_BUDGET=300, so up to 300 parts went
   through markdown + shiki syntax-highlighting synchronously on the switch
   commit. It now paints a small FIRST_PAINT_BUDGET=60 first, then bumps to
   the full 300 in a requestAnimationFrame after the first commit.

Salvaged from PR #49807 by professorpalmer — re-applied to the restructured
file layout (use-session-actions/utils.ts, thread/list.tsx) and tests merged
into the existing utils.test.ts.

Co-authored-by: Cary Palmer <professorpalmer@users.noreply.github.com>
2026-07-16 18:19:41 -04:00