Commit graph

586 commits

Author SHA1 Message Date
Brooklyn Nicholson
68680db10d feat(desktop): add Codex-style review pane 2026-06-25 16:40:27 -05:00
Brooklyn Nicholson
7a7f9a5b3d feat(desktop): add composer coding rail and worktree flow 2026-06-25 16:40:27 -05:00
Brooklyn Nicholson
488ae376db feat(desktop): render backend-authoritative projects sidebar 2026-06-25 16:40:27 -05:00
Brooklyn Nicholson
74352a1e61 feat(desktop): add project and coding stores 2026-06-25 16:40:27 -05:00
Brooklyn Nicholson
344415892f feat(desktop): add shared project UI primitives 2026-06-25 16:40:27 -05:00
Brooklyn Nicholson
e2b8018729 feat(desktop): add git worktree and review IPC 2026-06-25 16:40:27 -05:00
Brooklyn Nicholson
4cdd1a3230 feat(sessions): record git workspace metadata 2026-06-25 16:40:26 -05:00
brooklyn!
c4ba4770eb
Merge pull request #52704 from NousResearch/bb/desktop-root-boundary-recover
fix(desktop): recover root error boundary from transient render races (salvage #41787)
2026-06-25 16:17:18 -05:00
Brooklyn Nicholson
2e3efce66e fix(desktop): recover the root error boundary from transient render races
A stale-index render race in assistant-ui (a just-shrunk thread rendered
at an old message index during a session switch / teardown) throws
errors like "tapClientLookup: Index N out of bounds", "Cannot read
properties of undefined (reading 'type')", or "Tried to unmount a fiber
that is already unmounted". These bubble to the root ErrorBoundary and
latch the WHOLE desktop app on the "Reload window" fallback even though
the next render against fresh state would be fine.

Teach the root boundary to treat that small set of known-transient
renderer errors as recoverable: log them and schedule a next-tick
reset() so React re-renders against current state instead of stranding
the user on the fallback.

Auto-recovery is BOUNDED -- at most MAX_RECOVERIES (3) attempts within a
5s window -- so a genuinely persistent error can't spin the boundary in
a reset -> throw -> reset loop; after the budget is spent the fallback
is left up for the user. Manual retry (the button) resets the budget.
Only the root boundary auto-recovers; scoped boundaries keep their own
fallbacks, and unrecognized errors are never swallowed.

Tests: transient race recovers (fallback never sticks), a persistent
recoverable error stops at the cap and surfaces the fallback (proving
the loop is bounded), and neither a non-root boundary nor an
unrecognized root error auto-recovers.

Closes #41693. Supersedes #41787 by @izumi0uu, reimplemented with a
bounded recovery budget so a non-transient error can't loop forever.

Co-authored-by: izumi0uu <izumi0uu@gmail.com>
2026-06-25 16:15:20 -05:00
Brooklyn Nicholson
f7bf740640 fix(desktop): reject cross-wired runtime-id cache on session resume
resumeSession's warm-cache fast-path trusted the
storedSessionId -> runtimeId -> ClientSessionState mapping without
checking the cached state still BELONGS to the session being resumed.
A pooled profile backend that gets idle-reaped and respawned
(pruneSecondaryGateways) re-mints runtime ids, so a recycled id can
resolve to a live-but-DIFFERENT session's cache entry. The only
existing guard was a session.usage 404 -- that catches a fully-dead
runtime id, but a recycled id still 200s, so the fast-path happily
painted the wrong transcript under the current route (open chat A,
chat B loads).

Fold the belongs-to check into a single takeWarmCache() helper used at
BOTH cache reads -- the early transcript-keep decision and the fast-path
itself -- so a cross-wired entry can't even briefly flash a stale
transcript before the full resume repaints. On a mismatch the helper
purges both stale map entries and reports a miss, falling through to a
full resume that rebinds a correct runtime id. The full-resume path
already guards its final paint with isCurrentResume(), so only the
cached fast-path was missing the belongs-to check.

Pre-existing bug from the initial desktop app (#20059); not introduced
by the session-switch perf work (#49807), which left these lines
untouched.

Tests: two cases in use-session-actions.test.tsx driven through a
harness that owns the two cache maps -- a cross-wired mapping is
rejected + purged (the bug), and a correctly-wired cache still serves
from memory with no needless refetch (no perf regression).

Supersedes #50464 by @professorpalmer, reimplemented to also guard the
early transcript-keep read (whole-class fix, not just the fast-path).

Co-authored-by: professorpalmer <professorpalmer@users.noreply.github.com>
2026-06-25 16:11:18 -05:00
Teknium
c6575df927
feat(moa): expose MoA presets as selectable virtual models (#46081)
* feat(moa): expose MoA presets as selectable virtual models

Reconstructed onto current main (PR #46081's base had diverged with no common
ancestor, marking the PR dirty so CI never dispatched). MoA is now a virtual
provider: each named preset is a selectable model under provider 'moa', and the
preset's aggregator is the acting model that answers and calls tools.

Reference models fan out in parallel via a bounded ThreadPoolExecutor (the same
batch pattern delegate_task uses) — all references dispatched at once, collected
when every one finishes, then handed to the aggregator. Output order is
preserved, failures and the MoA-recursion guard stay isolated per reference.

- Removed the old mixture_of_agents model tool and moa toolset.
- Added moa as a virtual provider in the provider/model inventory.
- /moa is shortcut behavior over model selection (default preset / named preset
  / one-shot prompt).
- Dashboard + Desktop manage named presets; presets appear in model pickers.
- Parallel reference fan-out in agent/moa_loop.py with regression test.

* fix(moa): thread moa_config through _run_agent to _run_agent_inner

The reconstructed gateway MoA wiring declared moa_config on _run_agent (the
profile-scoping wrapper) and used it inside _run_agent_inner, but the wrapper
never forwarded it — _run_agent_inner had no such parameter, so the runtime hit
NameError: name 'moa_config' is not defined on the compression-failure session
sync path. Add moa_config to _run_agent_inner's signature and forward it from
both wrapper call sites (multiplex and non-multiplex). Caught by
tests/gateway/test_compression_failure_session_sync.py on CI shard test(4).

* fix(moa): classify moa as a virtual provider in the catalog

The moa virtual provider has no PROVIDER_REGISTRY/ProviderProfile entry, so
provider_catalog() fell through to the default auth_type="api_key" with no
env vars — tripping two catalog invariants:
  - test_provider_catalog: api_key providers must expose a credential env var
  - test_provider_parity: every hermes-model provider must be desktop-configurable

moa already declares auth_type="virtual" in HERMES_OVERLAYS; consult that
overlay as an auth_type fallback so the catalog reports moa as virtual (no real
credential, no network endpoint). Exempt virtual providers from the desktop
parity union check the same way 'custom' is exempt — derived from the catalog,
not a hardcoded slug, so future virtual providers are covered too.
2026-06-25 13:52:06 -07:00
brooklyn!
edf35918be
Merge pull request #52620 from NousResearch/bb/desktop-session-switch-perf 2026-06-25 14:19:59 -05:00
David Metcalfe
da73223f4a fix(desktop): show statusbar item tooltips on hover
Statusbar items declared a 'title' string (e.g. YOLO, gateway health,
agents, cron, version, context usage) that was populated by
use-statusbar-items.tsx but never forwarded to the rendered DOM in
StatusbarControls — so every statusbar button/menu/text/link had no
hover hint.

Wrap the four render branches (menu trigger, text, link, action) in
the existing 'Tip' component from components/ui/tooltip.tsx. Tip is
self-contained (carries its own Provider), instant (delayDuration=0),
themed (bg-foreground/text-background, auto-inverts per theme), and
already in use elsewhere in the desktop shell. Renders the child
untouched when label is falsy, so items without a title stay
zero-cost.
2026-06-25 12:11:17 -07:00
Brooklyn Nicholson
1ca1f9f2c7 refactor(tui_gateway): DRY the deferred-session paths
Collapse the duplicated cold-resume / lazy-watch / create scaffolding into
shared helpers: _deferred_session_record (the live-session dict minus the
agent), _lazy_resume_info (the not-yet-built session.info), _claim_or_reuse_live
(lock + double-checked register-or-reuse), and _schedule_agent_build (the
pre-warm timer). Net -12 lines, three copies of the ~30-key session dict and
the lazy-info block down to one each. No behavior change.
2026-06-25 14:03:03 -05:00
Brooklyn Nicholson
3bf00e459a perf(desktop): make deferred resume the default, not an opt-in flag
Per review: gating the faster path behind a `defer_build` flag that the
only caller always sends is pointless. Flip it — `session.resume` now
defers the agent build by default for every caller (desktop + Ink TUI);
a caller that needs the agent built synchronously passes `eager_build:
true` (used by the build-race test). The desktop no longer sends a flag.

While verifying the flip, fixed two real parity gaps the deferred path
had vs the old eager (`_init_session`) path:

- `_enable_gateway_prompts()` was never called on a deferred resume, so
  approvals/clarify wouldn't route through the gateway prompt callbacks.
- `_start_agent_build` never wired `background_review_callback` /
  `memory_notifications`, so a deferred-built session's self-improvement
  "💾 …" summary leaked to stdout instead of rendering in-transcript.
  Wiring it there also fixes it for `session.create` sessions, which
  build through the same path.

ACP is unaffected (it uses its own session_manager, not this RPC); the
Ink TUI already consumes the same lazy `info` shape from session.create
and upgrades on the later `session.info` event.
2026-06-25 14:03:03 -05:00
Brooklyn Nicholson
c4c590e4a1 perf(desktop): make session switching fast under load
Switching sessions in the desktop app could freeze the whole UI for
several seconds on heavy, tool-rich chats. Root causes and fixes:

- Cold `session.resume` built the AIAgent (MCP discovery, prompt/skill
  build) *before* returning, and the desktop awaits that RPC before it
  paints — so the entire switch blocked on the build. Add an opt-in
  `defer_build` resume path (the contract `session.create` already uses):
  return the full display transcript immediately, register an upgradable
  live session, and pre-warm the agent on a short timer. The persisted
  runtime identity (model/provider/base_url/api_mode/reasoning/tier) is
  restored on the deferred build so it can't drop the provider.

- Nothing bounded how many in-memory agents accumulate; a user who
  reconnects often piled up detached sessions for the full 6h TTL. Add a
  soft LRU cap (`max_live_sessions`, default 16) that evicts the
  least-recently-active DETACHED sessions (no live client) — never a
  running, awaiting-input, mid-build, or live-transport one. Reopening
  re-resumes from disk.

- On the prefetch-hit cold-resume path, skip rebuilding a throwaway
  merged-message array (and its 1000-entry Map) when the prefetch already
  painted the exact transcript; the downstream sameMessageList guard
  already drops the publish, so it was pure main-thread cost.

The desktop opts into `defer_build` for every non-watch cold resume; the
eager path stays for CLI/TUI and existing callers.
2026-06-25 14:03:03 -05:00
Brooklyn Nicholson
6b3ea2cea6 refactor(pets): tighten remix comments and confirm handler 2026-06-25 01:10:56 -05:00
Brooklyn Nicholson
5196575d40 feat(pets): remix a draft into a fresh round
Add a hover/focus "Remix" action on each completed draft card in the
generation grid. It re-runs generation with the chosen draft fed back in
as the reference image, keeping the same prompt and staying on step 2 so
the user can explore variations without starting over.

Because regenerating is slow and replaces the current drafts, the first
remix shows a one-time confirmation; the acknowledgement is persisted so
subsequent remixes fire immediately.
2026-06-25 01:09:19 -05:00
Brooklyn Nicholson
f3d6d9bbd3 fix(ui): share compact tool previews across clients
Move terminal/execute_code/read_file preview compaction into agent.display so CLI, gateway, and Ink TUI all inherit the same labels that desktop introduced in #52321.

The shared preview keeps raw args intact while trimming display-only shell plumbing (`cd`, pipe tails, banner/status echoes) and read_file line ranges. Desktop now prefers backend `context` for live rows and keeps its TypeScript fallback only for hydrated history.
2026-06-25 00:47:14 -05:00
Brooklyn Nicholson
25c31cab62 fix(pets): soften step-1 ETA copy to "several minutes"
The fixed "up to 5 minutes" wording undersells the slow quality-first path
(OpenAI image via OpenRouter), where a full hatch can run far longer. Use an
open-ended "several minutes" instead so the banner stays honest across the
fast and slow providers.
2026-06-25 00:35:54 -05:00
Brooklyn Nicholson
7078d9d1e2 fix(pets): raise generation timeouts for the slow quality-first model path
The quality-first default (OpenAI image via OpenRouter) is slow, and a full
hatch fans out ~8 rows with up to 3 retries each (300s/call) across 2 parallel
waves, so the absolute backend worst case is ~30 min. The old ceilings fired
mid-run:

- per-image HTTP call: 180s -> 300s (a single cold row can exceed 3 min)
- drafts RPC: 240s -> 420s (single wave, no retries — 7 min is ample)
- hatch RPC: 420s -> 1hr (sits above the ~30 min backend worst case)

The hatch ceiling is intentionally well above the realistic max so the frontend
never throws "request timed out" before the backend has exhausted its own
retries. The background-resumable notification path remains the real UX safety
net — the user can close the modal and get pinged on completion.
2026-06-25 00:34:52 -05:00
Brooklyn Nicholson
41f302fa73 fix(desktop): compact tool row titles
Make completed desktop tool rows read like useful activity labels instead of raw plumbing: terminal rows use a dispatch-style shell summarizer for agent wrappers, and read_file rows keep the action plus filename and requested line range.

The shell cleanup follows condensed-milk-pi's shape: split command compounds on real separators, strip pipe tails inside each segment, clean redirects/env prefixes, then classify setup/banner/status segments. Multi-command probes render as `first command + N commands`; the full command remains available in copy/detail.

Read rows now render as `Read package.json` or `Read main.ts L25-34`, using requested positive offset/limit and returned line numbers only as fallback for negative/unknown offsets.
2026-06-25 00:01:11 -05:00
brooklyn!
0c442fa1d3
Merge pull request #52303 from NousResearch/bb/pets-gen-qa
feat(pets): quality-first OpenRouter chain, stronger atlas gates, global pet-gen notifications
2026-06-24 23:16:40 -05:00
Brooklyn Nicholson
e92b5c6af8 feat(pets): quality-first OpenRouter model chain + stronger atlas gates + global pet-gen notifications
OpenRouter/Nous image gen now runs a quality-first model chain by default:
attempt the highest-fidelity OpenAI image model first, then fall back to
Gemini 3 Pro Image when it's access-gated/unavailable/times out. An explicit
OPENROUTER_IMAGE_MODEL / config model override pins one model with no fallback.

Atlas validation rejects malformed model output instead of shipping it: adds a
per-state collapse guard (a single sliver/fragment row no longer passes because
other rows are healthy), on top of the existing postage-stamp + multi-pose
checks.

Desktop: pet-gen native notifications are now "global" (not tied to a chat
session), so a background generation started from the command center fires an
OS notification when the user is away even with no active session. Adds a
neutral "This can take up to 5 minutes." banner on step 1, and lets the
provider picker auto-size.

Tests updated/added for the OpenRouter fallback chain, the collapse guard, and
the global notification path.
2026-06-24 23:11:21 -05:00
Brooklyn Nicholson
281b333cc5 test(desktop): cover localized tool title shimmer 2026-06-24 21:59:41 -05:00
Brooklyn Nicholson
f2c45e2c81 fix(desktop): limit pending tool shimmer to action verb
Localize tool titles and split pending rows so only the action segment
shimmers — paths, commands, and URLs stay static.
2026-06-24 21:59:41 -05:00
brooklyn!
cbe5c5689f
perf(desktop): bound tool-result rendering so big /learn runs don't freeze (#52273)
ToolFallback rebuilt the `part` wrapper every render, defeating the
buildToolView memo and re-running a full JSON.stringify of the result on
every ~33ms stream delta. A /learn over a large directory (many ~100KB
tool results) saturated the renderer main thread (hang/throttle) and
spiked memory until it OOMd (crash).

- Re-derive a stable `part` from the referentially-stable args/result so
  the view/copy memos hold across deltas.
- Clamp every inline-painted payload (detail, stdout/stderr, rawResult,
  technical trace) to MAX_TOOL_RENDER_CHARS; the row's Copy button still
  reads the uncapped view.detail for the full output.
2026-06-25 02:52:51 +00:00
xxxigm
4aeaba6922 test(desktop): cover undefined/null attachment holes in ref helpers
Regression for the refText crash: attachmentDisplayText and
optimisticAttachmentRef must return null (not throw) when handed an
undefined/null attachment hole, so the submit path can't reproduce
"Cannot read properties of undefined (reading 'refText')".
2026-06-24 18:22:01 -07:00
xxxigm
7e2db0a140 fix(desktop): stop refText crash on undefined composer attachment holes
A session switch or draft restore can leave undefined/null holes in the
composer attachments array. AttachmentList was guarded against this in
#49624, but the sibling submit path was not: submitPromptText maps the
same array through attachmentDisplayText/optimisticAttachmentRef and
buildContextText (a.kind / a.label / a.refText), so a hole threw
"Cannot read properties of undefined (reading 'refText')" — an uncaught
renderer error that blanks the chat pane and shows "Desktop app link
offline".

Close the whole bug class:
- attachmentDisplayText / optimisticAttachmentRef no-op on a falsy
  attachment (shared chokepoint, also protects thread.tsx drop handler).
- submitPromptText filters falsy entries from the source array, and
  buildContextText filters its (possibly post-sync) input before reading
  fields.
2026-06-24 18:22:01 -07:00
Gille
284be6cc24
Merge pull request #52210 from helix4u/fix/desktop-update-progress-visibility
fix(desktop): surface update progress lines
2026-06-24 19:45:05 -05:00
brooklyn!
7157b213f5
Merge pull request #47959 from NousResearch/bb/pets-gen
Pet generation: frame-perfect hatch flow, backend picker, CPU-safe chroma, and CI-hardening
2026-06-24 19:41:34 -05:00
brooklyn!
153ad79524
Merge pull request #52201 from NousResearch/bb/desktop-shallow-update-count
fix(desktop): don't report a bogus update count for a shallow checkout
2026-06-24 19:34:02 -05:00
Brooklyn Nicholson
2ea94c6c45 fix(pets): make inline generate cancel discard draft flow
Wire the sparkle generate button's cancel action to the same discard/reset path as step-2 cancel so abort semantics are consistent and always return to step 1 while retaining the prompt input.
2026-06-24 19:33:33 -05:00
brooklyn!
d635a6d507
Merge pull request #52208 from NousResearch/bb/desktop-update-steps
fix(desktop): stop the update overlay looking frozen while it works
2026-06-24 19:29:02 -05:00
Brooklyn Nicholson
9a4600c5fb fix(desktop): stop the update overlay looking frozen while it works
Two ways the update overlay read as stuck even though the update was
streaming progress underneath:

- In-app (macOS/Linux) UpdatesOverlay: runStreamedUpdate forwards every
  stdout line as a progress event with percent: null, and ingestProgress
  wrote that straight through — clobbering the milestone percents (10/60)
  so the bar fell back to indeterminate on every log line. Keep the last
  percent when a line carries null.

- Staged install/update overlay: the bar is completedCount / totalCount,
  which counts only *finished* stages, so a long first stage pinned it at
  "0 of 2" / 0% until the stage ended. Count the running stage as half a
  unit so the bar advances during the stage (the per-stage spinner already
  shows which step is live).

Both are display-only; no stage/event semantics change. (The Windows
hermes-setup Tauri progress UI in apps/bootstrap-installer has the same
counter-only-on-completion logic — parity follow-up.)
2026-06-24 19:20:38 -05:00
Brooklyn Nicholson
65b13e9dbc fix(desktop): route gateway restart / status / update to the active profile
restartGateway, getActionStatus, getStatus, updateHermes and
checkHermesUpdate all hit window.hermesDesktop.api WITHOUT spreading
profileScoped() — unlike their siblings (getModelInfo, setModelAssignment,
grantComputerUsePermissions). _apiProfile tracks the active gateway
profile, and the Electron proxy uses request.profile to pick which pooled
/ remote backend serves the call.

So for a multi-profile or global-remote user, the System-panel "Restart
gateway" (and its status poll, plus Update / status reads) targeted the
primary/default backend instead of the one they're on: the restart hit
the wrong gateway and the poll never saw the action → it looked like
restart silently failed. Single-profile users are unaffected
(profileScoped() returns {} when no profile is active).

Add ...profileScoped() to the five backend-action helpers so they follow
the active profile like the rest of the API surface.
2026-06-24 19:16:26 -05:00
briandevans
cb6edbf448 fix(desktop): skip the rev-list count when it is discarded anyway
checkUpdates() ran `git rev-list HEAD..origin/<branch> --count`
unconditionally in the parallel probe batch, even on the shallow +
no-merge-base path where resolveBehindCount() ignores the result and
falls back to a SHA compare. In the #51922 failure mode that count walks
the entire remote ancestry (thousands of commits), so the work was pure
latency on every update check for the exact case the fix targets.

Split the probes into two phases: resolve --is-shallow-repository and
merge-base first, then run rev-list --count only when shouldCountCommits
says the number is meaningful (full clone, or shallow-with-merge-base).
The shallow/no-merge-base SHA fallback is preserved unchanged.
2026-06-24 19:12:09 -05:00
briandevans
a6485bddb8 fix(desktop): don't report a bogus update count for a shallow checkout
The desktop installer clones with `--depth 1`, so a public install's local
history often shares no merge-base with the freshly fetched origin tip. In
that state `git rev-list HEAD..origin/<branch> --count` enumerates the
entire remote ancestry and returns a meaningless huge number, surfacing as
e.g. "v0.17.0 (+12104)" in the update indicator (#51922).

The official-SSH branch of checkUpdates() already sidesteps this by reporting
a binary up-to-date check (`behind: currentSha === targetSha ? 0 : 1`), and
hermes_cli/banner.py guards the identical class for the CLI banner. The
passive desktop count path was the one place the shallow guard was missing.

Detect shallow + no-merge-base up front and fall back to the same SHA-based
binary check; full clones (developers / Docker dev images) keep the exact
count path unchanged. The resolution logic lives in a pure update-count.cjs
helper so it is unit-testable without booting Electron.
2026-06-24 19:12:09 -05:00
Brooklyn Nicholson
1fe013ee16 feat(pets): polish generate flow and reduce hatch CPU pressure
Ship the final pet-generation UX polish (provider picker behavior, step-2 cancel flow, banner integration, and visual consistency) and make saturated-chroma background removal C-op driven so hatch processing no longer hammers the machine during long runs.
2026-06-24 19:08:06 -05:00
brooklyn!
a378b1e980
Merge pull request #52192 from NousResearch/bb/session-loop-guard
fix(desktop): let the session watchdog heal a stuck "looping" turn
2026-06-24 19:03:43 -05:00
brooklyn!
4127332f15
Merge pull request #52189 from NousResearch/bb/desktop-offline
fix(desktop): give the gateway reconnect loop an escape hatch
2026-06-24 19:03:34 -05:00
brooklyn!
70650e82a3
Merge pull request #52187 from NousResearch/bb/desktop-voice
fix(desktop): wire Ctrl+B voice, declutter voice settings, stop endless TTS hang
2026-06-24 19:03:25 -05:00
Brooklyn Nicholson
93192059c9 fix(desktop): let the session watchdog heal a stuck "looping" turn
The 8-minute stream-silence watchdog only removed a stuck session from
$workingSessionIds (the sidebar dot). The composer's busy state lives in
the session-state cache and was never cleared, so a hung or looping turn
that never delivered its terminal event — including an old session
re-opened while the backend still reports it "running" — stayed wedged on
"Thinking" / Stop indefinitely.

Have the watchdog notify subscribers when it force-clears a session, and
subscribe from the session-state cache to also drop that session's
busy/awaiting/needsInput flags. updateSessionState re-syncs $busy when the
healed session is the one on screen, so the composer recovers instead of
spinning forever.

Frontend-only safety net; doesn't touch the turn lifecycle. The backend
root (a stale in-memory session["running"] surviving a dead turn thread
and re-arming busy on every resume) is a separate follow-up.
2026-06-24 18:36:17 -05:00
Brooklyn Nicholson
2a75c4a8cb fix(desktop): give the gateway reconnect loop an escape hatch
When a remote gateway dropped after a healthy boot (internet loss,
sleep/wake, VPS restart), use-gateway-boot retried with backoff forever
and never surfaced an error. The renderer sat behind the fullscreen
CONNECTING overlay with gatewayState non-open and boot.error null — no
way to reach Settings, sign in again, or switch to a local gateway. To
the user the app was simply broken on connection loss.

Raise a recoverable boot error once the reconnect loop crosses
RECONNECT_ESCALATE_AFTER (6 attempts, ≈45s), so the BootFailureOverlay
(Retry / Sign in / Use local gateway) replaces the dead-end CONNECTING
screen. The loop keeps retrying underneath; the next successful reconnect
(or a manual/wake-driven one) clears the error and dismisses the overlay.

This implements the contract already specified — but never wired up — in
use-gateway-boot.test.tsx (desktop vitest isn't in CI, so the failing
"FIX:" specs went unnoticed). All 4 hook tests + the 3 connecting-overlay
tests pass.
2026-06-24 18:32:29 -05:00
Brooklyn Nicholson
8d1706ae5c fix(desktop): wire Ctrl+B voice, declutter voice settings, stop endless TTS hang
Three voice-mode papercuts in the desktop app:

1. Ctrl+B did nothing. The docs + `voice.record_key` advertise Ctrl+B to
   talk, but the desktop never bound it (only ⌘B = sidebar existed). Add a
   rebindable `composer.voice` action that toggles the voice conversation,
   defaulting to ⌃B on macOS (distinct from ⌘B; off-macOS `ctrl` folds to
   the sidebar chord, so it ships unbound there to avoid stealing it). The
   global keybind reaches the composer through a new focus-bus event.

2. The Voice settings page rendered every provider's options at once (~30
   fields). Filter to the *selected* TTS/STT provider's sub-fields; STT
   provider fields hide when STT is off. Picking "edge" now shows just the
   Edge voice, making it obvious voice chat also needs STT enabled.

3. Voice mode could hang "speaking" forever. Free Edge TTS sometimes returns
   audio that never fires `playing`/`ended`/`error`, so the playback promise
   never settled. Add a stall watchdog (rearmed on each progress tick, so
   long speech is never cut off) that rejects a stuck stream, letting the
   loop recover with a clear error.
2026-06-24 18:26:14 -05:00
Brooklyn Nicholson
a268dfff0a fix(desktop): make Agents indicator match the Spawn-tree panel
The status-bar "Agents" item conflated three unrelated signals — running
subagents (aggregated across all sessions), in-flight session turns, and
failed background *system* actions (gateway restarts, toolset installs,
computer-use grants via $desktopActionTasks/preview restart) — yet
clicking it opens AgentsView, which renders only subagents. A failed
gateway restart therefore showed "Agents (1 Failed)" over an empty
"No live subagents" tree. AgentsView also filtered to the active session,
so a subagent running in a background session showed "Agents N running"
with nothing in the tree (the desync reported in #49808).

Unify the scope both surfaces speak:
- AgentsView aggregates subagents across every session (salvages #49819).
- The indicator's running/failed counts come from subagents only
  (aggregated), never background system actions — those keep their own
  surfaces in settings / command center.

So "Agents (N …)" now always points at a populated Spawn tree.

Supersedes #49819. Fixes #49808.
2026-06-24 18:16:14 -05:00
Brooklyn Nicholson
b674f7ba28 feat(pets): offer backend setup when generation is unavailable
When no reference-capable image backend is configured, generating a pet is
impossible — so instead of a dead prompt + post-hoc error, the overlay now
detects it up front and offers a way out:

- pet.generate.status RPC reports whether a reference-capable provider
  (OpenRouter / Nous Portal / OpenAI) is set up; the overlay probes it on
  open and swaps the prompt for a friendly setup card (paw, one-line copy,
  "Set up image generation" → /settings?tab=providers, key links).
- useRouteOverlayActive(): reusable hook so any portaled modal yields the
  screen to a full-screen route overlay (e.g. settings) and reappears —
  re-running its mount effects — on return, instead of closing. The probe
  re-runs on that remount, so adding a key flips the card to the prompt.
2026-06-24 14:10:19 -05:00
Brooklyn Nicholson
743985bf1e feat(pets): Pokédex generate UI — overlay, animated egg, hatch FX, manage
Dedicated generate modal (Cmd-K → Pets → Generate): prompt → 2×2 draft
grid → egg hatch → preview → adopt, width fits each phase.

- Reuses shared primitives (Button/Input/Dialog/Alert/GenerateButton);
  cards use selectableCardClass; only canvas + range stay raw.
- Animated creme pixel egg + PetStarShower hatch celebration (canvas).
- Live streamed drafts with a real Stop (AbortSignal); clean default name.
- Manage generated pets: badge + top ranking, rename (optimistic), safe
  delete (confirm + drop), export — in both the Cmd-K and Settings lists.
- pet-gallery routes every RPC through profile-scoped petRpc; i18n ×5.
2026-06-24 13:51:34 -05:00
Brooklyn Nicholson
aab49f6927 feat(pets): generation RPCs, non-blocking gallery + gateway plumbing
- pet.generate / pet.hatch (parallel rows, off the reader thread) +
  cooperative pet.cancel; pet.export / pet.rename.
- pet.gallery localOnly fast path + background manifest prefetch so the
  picker never blocks on petdex; rename follows the active-pet config.
- gateway request gains optional timeout + AbortSignal for real Stop.
2026-06-24 13:48:38 -05:00
Brooklyn Nicholson
2de7549fe0 feat(desktop): remember window size/position/maximized across launches (salvage #39154)
The desktop window opened at a hardcoded 1220×800 every launch, discarding
whatever size and position the user left it at (#39101) — on macOS the dock
reopen was the most visible case, but every restart reset it.

A small window-state.json under userData (same pattern as connection.json /
updates.json) records the window's normal bounds plus its maximized flag,
written debounced on resize/move/maximize and flushed on close, applied on the
next createWindow(). getNormalBounds() captures the pre-maximize size so an
un-maximize next session lands where the user actually sized it.

Restore is defensive: sanitize rejects garbage, drops off-screen positions
(window falls back to Electron centering), and caps a size saved on a
since-disconnected larger monitor to the largest current display. The geometry
math lives in a side-effect-free window-state.cjs so it unit-tests with
node --test, no Electron boot. No new dependency.

Salvages #39154 by @jeffrobodie-glitch — same userData approach and validation
intent, reimplemented tighter and folded into one module.

Co-authored-by: jeffrobodie-glitch <jeffrobodie@gmail.com>
2026-06-24 13:32:05 -05:00