The Electron composite project imported src/global.d.ts and followed its renderer-only dependencies. Share the notification payload from a dependency-free Electron module instead; all desktop typechecks and lint now pass.
Capture durable focus identity before runtime bindings disappear, retain native handlers, and keep runtime approval actions and plugin callbacks on their source renderer.
* fix(desktop): filter expired Windows CAs and deduplicate trust roots
* test(desktop): verify CA expiry and deduplication with real certificates
* test(desktop): cover Linux alongside macOS CA no-op policy
Co-authored-by: Gabriel Stoltemberg <215755014+Stoltemberg@users.noreply.github.com>
* chore: map CA trust contributors for release attribution
* style(desktop): match CA filtering to lint rules
---------
Co-authored-by: wliu-dev <249166551+wliu-dev@users.noreply.github.com>
Co-authored-by: Gabriel Stoltemberg <215755014+Stoltemberg@users.noreply.github.com>
Use the existing opaque-surface contract on the search pill, which sits outside the raised card. Preserve the current theme tokens and palette handoff.
Co-authored-by: Moisés Valero <moisesvs84@gmail.com>
* fix(desktop): wait for session list before auto-draining queued prompts
Restored composer-queue entries were retrying as soon as the gateway
socket opened, burning the drain budget against reaped runtimes and
toasting "Queued message not sent" on every launch.
* test(desktop): cover composer-queue drain while sessions are still loading
* docs(desktop): clarify queue-drain discovery gates
* test(desktop): preserve hidden-session queues after failed drains
---------
Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
A rejected OAuth token (HTTP 401 'User not found' from Nous Portal, Codex,
xAI…) reached the desktop error card as 'Provider error' with Retry as the
first action, which just replays the same dead credential.
Backend: nonretryable_client_error_result dropped failure_reason /
failure_retryable, so error_surface classified every non-retryable 4xx as a
retryable provider failure. It now stamps the classifier verdict like the
max-retries path, and auth-layer descriptors carry auth_kind
(oauth|api_key, derived from the provider catalog tab) + provider_label.
Desktop: an auth/oauth surface renders 'Authentication error', explains that
the <provider> sign-in expired/was revoked, and offers 'Sign in to <provider>
again' which launches that provider's existing onboarding OAuth flow scoped
to the failed session's gateway profile. Retry stays as the follow-up click.
Re-login to the provider already in use keeps the current model instead of
swapping in the recommended default.
Drop the injectable `topEdgePx` knob (one caller, one constant) so the
threshold has a single home, and fold the two-branch tail of
shouldAutoShowEarlier into one predicate. Comments keep the WHY: wheel is
the only signal at a clamped scrollTop 0, and the gates name the states
where scrollTop is near 0 without meaning "read earlier".
Tests: collapse the 11-case matrix into one invariant test on the pure
gate, and add a rendered-list test that mounts the real Thread against a
transcript heavier than RENDER_BUDGET, escapes the bottom lock, wheels up
at the clamped top and asserts more turn groups mount — while an upward
wheel mid-transcript mounts none. Red on origin/main (28 groups stay 28),
green with the fix.
Long conversations and branched sessions hide older turns behind the
render budget, but only the Show earlier button loaded them. Auto-page
through the same showEarlier path when the reader is at the viewport top.
Co-authored-by: Cursor <cursoragent@cursor.com>
Every tool.start / tool.progress / tool.complete ran three whole-timeline
passes that each allocated per part: `completeOpenStreamParts` (`map` with a
spread per row), the pending-row scan (`map` → `filter` → `map`, one wrapper
object per part), and `generatedImageEchoSources` (`flatMap` + a throwaway
array per non-image row) — then `dedupeGeneratedImageEchoesInParts` copied
the array again even when it stripped nothing, handing React a fresh
identity per event. Replace them with single in-place loops, and return the
input array from the dedupe when there are no echoes so a no-op stays a
no-op for the store.
Measured on the real `useMessageStream` hook (vitest/jsdom, 2,000 starts +
2,000 completions interleaved with text deltas): 618 ms → 138 ms; the
tool-only 2,000×2 case 252 ms → 67 ms. The remaining O(n) is one
`findIndex` per event over a ~4k array (~8 µs), well under the cost of the
copy React needs anyway, so no per-stream id index is introduced.
Behaviour is unchanged: same first-match `toolCallId` resolution, same
sparse/no-id contextual correlation, same ordering of parts.
Co-authored-by: Xipong <217837358+Xipong@users.noreply.github.com>
Health checker: keep the 12-reconnect coalesce test and replace the
stop/disconnect drop rows with the invariant #106135 names — one fresh
follow-up still runs when the active sweep fails through the handled
config-error path (13 config reads on main, 2 after the fix). Log poller:
keep single-flight + resumed cadence, and shrink the replacement-lifecycle
test to "stop suppresses a late publish and leaves no timer". Both files
were run against origin/main source and failed (11 / 13 / 13 requests).
startCompletionPoll already keeps the pane single-flight and its stop
function already suppresses a late publish, so createSerialTask (a second
queue that never forwarded its AbortSignal to getLogs) and the AbortError
re-throw inside the poll callback were defense-in-depth on top of the fix.
A profile switch now tears the old lifecycle down and starts the new one
immediately; at most one stale read can still be in flight, and its result
is discarded. Replace the AbortController with a plain stopped flag.
Drop the two companion tests from #106124 and #106099 that already pass
on main without their fixes (a pinned active transcript is never measured
because prune() skips it before weighing; a rerendered effect already
discards the previous closure). Keep the one red-on-main invariant per
fix: no re-serialisation of unchanged warm transcripts, and change ticks
coalescing behind an in-flight session.active_list request.
The salvaged glyph swap and hover tooltip are visual-only: the codicon is
aria-hidden and a Radix tooltip only speaks on hover/focus, so a screen
reader still announced an auto-discovered repo exactly like an explicit
project ("Open my-repo"). #106162 asks that the distinction reach
assistive tech and not rely on the glyph/color alone — the link's own
aria-label now appends the same "Auto-discovered" string for isAuto rows.
Also trims the salvaged tests from three to two invariants (explicit vs
auto), each asserting glyph, tooltip and accessible name together; the
"isAuto absent" case was a duplicate of the explicit case.
Refs #106162
Refs #73888
Auto-discovered repo lanes (Tier 2/3 in project_tree.py, tagged isAuto on
the tree node) rendered identically to explicit projects — both used the
folder-library lead glyph under the same PROJECTS header, so a user with
one real project could not tell it apart from repos Desktop found by
scanning disk.
The backend already emits isAuto; the frontend used it for sorting,
dismissal, and context-menu behavior but never surfaced it visually.
Auto nodes now render the repo glyph instead of folder-library, and the
row label carries an "Auto-discovered" tooltip. Explicit projects and
Home are unchanged. Adoption still works: theming an auto repo creates a
real projects.db row, the next tree refresh promotes it to an explicit
project (id p_<hex>, isAuto unset), and the auto cue disappears.
Closes#80383. Also addresses #73888 (same request, filed earlier).
The gateway already ships the refusal as machine data: prompt.submit returns
JSON-RPC 4090 with error.data.reason = SESSION_NOT_OWNED. Matching the English
sentence in a new lib helper duplicated that contract and would silently miss a
reworded or localized message. submit.ts now classifies the rejection once
(isSessionNotOwnedError beside the sibling isSessionBusyError, reason code
first, prose only for pre-contract backends) and stamps the inline error bubble
with errorSurface {layer: gateway, code: SESSION_NOT_OWNED, retryable: false},
the same descriptor shape the gateway uses for turn failures. The error card
reads surface.code instead of re-sniffing text; retryable=false already hides
Retry through the existing gate.
Dropped from #106248: the stranded-resume overlay button. That overlay fires
when session.resume itself fails through every retry; reading is never fenced
by the lease, so the live-owner refusal never reaches it (the issue's screenshot
is the inline turn-error card). Also drops the resumeStartNewSession locale
keys and the separate detector module + its test; two invariant tests remain
(submit stamps the descriptor; the card offers Start new session / hides Retry).
Retry alone is a dead end when another surface holds the session lease
(JSON-RPC 4090 / SESSION_NOT_OWNED). Surface an explicit new-session
escape on the turn-error card and the stranded-resume overlay without
weakening the lease guard.
An aux task pinned to a private endpoint via `base_url` (a home Ollama
box at `byron.local`, a LAN IP, localhost) is the intended per-task
endpoint feature and can never bill a provider. The Settings → Model
banner still counted it as "still run on openai" forever and offered
"Reset all to main", which would wipe the working local setup; the
post-switch `stale_aux` report had the same blind spot; and the aux row
never showed the `base_url` the backend already sends, so the pin was
indistinguishable from a paid-provider pin.
- `GET /api/model/auxiliary` now stamps each task with `local_endpoint`,
the verdict of the one canonical classifier
(`agent/model_metadata.py::is_local_endpoint`) — no TS mirror of the
private-range rules, so frontend and runtime cannot drift.
- Desktop: the persistent banner filter is the pure
`staleAuxAssignments()` and skips `local_endpoint` pins; the pinned row
appends ` · <base_url>` when one is set.
- `_stale_aux_pins` (post-switch report) skips local pins the same way.
- `is_local_endpoint`: `*.local` (RFC 6762 mDNS) now counts as local, and
IPv6 literals no longer ride the "no dots ⇒ unqualified host" rule, so
a global-scope address (`2607:f8b0::1`) is not local while `::1`,
ULA and link-local still are via the `ipaddress` scope checks.
Slim redo of #106236 (@webtecnica) and #106234 (@huklaa), which fixed the
same symptom with a client-side classifier copy; the bug class, row
display and mDNS/IPv6 classifier corrections are theirs.
Refs #106228
Co-authored-by: Hukla <129692708+huklaa@users.noreply.github.com>
Co-authored-by: webtecnica <webtecnica@gmail.com>