Commit Graph

605 Commits

Author SHA1 Message Date
Teknium 383f827493 fix(desktop): satisfy import-sort and ref-mirror lint rules in comment mode
Auto-fix perfectionist import/export ordering across the new
preview-annotate files, and extract the conversation-switch annotate
reset into a useCallback so the effect body carries no .current writes
(no-restricted-syntax ref-mirror rule).
2026-09-01 00:30:56 -07:00
Adolanium 10f2a20966 feat(desktop): add comment mode to the in-app browser
Click or drag on the preview page to pin a numbered comment. Saving a pin only adds it to the stack. Add comments drops the crops and a short prompt into the composer and never sends the turn.

Capture takes the visible page then crops in bitmap space, because Electron's rect crop on guest webviews is empty on Windows and shifted on high-DPI.
2026-09-01 00:30:56 -07:00
hermes-seaeye[bot] de590cc3cb fmt(js): npm run fix on merge (#99917)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-01 02:09:50 +00:00
Brooklyn Nicholson 9052ce76c4 feat(desktop): collapse Yesterday / Last week groups in the sessions sidebar
Date and status labels were separators only. Clicking one now hides the
sessions under it, same as project and profile rows, and Collapse all
covers those buckets too.
2026-08-31 21:03:25 -05:00
Gille e07172319d fix(desktop): make /stop interrupt active turns
Preserve the existing background-process cleanup after interrupting the targeted Desktop session. This salvages the narrow /stop behavior from the broader, conflicting PR #45030 on the current slash-command architecture.

Co-authored-by: AlvaroBiano <alvarobiano@users.noreply.github.com>
2026-08-31 20:50:10 -05:00
xxxigm bcecd675f7 fix(desktop): unwrap Models-page code-skew 503 and recycle the owned backend (#97046)
Show a Restart backend action that kills the SSH serve before the local child
so reconnect cannot reuse a stale lockfile, instead of dumping raw IPC JSON.
2026-08-31 20:43:36 -05:00
Brooklyn Nicholson 308454b372 fix(desktop): route model catalogs and picker reseeds through the focused owner
requestModelOptions now sends the owner profile on the RPC, forced reseeds call getGlobalModelInfo(profile), and picker/cache keys include the registry connection so a tile cannot fall back through the ambient socket.

Co-authored-by: fangliquanflq <fangliquan@qq.com>
2026-08-31 20:40:09 -05:00
Brooklyn Nicholson 38e4de48d8 test(desktop): cover /btw prompt.btw dispatch and btw.complete
Pin the RPC route (not slash.exec), bare-/btw usage, older-gateway
fallback, and originating-session answer rendering.

Co-authored-by: SsSs <w-kwan@hotmail.com>
Co-authored-by: kokhlo <konstantin.khlopkov93@gmail.com>
Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
2026-08-31 19:36:03 -05:00
Brooklyn Nicholson 5994b833b0 fix(desktop): route /btw through prompt.btw so the answer reaches the chat
Desktop sent /btw through the slash worker, which printed the answer after
process_command returned, so only the acknowledgement ever showed. Use the
TUI's prompt.btw RPC and persist btw.complete on the originating session.

Co-authored-by: SsSs <w-kwan@hotmail.com>
Co-authored-by: kokhlo <konstantin.khlopkov93@gmail.com>
Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
2026-08-31 19:36:03 -05:00
briandevans cebe2168ab fix(desktop): resolve the clarify card by zone, not document order
visibleClarifyCard() bottomed out in queryVisible(), which only drops
[data-pane-hidden] and then returns the first remaining DOM match. That is
enough to tell a foreground tab from a background one, but a split layout
has two chat surfaces on screen at once: both cards survive the hidden-pane
filter, so the winner is decided by document order. The earlier zone owns
Enter and 1..N permanently and the other visible card can never receive its
own shortcut — worse than the mount-order behaviour it replaced, because
mount order at least changed as panes came and went.

Break the tie on the ladder the app already has instead of inventing a
second notion of which surface is "the" one: tree/store's tabTargetGroup
walks hovered zone, then focused zone, and every tab verb (Cmd+1..9,
Ctrl+Tab, the Cmd+W family) targets through it; composerTargetInHoveredZone
(#74447) mirrors it for the model hotkey. visibleClarifyCard now does the
same, matching each visible card's closest [data-tree-group] against those
rungs. The single-card path short-circuits before any store read, so the
common case costs nothing extra.

The final fallback stays document order rather than null on purpose. When
neither rung names a zone holding a card — pointer off every zone, nothing
interacted with yet — returning null would leave Enter doing nothing at all,
a strictly worse regression than answering the first visible card.

Regression coverage pins both halves: the resolver unit tests assert the
active zone selects either card (not just the later one), that hover
overrides focus, that a hovered zone with no card falls through to the
focused one, and that neither rung resolving still yields a card; the
ClarifyTool integration tests render two visible cards in two zones and
assert exactly one clarify.respond fires, carrying whichever zone is
focused. All four order-sensitive assertions fail against the previous
resolver.
2026-08-31 19:20:32 -05:00
briandevans a3662bb32c fix(desktop): answer the visible clarify card, not the first-mounted one
A pending clarify card binds Enter / 1-9 / A-Z / arrows on `window`, and
inactive tabs stay MOUNTED, so every parked clarify keeps a live listener.
Nothing in the handler asked whether the card was on screen: the first
listener registered won, called preventDefault(), and the rest bailed on
defaultPrevented. Registration order is mount order, not visibility.

With two chats waiting on a question, answering the one in front of you
sent `clarify.respond` for a background session's request instead —
silently answering a question the user never saw and resuming that turn.

The invariant is already stated one file over, in composer-focus-keys.ts:
"a clarify card waiting in a background thread must not take the
foreground composer's letter keys". `clarifyCardOwnsKey` honours it via
queryVisible(); the card's own listener did not. That asymmetry is the
bug — the key-ownership resolver and the key handler disagreed about
which card is live.

Export that lookup as `visibleClarifyCard()` and have both sides use it,
so they cannot drift apart again. The card bails unless it IS the visible
card, which also leaves the keystroke unprevented for the composer when
the only pending card is hidden.

Scoped by visible-card identity rather than `usePaneVisible()`: split
zones each render their own active pane, so two cards can be visible at
once and a per-pane visible flag would not disambiguate them.
2026-08-31 19:20:32 -05:00
hermes-seaeye[bot] 60a4442826 fmt(js): npm run fix on merge (#98265)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-30 01:45:35 +00:00
itsflownium 393af4a310 fix(todo): live task state via revisioned snapshots and a dedicated todo.updated event
Salvaged from PR #97815 by @itsflownium, slimmed to the schema-free core:
- TodoStore gains a monotonic in-memory revision; the todo tool result
  returns it so clients can reject stale updates
- tui_gateway emits a dedicated todo.updated full-snapshot event that
  bypasses optional tool-progress display settings
- session resume/activate responses attach the authoritative todo
  snapshot; renderer restores it with revision arbitration
- desktop store tracks per-session revisions and rejects regressions

The session_todo_state DB table from the original PR is intentionally
dropped: canonical todo tool results already persist in conversation
history, so resume paths derive the snapshot from the stored transcript
instead of a parallel store.
2026-08-29 18:40:51 -07:00
Adolanium cf692532cf fix(desktop): merge todo patches instead of replacing the Tasks list
tool.start for a merge:true todo write used args.todos as a full replace. A one-item status patch became Tasks 1/1, or vanished if content was omitted, so the panel looked stuck at 0/5 until the final complete result. Apply merge by id on start, keep replace for the full result, and show the in-progress spinner on the expanded header too.
2026-08-29 18:40:51 -07:00
Teknium 74a95a3ddf feat: /btw now answers side questions with conversation context; /background renamed to /bg
/bg (formerly /background, which is retired) keeps the existing semantics:
spawn a fresh, independent agent session in the background.

/btw is now its own command matching the convention other harnesses use:
ask a quick side question ABOUT the current conversation without
interrupting it. A one-shot auxiliary LLM call (main model by default,
overridable via auxiliary.side_question.* in config.yaml) answers from a
read-only transcript snapshot — the live session's history, role
alternation, and prompt cache are untouched, and the current turn keeps
running.

Surfaces wired: CLI (inline mid-run dispatch), gateway (all messengers,
busy-dispatch table + idle dispatch, i18n across all 17 locales), TUI
(prompt.btw RPC + btw.complete event), Discord native slash, relay
command manifest, desktop exec routing, docs (EN + zh-Hans).
2026-08-29 07:25:17 -07:00
Teknium b6bd681e89 feat(todo): nested subtasks via optional parent field
The todo tool now supports hierarchical task lists: an item's optional
'parent' field points at another item's id, making it a subtask.

- tools/todo_tool.py: parent validated (self-ref dropped), dangling refs
  and cycles sanitized; merge mode can set/clear parent; post-compression
  injection renders the tree indented and keeps a finished parent visible
  while any descendant is still active; the in-progress reorder pass is
  skipped for nested lists (a flat move would tear subtasks from parents).
- Schema cost: ~45 tokens added to the cached tool schema (one string
  property + one behavior sentence).
- acp_adapter/tools.py: todo result markdown indents by parent depth.
- Desktop: TodoItem carries parent; todoTree() DFS helper; composer
  status stack renders subtask rows indented (depth-capped), stabilizer
  compares depth.
- Docs: tools-reference todo entry mentions nesting.

Hydration/replay paths (gateway fresh-agent, API-server history) work
unchanged: parent rides inside the same todos array.
2026-08-29 07:25:12 -07:00
StanleyStetson 24e54b55f5 fix(desktop): preserve streamed assistant text and unify atomic persistence (#95514)
- Preserve streamed assistant text in Desktop UI when message.complete delivers empty text.

- Prevent destructive hydration in Desktop useMessageStream over rendered text on empty completion.

- Recover stream buffer in finalize_turn when final_response is empty on healthy turns.

- Unify in-place blank assistant repair, watermark clone resolution, non-blank concurrent winner adoption, and batch row appends into a single atomic guarded SessionDB transaction.

- Synchronize canonical committed content to live in-memory messages dicts and preserve all-or-nothing rollback semantics on persistence failure.
2026-08-29 11:33:05 +05:30
hermes-seaeye[bot] d7c0fb9d66 fmt(js): npm run fix on merge (#97706)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-29 05:56:24 +00:00
Brooklyn Nicholson 178c23fb27 fix(desktop): open HUD links in the system browser
The HUD has no in-app browser, so a click tried to paint a webview
into the transparent overlay (OAuth). Hand those links to the OS,
mount the context menu, and skip preview-tile docking.
2026-08-29 00:51:43 -05:00
Brooklyn Nicholson 21cc469eb1 Merge remote-tracking branch 'origin/main' into bb/bot-mode-design-system
# Conflicts:
#	apps/desktop/src/plugins/hermes-bots/plugin.js
#	apps/desktop/src/plugins/hermes-bots/tests/group-chat.test.mjs
#	apps/desktop/src/sdk/profile-routing.test.ts
2026-08-28 12:05:11 -05:00
Nacho Chiappero 49761cebad test(desktop): pin that field-level junk in an mcp_servers entry survives
`getServers` filters on whole-entry shape only — an entry with a junk field
(`{ command: 42, enabled: 'yes' }`) is still handed to the readers, which
coerce and tolerate. Pin that so a later tightening of `isEntry` can't
quietly start dropping entries that merely carry a bad field.

Raised in review on #94338.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 06:33:40 -07:00
Nacho Chiappero 13afe9e9e1 fix(desktop): drop malformed mcp_servers entries instead of crashing
An `mcp_servers` key left without a value parses as `null`, and the MCP tab
reads `enabled` straight off every entry (`serverEnabled`), so a single
dangling key threw during render and took the whole Capabilities workspace
with it — including the screen you would use to fix the config.

Filter non-object entries in `getServers`, the one choke point every reader
goes through, so a hand-edited config degrades to a missing row instead of a
dead pane. The backend already refuses to write such an entry
(`_replace_mcp_servers`), so this only has to survive a config edited
outside the app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 06:33:40 -07:00
Brooklyn Nicholson 832ec82f75 fix(desktop): mount every chat.empty contributor, not just the first
Ownership of an empty transcript is per session and is not known until each
plugin has loaded its own data — which is why the slot mounts contributors
rather than resolving a claim up front. Taking only contributions[0] then let
a plugin that DECLINES a session suppress the one that owns it: silently,
permanently, and only for the users whose installed plugins happen to
register in that order.

Every registration is mounted and answers for itself. Declining is free; two
claiming one session render both, a visible conflict instead of a silent drop.
2026-08-27 23:23:47 -05:00
Brooklyn Nicholson 0f4d4d4d8b fix(desktop): subscribe the bot-chat flag to every store it reads
The composer subscribed to $sessionStates while isBotChatSession reads the
scope set — and, to resolve a live id to the stored one it is filed under,
$sessionTiles too. It stayed roughly right only because $sessionStates
republishes per streaming token, so the stale answer was overwritten within a
frame. A quiet session had no such luck.

Adds useStoresSelector beside useStoreSelector for the general case: a scalar
whose inputs span several stores, recomputed when any notifies, still
re-rendering only when the scalar changes.
2026-08-27 23:23:43 -05:00
Brooklyn Nicholson 6559448306 Merge origin/main into bb/bot-mode-design-system
Keeps plugin.js and its new .mjs test deleted. main's closed-chat fix
(7c91079) landed in both; it is a real behaviour change, so the commit that
follows ports it onto the split modules rather than dropping it with the
files. Its core half — focusWorkspaceOwnerSessionTile and the
host.focusOpenWorkspaceSession verb — merged cleanly and is used as-is.
2026-08-27 22:51:30 -05:00
Brooklyn Nicholson 595ee92289 fix(commands): put desktop slash metadata on the CommandDef
Inference is now any args_hint without subcommands → text. Mixed is the
only remaining hint-token path. desktop= and the few argument_mode
overrides live on the registry entry; the side tables are gone. Catalog
aliases get their own dict copy. Composer tests seed the catalog so
/goal stays mixed without an overlay row.
2026-08-27 22:05:40 -05:00
Brooklyn Nicholson 60f58249e5 fix(desktop): resolve slash commands from the catalog
The overlay table is only actions, pickers, and RPCs. Completions group
by backend kind, and argument mode comes from the warmed catalog so
/review and plugin commands stay typeable.
2026-08-27 22:05:40 -05:00
Brooklyn Nicholson bbcf5691a6 Merge origin/main into bb/bot-mode-design-system
main's Bots-home flash fix (6f8be61) landed in plugin.js, which this branch
deletes. Both files stay deleted: the guard it adds (botOpenInFlight gating
botsHomeMayOpen) protects a surface this branch removes, and the two sites
where it renamed the generation bump to cancelBotOpen already bump here via
bumpBotOpenGeneration in shared.ts.
2026-08-27 21:52:40 -05:00
Brooklyn Nicholson 911c6c50d3 feat(tools): let Hermes point at one thing with the tip tool
The quiet sibling of `tour`, in the same `desktop_ui` toolset and reading the
same `tour(action='targets')` discovery call: one bubble with an arrow, for a
sentence that would be clearer with a finger on the thing it's about. Dimming
the whole app to say "the model name is a button" is the wrong weight.

Fire-and-forget rather than a round-trip, because a tip is not a question and
blocking the turn on one would stall the reply it belongs to. The renderer
enforces the user's opt-out itself, so a stale config read can never put a
bubble on a screen that asked for none.
2026-08-27 21:50:18 -05:00
Brooklyn Nicholson baaf304992 feat(desktop): in-app tips
An ambient rotation that points at parts of the app you may not have found yet
— one accent bubble, an arrow, and an outline around the subject. No scrim and
no spotlight: a tip is a pointer beside your work, not a modal in front of it,
so it takes no focus, owns no Esc, and blocks nothing.

It only speaks when the app is genuinely quiet — nothing streaming, no dialog,
menu or tour up, window focused, a few seconds since the last keystroke — and
walks the catalog in order rather than shuffling, so tips arrive as a tour of
neighbouring parts of the app instead of unrelated ones. A tip with nothing on
screen to point at is skipped, not waited for.

Closing one with its ✕ retires it for good. That is the whole reason the ✕ is a
heavier gesture than letting the bubble time out, and Settings → Appearance is
the only way back — alongside the switch that turns the feature off entirely.
2026-08-27 21:50:18 -05:00
Brooklyn Nicholson 46512ee1d6 feat(desktop): give the app's main surfaces durable handles
Tours and tips both address elements by selector, and everything they most want
to point at — the composer, the model pill, the nav rows, the profile rail, the
right-pane toggle — was reachable only by icon, position, or a translated
aria-label. None of those survive a re-render, a theme, or a locale change.

Two handles are needed per surface, not one, because where an arrow points and
what an outline wraps are different questions: a nav row's label carries the
`data-tour` handle so an arrow lands at the end of the word, and defers the
outline to the row via `data-tip-arrow-only`.

The collector also has to skip panes hidden by the keep-alive stack. An inactive
tab stays mounted under `visibility: hidden` to keep its scroll position, so its
rect is identical to the live tab's and no geometry test separates them — which
is how a tour could spotlight a background tab's composer.
2026-08-27 21:50:18 -05:00
Brooklyn Nicholson 610d1ed0da refactor(desktop): give the sidebar row geometry and a bounded cache one owner
Row geometry lived inside chrome.tsx as private consts, so anything that
wanted to line up with a session row copied the literals instead — the lead
cell alone appeared verbatim in six files. Its own comment warns that owning
height anywhere else makes rows float 1-2px off sessions, which is exactly
what a copy invites. Split the measurements into row-geometry.ts, keep the
public names re-exported from chrome.tsx, and put the lead cell and
ConnectionGlyph on the plugin SDK so a plugin composes the same box rather
than re-deriving it. ConnectionGlyph takes a className now so a caller can
tint it without forking the component.

LruCache is the same move for a different leak: katex-memo.ts had a private
one with a hardcoded ceiling, and the bot rail had two Maps that never
evicted. One shared class, exported, katex's twin deleted.
2026-08-27 21:46:40 -05:00
Brooklyn Nicholson 45176219a2 feat(desktop): let a plugin title an empty chat it owns
Core has one empty state, the intro splash, and it belongs to a fresh
draft with no session selected. A session that exists but has nothing in
it yet falls outside that, and whoever opened it is the only one who
knows what should stand in the gap — core has no business learning a
bot's face and name.

Add `chat.empty` as a contribution area, resolved by the transcript the
same way `transcript.directives` resolves an inline widget. A
contribution mounts for every empty session and renders nothing for the
ones it does not own, so it can subscribe to its own stores and appear
once they load.

Pull the splash's lettering out of `intro.tsx` into a `Wordmark`
primitive so a second caller cannot fork it, and move the typeface,
weight and tracking into `.wordmark` beside the `.fit-text` rules they
travel with — as utilities they were one class-merge or one missed scan
away from silently rendering the wordmark in body text. `Wordmark` takes
its width, because fit-text sizes to fill and a short name set at the
splash's full width comes out enormous.
2026-08-27 19:08:28 -05:00
hermes-seaeye[bot] ca2a0d4d6f fmt(js): npm run fix on merge (#96506)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-27 16:29:27 +00:00
HexLab98 c086bb6f71 test(desktop): cover Voxtral JSON unwrap on client-direct STT
Pin the Groq plain-text path and the Mistral envelope so dictation
keeps spoken words, not the raw transcription object, in the composer.
2026-08-27 11:23:40 -05:00
HexLab98 bc737576ba fix(desktop): unwrap Mistral Voxtral JSON in client-direct STT
Mistral ignores response_format=text and returns the full transcription
object. Desktop was dumping that JSON into the composer; pull .text out
so dictation shows the spoken words instead.
2026-08-27 11:23:40 -05:00
Teknium 9faa685385 feat(desktop): read-only stored-transcript resume + legacy owner-backfill trigger (#94724)
The fail-closed owner ladder (#95407) is correct for new sessions, but
legacy unowned rows on registry-topology installs dead-ended in
SessionOwnerResolutionError (reporter's Error B) with their transcripts
fully intact in state.db.

- resolveLegacyOwnerBackfillScope: pick the single-match store for the
  server-side owner backfill at enumeration time (serving registered
  connection / primary pool); fail closed on multi-candidate topologies.
- maybeBackfillLegacySessionOwners: one-shot per scope per renderer,
  fire-and-forget from the #95407 stamp path, logs the stamped count.
- Read-only stored-transcript resume: when session.resume fails closed,
  fetch the transcript over id-only REST (ambient first, then registered
  backends, read-only probes only) and open the session as a read-only
  transcript instead of dead-ending; sends are refused with a notice and
  a later successful live resume clears the latch. Wired into the main
  pane resume recovery and the session-tile delegate (which now runs the
  same fail-closed owner gate as the RPC dispatcher).

Refs #94724
2026-08-27 02:17:56 -07:00
hermes-seaeye[bot] 36b0a96dcb fmt(js): npm run fix on merge (#96076)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-27 04:43:58 +00:00
Teknium 61b6788dd4 fix(desktop): Bots-mode picker routes guarded model switches through the shared confirm handler
The Bots editor's model write (profiles.configure) was the one switch
surface that bypassed the data-policy / expensive-model selection guard:
a guarded pick (e.g. muse-spark contributor tier) was applied silently,
with no confirm flow anywhere — the #95293 remainder after the core
picker's confirm handshake landed in use-model-controls.

Gateway: profiles.configure now answers confirm_required +
confirm_message for a guarded model (same handshake as config.set
model) and writes NOTHING until the client resends with
confirm_expensive_model: true. Other sections still apply; the pending
model section is not reported as failed.

Desktop: the confirm flow is extracted out of use-model-controls into
one shared applier (lib/guarded-model-switch.ts, exported through the
plugin SDK) — warning toast, staleness-guarded Confirm, single
confirmed resend, never a retry loop. The core picker and the Bots
editor now consume the SAME handler; the Bots editor's Confirm resends
only the model section with confirm_expensive_model: true.

Fixes #95293 (Bots surface remainder).
2026-08-26 21:38:41 -07:00
rainbowgits 0fa98d6a1b fix(desktop): switch model after refresh when it leaves the catalog
Refresh Models only updated the catalog cache, so the composer kept showing a model that was no longer in the new group list.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 21:38:41 -07:00
Finn763 b1f133d4ea fix(desktop): defer gateway liveness force-close while a turn is in flight (#95327)
A wake-path liveness-probe timeout force-closed the primary renderer
socket even when the backend was merely busy mid-tool-call; the gateway
then saw its client vanish, ws_orphan_reap expired, and the running turn
died as a bare "Operation interrupted." placeholder.

While any session still reports working, the first inconclusive probe
timeout now defers the teardown behind one bounded re-probe; only an
exhausted consecutive-failure streak (or no in-flight work at all)
rebuilds the transport. The streak resets on a successful ping, a
healthy -32601 answer, a clean open, a gateway switch, and unmount.
2026-08-26 21:38:00 -07:00
nftpoetrist d81b801fb4 fix(desktop): bound getConnection()/resolveGatewayWsUrl() on every remaining route
20s withTimeout() on the boot() and soft-switch paths (use-gateway-boot.ts),
since these are IPC round-trips into the main process with no timeout of
their own — a wedged main-process round-trip hangs the awaiting caller
forever instead of surfacing a failure.

Every other production call site of the same IPC pair was still unbounded:

- store/gateway.ts's openSecondary() and sharedPrimaryRoute() — the actual
  connection-establishment underneath requestGatewayForProfile/Agent,
  ensureGatewayForProfile/Agent, and every other exported routing entry
  point that opens a non-primary profile's socket.
- use-gateway-request.ts's on-demand reconnect (the primary gateway's
  "not connected" retry path hit by every RPC).
- voice-playback.ts's resolveSpeakStreamUrl().
- api/plugins.ts's activeConnection() (pluginSocket's connect()).

Extracted RECONNECT_ATTEMPT_TIMEOUT_MS into the shared lib/with-timeout.ts
(previously local to use-gateway-boot.ts) so every call site uses the same
budget instead of duplicating the constant.

Regression tests mirror the existing use-gateway-boot.test.tsx hang-repro
pattern: wedge getConnection()/getConnectionFor() with a never-resolving
promise, advance fake timers past the 20s bound, assert the caller settles
instead of hanging. Mutation-verified: reverted the production fix (kept
tests) and confirmed the 6 new tests fail — 4 by genuinely timing out at the
vitest level, 2 by TypeError on the not-yet-exported activeConnection —
restored the fix and confirmed all 70 tests across the gateway/voice/boot/
plugins suites pass, with tsc -p . --noEmit clean throughout.
2026-08-26 21:38:00 -07:00
Teknium 574bd7175c fix(desktop): unify boot-class getConnection() budgets on one shared 45s constant
Follow-up to the #95039 salvage: the cherry-picked bound used the 20s
RECONNECT_ATTEMPT_TIMEOUT_MS on boot()/softSwitch() getConnection(), but a
reviewer note (and the Phase A registry-restore work) established that
boot-class awaits must ride out a full backend cold spawn — main's spawn
budget is 45s (DEFAULT_BACKEND_READY_TIMEOUT_MS). A 20s renderer bound would
latch boot errors on healthy-but-slow cold boots.

Introduce BACKEND_BOOT_WAIT_TIMEOUT_MS (45s) in lib/with-timeout.ts as the
single shared boot-class budget, point boot()/softSwitch() getConnection()
and connections.ts BOOT_DESCRIPTOR_WAIT_TIMEOUT_MS at it, and keep the 20s
reconnect budget only for reconnect-class awaits against an already-spawned
backend. No magic-number drift: 45_000 now appears once in renderer code.
2026-08-26 04:49:22 -07:00
hermes-seaeye[bot] c427367938 fmt(js): npm run fix on merge (#95457)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-26 10:27:25 +00:00
Teknium 2947272233 refactor(desktop): one canonical write shape for connection_id row stamping
Reconcile #94901's API-layer row stamping with #94656's durable-owner
persistence: extract lib/session-owner-stamp.ts as THE canonical
stamp-untagged-rows write path (never clobbers an explicit owner,
never stamps `local`) and re-express api/sessions'
stampActiveConnectionOwner through it. #94656's writers (optimistic
row from the captured owner route, mergeSessionPage carry, cache
patch) are exact-owner writers and stay as-is; the helper's contract
documents why it must not overwrite them.

Credit: row-stamping concept from PR #94901 (joe-rodgers) and
PR #95007 (weismanfamily); persistence shape from PR #94656
(Zeus-Deus).

Co-authored-by: joe-rodgers <25499388+joe-rodgers@users.noreply.github.com>
2026-08-26 03:22:37 -07:00
hermes-seaeye[bot] 1a19c52dd2 fmt(js): npm run fix on merge (#95365)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-26 07:45:59 +00:00
Jeremy McKeehen ff57f173d8 fix(desktop): keep pin list identity gateway-wide
Pin localStorage was keyed per connection and profile, so an unpin
reloaded a stale copy on switch and re-asserted pinned=true.
Scope pins by connection only so they survive rescope and stay isolated per gateway.
2026-08-26 00:22:27 -07:00
Teknium 57876f4b71 fix(desktop): typecheck fixes for salvaged tests (afterEach import, routed-request mock typing) 2026-08-26 00:21:29 -07:00
LovePlayCode 1808d33a24 fix(desktop): keep owner-routed tile gateways out of idle prune
Bot chats stay on a secondary while chrome stays on the launch profile.
The keep-set only counted busy sessions, so idle prune closed the tile
socket and resume spun forever. Keep open tiles, route catalog reads to
the owner, and hydrate model/provider from resume.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 00:21:29 -07:00
Zeus-Deus 693dd5d042 fix(desktop): guard stale refresh ownership 2026-08-25 22:51:31 -07:00