Commit graph

1206 commits

Author SHA1 Message Date
yoniebans
faa3bce6dc style(desktop): satisfy merged eslint/prettier config
The SSH modules predate the stricter lint config that landed on main (curly, no-empty, perfectionist sorting, prettier). Mechanical lint:fix + fmt pass, empty catch blocks filled with the codebase's void-0 convention, and inline no-control-regex disables on the three deliberate control-char patterns (same pattern as lib/ansi.ts).
2026-07-20 23:01:49 +02:00
yoniebans
578374675e Merge origin/main into feat/desktop-remote-ssh-current
Sync with main after the contribution-shell refactor (#60638) and the backendConnectionState extraction (#65885) landed. Seven conflicts, all resolved in favor of main's new architecture with the SSH lifecycle ported on top:

- electron/main.ts: adopt backendConnectionState (attempt tokens, attachProcess, clearPromiseForAttempt) as the sole owner of the primary backend; drop the branch's raw hermesProcess/connectionPromise globals and commitConnectionFailure call site. SSH liveness-streak classification, teardown, and the SSH quit path are layered onto the new ownership model; before-quit combines SSH transactional teardown with the Windows sandbox marker (#38216).
- desktop-controller.tsx: deleted on main; its SSH beforeConnectionSwitch cleanup (preserved-route fresh draft, overlay return-route reset, project-tree reset, close-all-terminals) moves to contrib/wiring.tsx's useGatewayBoot call.
- use-session-actions/index.ts: keep main's onFreshDraftRouteIntent (fires unconditionally); preserveRoute gates only the navigate.
- gateway-settings.tsx: keep main's acceptSavedConfig/connectedCloudUrl; every save/sign-in/sign-out success path routes through acceptSavedConfig with the branch's stale-async seq guards intact.
- boot-failure-reauth.ts: keep both sshFailureMessage and main's isRemoteReauthFailure formatting.
- Both test harnesses take the union of props.

Validation: tsc -b clean; desktop suite 1704 passed / 1 skipped; electron SSH/connection modules 225 passed; python SSH + web_server suites 508 passed.
2026-07-20 22:46:39 +02:00
ethernet
c363db81e0
fix(desktop, ink): don't wipe messages before final message (#65919)
* fix(desktop): preserve interim assistant text wiped at message.complete

When the agent emits interim text (commentary alongside tool calls, or the
attempted final answer before a verify-on-stop nudge), all UI surfaces
streamed it live but then wiped it at message.complete — keeping only the
final response. The user saw text appear during inference, then disappear.

This is the complete fix across all three layers: agent core, gateway
transport, and all UI surfaces (desktop + Ink TUI).

The verify-on-stop and pre_verify paths flagged the assistant's attempted
final answer as _verification_stop_synthetic, suppressing it from both
state.db and the UI. The user only saw the terse post-verification reply.

Now the assistant response is real content: it's persisted to state.db and
emitted as an interim message via _emit_interim_assistant_message(force_display=True)
before the verification loop runs. Only the synthetic nudge messages keep
the synthetic flags. The turn finalizer drops nudges from live history and
compares content (not just role) to avoid duplicating a published candidate.
Message sequence repair collapses verification candidates in the
consecutive-assistant merge.

Wire agent.interim_assistant_callback both at construction (_agent_cbs())
and per-turn (defense-in-depth), emitting a new message.interim event with
{text, already_streamed}. Gated on display.interim_assistant_messages
(default true). Cleared in the finally block so a stale closure can't
fire on a later turn.

Add message.interim to the GatewayEventName union (apps/shared) and a
typed payload to the TUI's GatewayEvent discriminated union.

The TUI already had the segment-anchoring machinery (flushStreamingSegment +
finalTail) but had no handler for message.interim. Added recordInterimMessage
+ interimBoundaryIndex to seal segments mid-turn, and updated
recordMessageComplete to only dedupe segments after the interim boundary.

Replaced the fragile sealed-set approach with a proper interimBoundaryPending
state flag on ClientSessionState. finalizeInterimAssistantMessage finalizes
the streaming bubble in place (or creates a standalone one), rotates the
stream ID so next deltas create a new bubble, and sets the flag. When the
final text equals an already-sealed interim, they stay as distinct messages.

Extracted mergeFinalAssistantText() as a pure function in chat-messages.ts,
used by both completeAssistantMessage and finalizeInterimAssistantMessage.
Split the bidirectional dedup predicate: reasoning is a restatement only when
the final FULLY covers it. A short final ("Done.") no longer swallows a
longer reasoning block that merely starts with it.

Honor display.interim_assistant_messages (default true) across all layers:
the tui_gateway gates the callback, the desktop wires it to a nanostores
atom via use-hermes-config. Updated hermes_cli/config.py and
cli-config.yaml.example comments to document the Desktop behavior.

_split_segment_tokens now accepts posix=False and _find_ad_hoc_match tries
both posix modes so ad-hoc verification scripts with Windows backslash
paths are matched correctly. (response_previewed forwarding from #53553
is not included — our emit-interim + persist approach makes it unnecessary
since the attempted answer is now surfaced before the verification loop.)

- tsc: clean (desktop + TUI + shared)
- vitest desktop: 73/73 pass (7 interim-sealing + 5 mergeFinalAssistantText + 4 config atom)
- vitest TUI: 83/83 pass (4 new message.interim tests)
- python: 390 tests pass (340 tui_gateway + 33 verification/finalizer + 6 config gating + 3 evidence + 8 continuation budget)

Co-authored-by: Liam Zhang <yingliang-zhang@users.noreply.github.com>
Co-authored-by: Lucas D'Alessandro <lucasfdale@users.noreply.github.com>
Co-authored-by: Eric Manganaro <superposition@users.noreply.github.com>
Co-authored-by: sweetcornna <sweetcornna@users.noreply.github.com>
Co-authored-by: DECK6 <DECK6@users.noreply.github.com>
Co-authored-by: matantsevs <matantsevs@users.noreply.github.com>
Co-authored-by: gitcommit90 <gitcommit90@users.noreply.github.com>

* fix: prefix-match interim streamed content to avoid benign duplicate bubbles

_interim_content_was_streamed used exact equality (streamed == visible_content),
so a final response that was the streamed text plus a trailing delta — or a
partial stream before the verify nudge fired — failed the match and left
_response_was_previewed false. The turn then showed two bubbles (interim +
identical final) instead of settling the interim in place.

Relax to a prefix check (visible_content.startswith(streamed)) in both the
core match and the desktop's settle-in-place gate. The TUI already used
prefix matching via finalTail. The reverse direction (streamed longer than
final) is intentionally not matched — that could suppress a needed resend
in the gateway path where already_streamed=True calls on_segment_break().

* test(desktop): add partial-stream-then-nudge dedup edge case

Third edge case for the interim-sealing dedup: model streams part of its
answer via message.delta, verify nudge fires, interim seals the streamed
prefix, then the final response is the same text plus a trailing delta.
Asserts one bubble (not two) containing the full final text.

Acceptance protocol #2 — covers all three dedup edges:
  1. interim == final (existing)
  2. interim = strict prefix of final (existing)
  3. partial-stream-then-nudge (this commit)

---------

Co-authored-by: Liam Zhang <yingliang-zhang@users.noreply.github.com>
Co-authored-by: Lucas D'Alessandro <lucasfdale@users.noreply.github.com>
Co-authored-by: Eric Manganaro <superposition@users.noreply.github.com>
Co-authored-by: sweetcornna <sweetcornna@users.noreply.github.com>
Co-authored-by: DECK6 <DECK6@users.noreply.github.com>
Co-authored-by: matantsevs <matantsevs@users.noreply.github.com>
Co-authored-by: gitcommit90 <gitcommit90@users.noreply.github.com>
2026-07-20 11:42:29 -04:00
yoniebans
45acb62c6d feat(desktop): route Desktop SSH to the Windows lifecycle
Detect the remote platform on connect and Test SSH: uname first (Linux/Darwin
keep the POSIX lifecycle), else an encoded-PowerShell probe that routes a
Windows host through connectWindowsRemote. The Windows lifecycle mirrors the
POSIX one — probe, one-shot token upload, spawn, readiness scrape, tunnel,
reuse-by-exact-ownership, and teardown — over a PowerShell helper dialect.

Preserve a backend on indeterminate process state (never destroy on unknowns);
a thrown terminate aborts before remove-lock so a live backend is never
orphaned. Interactive terminals on a Windows backend open PowerShell in the
requested cwd instead of a POSIX login shell.
2026-07-20 17:19:22 +02:00
UnathiCodex
8f33e39682
fix(desktop): prevent false runtime-not-ready under gateway load (#66174)
* fix(desktop): stabilize runtime readiness polling

* fix(tui_gateway): pool live-session status polling

* fix(desktop): clear readiness when gateway disconnects
2026-07-20 10:51:27 -04:00
Po-Han Shih
3e6cead363
fix(desktop): stop session.info from overwriting composer model (#66603)
Closes #66265. Credit: Stan Shih (stantheman0128).

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Austin Pickett <pickett.austin@gmail.com>
2026-07-20 10:40:59 -04:00
UnathiCodex
c59ca46940
fix(desktop): keep quiet sessions visibly running (#65870)
* fix(desktop): keep quiet sessions visibly running

* fix(desktop): restore running status after reconnect

* fix(desktop): keep live session status unmistakable
2026-07-20 10:31:58 -04:00
nousbot-eng
3d7e1c5f43
fmt(js): npm run fix on merge (#68065)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-20 13:58:53 +00:00
Austin Pickett
2bb531b67a
fix(desktop): extend read aloud + transcription timeouts (salvage of #39286) (#68056)
* fix(desktop): extend read aloud timeout

Hermes Desktop Read Aloud can show a false failure after 15s even when
backend TTS synthesis succeeds. The Desktop renderer bridge request to
/api/audio/speak blocks until provider synthesis, audio file read, and
base64 response encoding finish, so larger messages or slower remote TTS
providers can legitimately exceed the default 15s Electron backend
timeout (DEFAULT_FETCH_TIMEOUT_MS in hardening.ts).

Give only the blocking Read Aloud request a bounded TTS-specific timeout
(180s floor, 600s cap, text-length scaling). Normal Desktop API requests
keep the short default so real backend hangs still fail promptly.

Salvage of #39286 rebased onto current main (original branch conflicted
in apps/desktop/src/hermes.ts and hermes.test.ts against the newly-added
STARTUP_REQUEST_TIMEOUT_MS / PROMPT_SUBMIT_REQUEST_TIMEOUT_MS constants
and model-options tests). Both additions coexist; no behavior changed.

Co-authored-by: Ruslan Vasylev <ruslan.vasylev.vfx@gmail.com>

* fix(desktop): extend read aloud + transcription timeouts

Hermes Desktop Read Aloud can show a false failure after 15s even when
backend TTS synthesis succeeds. The Desktop renderer bridge request to
/api/audio/speak blocks until provider synthesis, audio file read, and
base64 response encoding finish, so larger messages or slower remote TTS
providers can legitimately exceed the default 15s Electron backend
timeout (DEFAULT_FETCH_TIMEOUT_MS in hardening.ts).

/api/audio/transcribe is the sibling blocking endpoint with the same bug
class: it blocks on provider STT + file handling + encoding behind the
same 15s default, so long clips / remote providers hit the same spurious
timeout. Give both requests a bounded, endpoint-specific timeout (180s
floor, 600s cap) — speak scales on text length, transcribe on the base64
payload length. Every other Desktop API request keeps the short default
so real backend hangs still fail promptly.

Salvage of #39286 rebased onto current main. The original PR was scoped
to /api/audio/speak only; this extends it to also close the transcribe
twin per OutThisLife's review suggestion on #39286, closing the whole
'audio endpoints time out at 15s' class in one shot.

Co-authored-by: Ruslan Vasylev <ruslan.vasylev.vfx@gmail.com>

---------

Co-authored-by: Ruslan Vasylev <ruslan.vasylev.vfx@gmail.com>
2026-07-20 09:51:31 -04:00
Teknium
a1813c1ef4
feat(desktop): inline TTS voice/model settings in the Capabilities tab (#68017)
* feat(desktop): inline TTS voice/model settings in the Capabilities tab

The Capabilities > Tools > Text-to-Speech panel only surfaced API keys per
provider — voice and model settings (e.g. tts.openai.voice) lived exclusively
in Settings > Voice, so users couldn't select or type a voice/model name where
they configure the backend.

- web_server: TTS provider rows now carry their tts_provider config key
  (the section holding that backend's voice/model settings)
- desktop: new VoiceProviderFields renders the provider's config fields
  inline in the toolset panel, deriving the key list from the curated
  Settings > Voice section so the two surfaces can't drift; shared
  ConfigField extracted to config-field.tsx
- voice/model name fields are now free-input comboboxes (Input + datalist)
  instead of closed Selects — custom voice IDs (ElevenLabs cloned voices,
  xAI custom voices, Edge's 400+ catalog) are typeable, known values remain
  suggestions
- refreshed the stale OpenAI voice list (adds ash/ballad/cedar/coral/marin/
  sage/verse) and added suggestion lists for edge/gemini/minimax/mistral/
  kittentts/piper/neutts models and voices
- config.py: added missing tts.minimax and tts.kittentts default blocks and
  deepinfra model/voice fields to the Voice section so those providers are
  configurable from the GUI at all

* test(desktop): await effect-driven panel content in post-setup CTA tests

The auto-expand effect renders the provider's inner panel one re-render
after the row; with the QueryClientProvider wrapper the extra provider
tick made the synchronous getByRole race it (~10% local flake, failed on
CI). Await the panel content with findBy* instead.
2026-07-20 05:38:38 -07:00
nousbot-eng
e89bc58a5b
fmt(js): npm run fix on merge (#67995)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-20 10:47:15 +00:00
Teknium
4ef92d2e5d
fix(desktop): survive older backends missing the batched sidebar endpoint (#67986)
Two stacked failures turned desktop/runtime version skew into a full
'Hermes couldn't start' boot brick:

1. listSidebarSessions() had no fallback when the backend predates the
   batched /api/profiles/sessions/sidebar route (added Jul 18) — the
   backend catch-all 404 ('No such API endpoint') rejected the refresh.
2. boot() awaited refreshSessions() unguarded inside Promise.all —
   unlike the reconnect and softSwitch call sites — so a session-LIST
   failure rejected the whole boot and raised failDesktopBoot() even
   though the gateway WS was already open and the app was usable.

Fixes:
- listSidebarSessions() now detects endpoint-missing errors (backend
  catch-all, IPC-wrapped 404, Electron HTML-guard), falls back to the
  three proven per-slice /api/profiles/sessions calls with identical
  scoping (recents on the caller's profile, cron + messaging
  cross-profile), and remembers the verdict so later refreshes skip the
  dead probe. Transient failures (timeout, 5xx, ECONNREFUSED) still
  throw — no silent fast-path degradation from one blip.
- wipeSessionListsForGatewaySwitch() resets the capability flag so a
  soft gateway switch re-probes the next backend instead of leaking the
  old one's verdict (hard re-homes reload the window and reset anyway).
- boot() treats refreshSessions() as non-fatal, matching its sibling
  call sites: worst case is an empty sidebar that the next
  reconnect/turn refresh repopulates, never a bricked boot.

Tests: endpoint-missing fallback slices + scoping, sticky verdict (no
re-probe), re-probe after gateway switch, transient errors NOT
triggering fallback, and a real-hook harness test proving a rejecting
refreshSessions still completes boot with no overlay.
2026-07-20 03:40:04 -07:00
Teknium
2684e3077f chore: fix import ordering after cherry-pick conflict resolution 2026-07-20 00:41:11 -07:00
David Metcalfe
0831e5e326 fix(desktop): preserve collapsed-provider set across profile switches
The collapsed-providers atom (`hermes.desktop.collapsed-providers`) is a
global presentation-layer preference, but the catalog the picker renders is
profile-scoped (`getGlobalModelOptions` routes through `profileScoped()`,
model-menu-panel.tsx:87-93). The previous code called `pruneStaleCollapsed`
on every render against `pickerProviders`, which silently deleted any
slug not present in the active catalog.

Bug class (review from @teknium1):
- Profile switch to a catalog that lacks a previously-collapsed provider
  → that collapse preference is permanently lost.
- Refresh Models that drops a provider (revoked key, plugin disabled,
  backend policy change) → same loss.
- Any transient empty catalog (loading → []) → guarded by `length > 0`,
  but still loses state once the new (smaller) catalog resolves.

Fix:
- Remove the prune `useEffect` from model-menu-panel.tsx.
- Delete `pruneStaleCollapsed` (no other caller; the AGENTS.md "no
  speculative infrastructure" rule applies — keeping it exported with a
  plausible-sounding docstring is a foot-gun for future contributors who
  would re-call it against a single active catalog and reintroduce the bug).
- Document on the atom why we deliberately don't prune: provider slugs
  come from a bounded configured set (not user input); the render loop
  only visits providers in the active `groups`; dead slugs have no
  observable effect (`collapsedProviders.includes(slug)` against an
  absent slug is a no-op).

Tests:
- `preserves the collapsed set across a profile switch whose catalog
  lacks the slug` — regression pin for the profile-switch case.
- `preserves the collapsed set when Refresh Models drops a provider` —
  regression pin for the refresh-models case.

Verified: `tsc -p . --noEmit` is clean on the changed files. CI is the
source of truth for the vitest run (the local React-detection env
failure pre-dates this PR; CI passes).

Closes the review thread on #64690.
2026-07-20 00:41:11 -07:00
David Metcalfe
f52b6530ff feat(desktop): collapsible provider groups in model picker
Click a provider header (DEEPSEEK, GOOGLE, etc.) in the model picker dropdown
to collapse/expand its model list. State persists across sessions via localStorage.

- New provider-collapse store with persistentAtom<string[]> + stale-key pruning
- Provider headers become clickable DropdownMenuItems with chevron indicators
- textValue="" excludes headers from Radix typeahead (both reviewers flagged)
- Auto-expands the active provider so the checkmark is always visible
- Search bypasses collapse (typing shows all matching models)
- Keyboard accessible via Enter/Space on the header row
- Store double-read eliminated per cross-vendor review feedback

Reviewed-by: Flash + GPT-OSS cross-vendor review (all SHOULD-FIXs addressed)

Related: #60966 (different interaction model — hover-to-expand)
2026-07-20 00:41:11 -07:00
Ben Barclay
f0aae14c68
fix(desktop): retry OAuth cookie read on cold-start jar race (#67769)
A `persist:` partition's cookie store hydrates lazily, so the first
cookies.get() on a fresh launch can return empty for a signed-in user.
That false-negative made hasLiveOauthSession() throw "not signed in",
which on the no-retry initial boot path surfaced as the transient
"Hermes couldn't start" OAuth overlay that always cleared on Retry.

hasLiveOauthSession now reads once (no added latency on the happy path);
only on an empty read does it warm the store (flushStorageData + a
throwaway get, memoized) and re-read with a bounded ~180ms backoff
before trusting the negative. Genuinely signed-out users still resolve
false quickly and get the overlay. Fixes the whole class: the same
function backs the reconnect path and the Settings connected indicator.
2026-07-20 16:01:38 +10:00
brooklyn!
3aeded6e32
fix(desktop): scope multi-pane model UI and stabilize tile chrome (#67855)
* fix(desktop): scope multi-pane model UI and stabilize tile chrome

Composer model controls were still keyed off the primary session globals, so every tile showed the same model and a busy primary blocked switches in idle panes. Bind the pill/menu/select path to SessionView, force lone session-tile headers (incl. after tab cycle), and persist strip order so add/remove/switch stops scrambling adjacent panes.

* fix(desktop): scope preset effort/fast writes per surface, simplify tile order sync

A tile's model pick still pushed effort/fast onto the primary composer globals via applyModelPreset — scope it to the surface (primary → globals, tile → its session slice). Tile order persistence drops the before-stamping walk for a plain sort by tree encounter order; restore replays the array sequentially so array order is strip order.

* test(desktop): cover tile strip-order + selection-home; fix stale docs

Extract syncTileStripOrder's sort into a pure `orderTilesByTree` and the
selection listener's guard into `selectionHomesToWorkspace` (same shape as
the PR's lone-header extraction), then unit-test both — the two store
behaviors that shipped without coverage. Correct the `anchor`/`before` docs
(now persisted, not in-memory) and note that a tile's effort/fast edit still
writes the shared per-model preset even though the session write is scoped.

* fix(desktop): drop forbidden import() type annotations in model tests

`importOriginal<typeof import('…')>()` trips consistent-type-imports (error)
and reddens the desktop lint job. Switch to the repo's accepted top-level
`import type * as X` + `typeof X` form, matching skills/index.test.tsx.
2026-07-20 05:11:36 +00:00
brooklyn!
e702a45b5d
perf(desktop): idle-mount boot-hidden panes off the cold-start critical path (#67857)
* perf(desktop): idle-mount boot-hidden panes off the cold-start critical path

The layout tree keeps a chrome-hidden pane's content MOUNTED behind
display:none (so toggling back is instant) — but that means files, preview,
review (Shiki diff) and logs all mount their real content during first paint
even though none are visible at launch (fresh profile: no cwd, review off,
no preview target, logs not in the default tree). First paint only needs
sessions + workspace + statusbar; the rest is pure app-mount tax, the one
cold-start lever that's actually in our code (Electron startup and the
un-splittable bundle eval are not).

Wrap those four pane renders in <IdleMount>: mount on requestIdleCallback
(2s timeout fallback), then stay mounted. Idle fires within a frame of first
paint, so a hidden pane is warm before it can be revealed — zero UX change,
the instant-toggle contract intact. Degrades to eager mount where rIC is
absent (jsdom/tests), so no behavioral fork.

* refactor(desktop): collapse the four idle-mount wrappers into one idle() helper
2026-07-20 04:31:32 +00:00
brooklyn!
9c3ffcaae3
test(desktop): widen Testing Library async deadline to de-flake UI panels (#67849)
findBy*/waitFor default to a 1000ms deadline, which is too tight for
async-heavy settings panels (radix menus + refetch chains) when the full
suite runs under xdist CPU contention in CI. toolset-config-panel.test.tsx
has reddened unrelated PRs multiple times with `Unable to find ...` timeouts
that pass on re-run — the textbook contention flake.

Bump asyncUtilTimeout to 5000ms in the shared ui setup. Success still
resolves the instant the node appears; the wider deadline only absorbs a
starved runner, so happy-path speed is unchanged and only genuine failures
wait longer.
2026-07-20 04:09:40 +00:00
brooklyn!
8eb63da470
Merge pull request #67844 from NousResearch/perf/desktop-tool-row-memo
perf(desktop): stop tool rows re-rendering on session/cwd change + memo leaves
2026-07-19 23:04:13 -05:00
Brooklyn Nicholson
fb2a35c0b5 perf(desktop): stop tool rows re-rendering on session/cwd change + memo leaves
Two tool-render wins during streaming / on session switch:

1. Every ToolEntry did useStore($activeSessionId)+useStore($currentCwd), so any
   session or cwd change re-rendered *every* mounted tool row — but they're only
   read inside the preview-artifact effect. Read .get() at fire time instead
   (the effect only runs when a previewable target appears); no subscription.

2. memo() AnsiText + CompactMarkdown. Their text props are string values
   (value-equal across renders), so memo skips the re-render — and the per-tick
   ANSI parse / Streamdown re-run — when a parent ToolEntry re-renders on an
   unrelated stream delta.

No behavior change. typecheck + eslint clean; tool fallback tests green (30).
2026-07-19 22:58:22 -05:00
Brooklyn Nicholson
88cb824b14 perf(desktop): stop eagerly JSON.stringify-ing every tool's args + result
buildToolView ran prettyJson (JSON.stringify + clamp) on part.args AND part.result
for EVERY tool row, on every rebuild:
- rawArgs was dead — assigned + typed, never read anywhere. Removed.
- rawResult is only rendered by the web_search raw-JSON drilldown, yet was
  serialized for read_file/terminal/every tool. Moved to a memoized, web_search-
  only computation in the consumer (fallback.tsx), so a 100KB read_file result
  is no longer stringified just to be discarded.

No behavior change (web_search drilldown identical; clamp still applies via
prettyJson). The oversized-result guard test retargets from view.rawResult to
prettyJson (its real layer now).

typecheck + eslint clean; fallback-model tests green (26).
2026-07-19 22:50:20 -05:00
Brooklyn Nicholson
358e26a1c2 refactor(desktop): extract shared rafCoalesce helper for sash drags 2026-07-19 22:38:57 -05:00
Brooklyn Nicholson
1dffe0e670 perf(desktop): rAF-coalesce pane + console sash resizes
Both drag handlers wrote to nanostores on every pointermove — the pane sash via
setPaneWidth/HeightOverride / setTreeSplitWeights (relayouts the whole pane
tree), the preview console sash via consoleState.setHeight (reflows webview +
split). pointermove outpaces 60fps, so that's several store-driven relayouts per
frame during a drag.

Stash the latest clamped value and apply it once per frame in a requestAnimation-
Frame (the same pattern drag-session.ts / use-popout-drag.ts already use);
cleanup cancels the pending frame and commits the final position. Behavior
identical, just one relayout per frame instead of per event.

typecheck + eslint clean; preview-pane tests green.
2026-07-19 22:27:36 -05:00
Brooklyn Nicholson
0aa64ffcfc perf(desktop): targeted file-tree revalidation instead of whole-tree rescan
Rewrite of the paradigm, not just a cheaper version of it. Before, any file
mutation bumped a contentless $workspaceChangeTick and the tree re-read EVERY
loaded directory to diff — the parent state was never told what actually changed.

Now the mutation carries its path:
- workspace-events accumulates the changed dir(s) (dirname of an absolute tool
  path) and exposes consumeWorkspaceChange(); an opaque mutation (terminal, or a
  relative/unresolvable path) sets `full` instead.
- gateway-event passes toolChangedPath(payload) through on tool.complete.
- revalidateTree(cwd, change) re-reads ONLY the changed dirs that are loaded and
  patches just those subtrees — root + untouched folders never hit the FS or
  re-render. Full recursive reconcile is kept as the fallback for `full`.

So a write in one folder no longer crawls the whole tree; the opaque terminal
case still self-heals via the full path. Safe fallback everywhere a path can't be
resolved, so no change is ever missed.

typecheck + eslint clean; use-project-tree / right-sidebar / gateway-events tests green.
2026-07-19 21:56:57 -05:00
Brooklyn Nicholson
ae15742bc2 style(desktop): tighten revalidateTree comments 2026-07-19 21:46:01 -05:00
Brooklyn Nicholson
61bda4f3ca perf(desktop): stop the file tree going sticky during agent edit bursts
revalidateTree runs on every $workspaceChangeTick (mutating-tool completion,
coalesced ~500ms). Two costs per tick, gone:

1. clearProjectDirCache() wiped the gitroot + gitignore caches. But listings are
   read fresh every time (readProjectDir never caches them), so the wipe bought
   nothing except forcing a full re-read of every ancestor .gitignore — each a
   full readdir — for every loaded dir, every tick. Dropped; a .gitignore edit is
   still picked up on the next full refresh (cwd/connection change / manual).
2. reconcile awaited each child dir serially, crawling a wide/deep tree one dir
   at a time. Now Promise.all over siblings (order preserved), recursing per
   loaded subfolder.

use-project-tree.test.ts + right-sidebar/index.test.tsx green (15). tsc + eslint clean.
2026-07-19 21:43:35 -05:00
Brooklyn Nicholson
13337edcbe refactor(desktop): merge the two windowed diff returns into one 2026-07-19 21:20:36 -05:00
Brooklyn Nicholson
5ecf06e0ed perf(desktop): virtualize the review-pane diff (no more full-Shiki freeze)
Selecting a large changed file in the review pane froze it: FileDiffPanel with
no fullText + no showLineNumbers rendered SyntaxDiff over EVERY line — a full
Shiki highlight + thousands of mounted DOM nodes — because windowing was tied to
showLineNumbers/fullText and the review call had neither.

Decouple windowing from the gutter:
- `windowed = showLineNumbers || virtualized`; windowed paths always render the
  fixed-row chunked body (TokenizedDiffBody chunked / PreviewDiffRows), never
  SyntaxDiff, so only visible rows mount.
- New `virtualized` prop → windowed scroller WITHOUT the line-number gutter.
- Review passes `virtualized` + the preview's fill className.

Preview (showLineNumbers + fullText) and tool-card (compact) render byte-for-byte
as before — the gutter body just reads the same chunked window it already used,
and the no-fullText+highlight case (previously SyntaxDiff) now windows too.

tsc + eslint clean. Visual paths preserved by construction; needs an in-app
eyeball on a large review diff.
2026-07-19 21:13:08 -05:00
nousbot-eng
26480e6c57
fmt(js): npm run fix on merge (#67793)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-20 01:10:16 +00:00
brooklyn!
04113b5a89
Merge pull request #67742 from NousResearch/perf/desktop-streaming-rerenders
perf(desktop): stop per-token sidebar + tool-row re-renders during streaming
2026-07-19 21:02:48 -04:00
Brooklyn Nicholson
b6df712f44 refactor(desktop): DRY the computed-dedup into stableArray + freeze
One shared `stableArray(prev, next)` helper replaces the duplicated
element-equal/keep-prev logic in both stores, and freezes the shared ref so a
future in-place mutation fails loud instead of silently corrupting the cache.
Computed return type is now `readonly string[]` (it always was, immutably).
2026-07-19 19:46:09 -05:00
nousbot-eng
a7d7c02cb6
fmt(js): npm run fix on merge (#67771)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-20 00:06:43 +00:00
Austin Pickett
3d97893571
feat(desktop): custom endpoint settings (supersedes #42745) (#67759)
* feat(desktop): add custom endpoint settings (supersedes #42745)

Salvages PR #42745 (elashera:custom-endpoints-desktop), which could no
longer merge cleanly against main. Re-integrated the work onto current
main and reconciled the conflicts:

- Settings nav: wired the new 'Custom Endpoints' provider sub-view into
  main's data-driven navGroups/OverlayNav layout (PR predated that
  refactor) and added it to PROVIDER_VIEWS.
- providers-settings: kept BOTH main's LocalEndpointRow affordance and
  the PR's fuller CRUD panel; unified ProvidersSettingsProps to carry
  onClose + onConfigSaved + onMainModelChanged.
- web_server: kept main's _normalize_main_model_assignment + api_key
  propagation AND the PR's provider base_url lookup in
  _apply_model_assignment_sync.
- model_switch: dropped the PR's bare direct-custom-config picker block;
  main already implements it (source='model-config', with live model
  discovery). Updated the salvaged test to assert main's behavior.
- Merged additive import/type blocks in hermes.ts and types/hermes.ts.

Backend endpoints, i18n labels (en/ja/zh/zh-hant), and the
custom-endpoints-settings.tsx panel carried over. 28 custom-endpoint
tests pass.

Co-authored-by: elashera <emilio.jesus.lasheras.romero@nttdata.com>

* chore(contributors): map elashera's commit email

Salvage of #42745 (superseded by #67759) preserves @elashera's
authorship, whose corporate commit email had no contributor mapping.
Adds contributors/emails/ mapping so check-attribution passes.
Verified: GitHub user 'elashera' id=135239963 matches their own
noreply commit email (135239963+elashera@users.noreply.github.com).

---------

Co-authored-by: elashera <emilio.jesus.lasheras.romero@nttdata.com>
2026-07-19 19:59:46 -04:00
nousbot-eng
57063ad47f
fmt(js): npm run fix on merge (#67749)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-19 23:42:14 +00:00
Austin Pickett
33d71d687f
fix(desktop): preserve new-chat selector choices (#67729)
Salvaged and rebased from #66354 by @UnathiCodex onto current main.

Fixes a fresh-chat race in Hermes Desktop where a model, reasoning-effort,
or Fast selection made before the first Send could be replaced by an
in-flight profile refresh, or read only after the profile handshake
yielded. Send is now the linearization point: the visible selector state is
snapshotted before awaiting profile readiness, and intent-generation guards
make older config/model responses stand down after a picker/toggle action.
Adds the contract-v4 session-create wire contract for explicit Fast=false.

Conflict resolution vs the original branch (use-model-controls.ts / .test.tsx):
combined main's catalog-aware keepManualPick() sticky-pick logic with the
PR's profileRefreshEpoch + composerSelectionGeneration staleness guards so
both a removed-from-catalog reseed and the in-flight-picker race are handled.

Verified on current main: apps/desktop tsc --noEmit clean; 80 affected
UI/store tests pass (use-model-controls, use-hermes-config,
use-session-actions, model-edit-submenu, model-presets, updates).

Co-authored-by: UnathiCodex <theunathi@gmail.com>
2026-07-19 19:31:34 -04:00
Austin Pickett
bc6839aa37
fix(desktop): stop hard-failing pack on non-git checkouts + fix ZIP-path autocrlf (supersedes #67643) (#67730)
* fix(desktop): allow write-build-stamp from non-git checkouts

Stop hard-failing npm pack when neither GITHUB_SHA nor git HEAD is
available (ZIP installs / broken .git). Emit an explicit fallback stamp
instead so local Windows desktop builds can finish (#50823).

* fix(desktop): treat fallback stamps as unpinned; harden Windows install

Keep all-zero fallback commits out of -Commit/--commit pins and fetch
install.ps1 by branch instead. After bootstrap, pin the marker to the
checkout HEAD so isBootstrapComplete accepts it. On Windows, force ZIP
checkout, seed GITHUB_SHA (ASCII-only install.ps1), and avoid the pack
stamp failure.

* fix(install): pin core.autocrlf=false before ZIP-path checkout (#50823 review)

The ZIP-fallback path added in #67643 runs `git checkout -f FETCH_HEAD`
before core.autocrlf gets pinned (which only happened later, on the
shared clone-path config). On Git for Windows -- where core.autocrlf
defaults to true -- that renormalizes the repo's LF text files to CRLF in
the working tree during checkout, leaving the freshly-created managed
checkout dirty versus HEAD and aborting the next `hermes update`. That is
the exact "dirty tree the user never touched" failure the surrounding
code already guards against (install.ps1:1461-1469, 1750-1753).

Move the `config core.autocrlf false` pin to run immediately after
`git init`, before the fetch/checkout. The later idempotent pin on the
shared clone path is retained so git-clone installs are unaffected.

Addresses teknium1's review on #67643 and supersedes it, preserving the
original author's two commits.

Co-authored-by: HexLab98 <8422520+HexLab98@users.noreply.github.com>

* chore(contributors): map austinpickett commit email for attribution

The check-attribution CI gate flagged austinpickett@users.noreply.github.com
as an unmapped commit-author email (introduced by the autocrlf fix commit
on this PR). Add the per-email mapping file as the gate instructs (the
legacy AUTHOR_MAP in scripts/release.py is frozen).

---------

Co-authored-by: HexLab98 <liruixinch@outlook.com>
Co-authored-by: austinpickett <austinpickett@users.noreply.github.com>
Co-authored-by: HexLab98 <8422520+HexLab98@users.noreply.github.com>
2026-07-19 19:29:36 -04:00
Brooklyn Nicholson
5f154e881c perf(desktop): stop per-token sidebar + tool-row re-renders during streaming
Two real render-cost wins found by inspection (no behavior change):

1. Sidebar re-rendered on every stream token. $sessionStates is republished on
   every message delta (tens/sec during a turn), and the derived ID computeds
   ($workingSessionIds, $attentionSessionIds, $backgroundRunningSessionIds)
   allocated a fresh array each time. nanostores notifies on !==, so the whole
   ChatSidebar + every mounted row re-rendered per token even when the working/
   attention/background set was unchanged. Return the previous array reference
   when the contents match → nanostores skips the notify unless the set actually
   changes. Turns streaming from O(visible rows)/token into O(0) for the sidebar.

2. Tool rows normalized the FULL uncapped detail every render. `looksRedundant`
   (lowercase + whitespace-collapse over the entire read_file/terminal payload)
   ran twice in the ToolEntry render body, so every completed tool re-normalized
   its whole output on every stream tick of the running message. Memoize on the
   view fields so it recomputes only when the tool's content changes.

Both are correctness-preserving (stable refs + memoization). The CI stream
scenario drives $messages directly, not the publishSessionState path, so it
won't reflect #1 — verified by inspection.
2026-07-19 18:29:22 -05:00
brooklyn!
8142331616
bench(desktop): measure representative (warm-cache) cold start (#67733)
Profiling the boot answered "is there a real cold-start win?": no wasteful
hotspot — the renderer does only ~tens of ms of work at mount, no heavy library
(shiki/mermaid/katex/d3/motion) initializes at startup; the rest is Electron
runtime + waiting, near the Electron floor.

It also exposed that the cold-start number was pessimistic: a fresh
--user-data-dir per run means a COLD V8 code cache and worst-case bundle
recompile every launch. Real users reuse their profile. Measured delta:
  fresh (cold cache):  spawn→interactive ~1.48s
  reused (warm cache): ~1.0s
So representative launch is ~1.0s; only first-launch-after-install pays ~+400ms.

- coldStartSamples() reuses one profile (run 0 warms the cache, discarded;
  runs 1..N are warm samples), stepping ports + pausing so the single-instance
  lock releases. `--cold-fresh` measures the first-launch worst case.
- Re-baselined cold-start with the representative warm numbers.

Net: nothing high-ROI left to optimize. The only lever is shipping a pre-warmed
V8 code cache to make first launch match warm (~400ms, once per update) — real
packaging complexity for a marginal win, deliberately not pursued.
2026-07-19 23:21:45 +00:00
brooklyn!
b6ae910d8c
bench(desktop): trustworthy cold-start measurement (code-splitting is not the lever) (#67720)
* bench(desktop): measure the full picture — prod build, cold-start, first-token

Stop drip-feeding scenarios: extend the harness to cover the latencies that
actually dominate perceived speed, and measure them on a REAL production build.

- --prod: build a production renderer with the probe included (VITE_PERF_PROBE=1,
  off in normal builds) and launch it from dist/. Measures minified React, so
  numbers are representative shipped figures instead of ~3x-inflated dev ones.
- cold-start scenario (tier "cold"): launch → CDP → driver → first paint, via a
  fresh isolated spawn per run. Captures spawn_to_cdp_ms, spawn_to_driver_ms, fcp_ms.
- first-token scenario (backend tier): Enter → first assistant token painted —
  the TTFT latency an agent app is uniquely judged on.
- run.mjs gained --prod (build once), cold-start fresh-spawn loop, and gates
  ci+cold tiers against the baseline.

Baseline re-captured on a PRODUCTION build (median of 5), darwin-arm64 — all
green. Representative numbers:
  cold-start  spawn→interactive ~1.6s, FCP ~0.5s
  stream      frame p95 22ms, 1 longtask
  keystroke   p50 2ms, p95 8.7ms
  transcript  mount 145ms, 82ms longtask (400-msg open)

The prod build also settled the open question from the dev numbers: the
transcript-mount "lead" (221ms longtask in dev) is only ~72-82ms in prod — not
actionable. Measurement did its job.

* bench(desktop): trustworthy cold-start measurement (code-splitting is NOT the lever)

Investigated code-splitting the ~22MB renderer bundle to cut cold start. It is
the wrong fix on both counts:

1. Intentional design: vite.config disables codeSplitting because Shiki emits
   thousands of dynamic chunks and electron-builder OOMs scanning them — a
   packaging/installer constraint, not an oversight.
2. The data says it wouldn't help. Fixing the cold-start measurement to be
   trustworthy and reading the boot composition (prod build):
     spawn → interactive ~1.5s
     renderer nav → DOMInteractive ~0.8s, → DOMContentLoaded ~1.06s
   so the whole 22MB bundle EVAL is only ~0.27s (DCL − DOMInteractive) of the
   ~1.5s. The dominant costs are Electron/window startup and React app mount —
   neither touched by splitting.

The measurement fixes (the real content of this PR — no app change, since the
optimization was rejected):
- Drop HERMES_DESKTOP_BOOT_FAKE from spawned instances — it injected artificial
  per-phase boot-overlay sleeps that inflated cold-start (and slowed every run).
- Unique debug/dev port per cold-start run — a just-killed instance can hold
  :9222 briefly, so reusing it made CDP attach to the DYING instance and report
  garbage (spawn_to_cdp of ~4ms). Stepping the port per run fixes the race.
- Richer boot marks (dom_interactive, dom_content_loaded, main-script size) so
  cold-start composition is visible, not just a single number.
- Forward all numeric boot marks from the cold-start loop.
- Re-baseline cold-start with the clean numbers.

A real cold-start win would target Electron startup / app-mount (e.g. V8 code
cache, deferred non-critical mount) — a future pass, now that it's measurable.
2026-07-19 18:13:04 -05:00
brooklyn!
b1fb3c5285
bench(desktop): measure the full picture — prod build, cold-start, first-token (#67697)
Stop drip-feeding scenarios: extend the harness to cover the latencies that
actually dominate perceived speed, and measure them on a REAL production build.

- --prod: build a production renderer with the probe included (VITE_PERF_PROBE=1,
  off in normal builds) and launch it from dist/. Measures minified React, so
  numbers are representative shipped figures instead of ~3x-inflated dev ones.
- cold-start scenario (tier "cold"): launch → CDP → driver → first paint, via a
  fresh isolated spawn per run. Captures spawn_to_cdp_ms, spawn_to_driver_ms, fcp_ms.
- first-token scenario (backend tier): Enter → first assistant token painted —
  the TTFT latency an agent app is uniquely judged on.
- run.mjs gained --prod (build once), cold-start fresh-spawn loop, and gates
  ci+cold tiers against the baseline.

Baseline re-captured on a PRODUCTION build (median of 5), darwin-arm64 — all
green. Representative numbers:
  cold-start  spawn→interactive ~1.6s, FCP ~0.5s
  stream      frame p95 22ms, 1 longtask
  keystroke   p50 2ms, p95 8.7ms
  transcript  mount 145ms, 82ms longtask (400-msg open)

The prod build also settled the open question from the dev numbers: the
transcript-mount "lead" (221ms longtask in dev) is only ~72-82ms in prod — not
actionable. Measurement did its job.
2026-07-19 17:52:39 -05:00
brooklyn!
dd418284db
bench(desktop): trustworthy --spawn stream numbers + real baseline (#67694)
Chased the "stream frame p95 = 60ms with ZERO longtasks" mystery to its actual
cause: the default stream chunk had no paragraph breaks, so it grew into one
giant ~22KB block that re-rendered fully every flush — defeating the block
memoization real streaming relies on. Plain text = 21ms; realistic chunk with
`\n\n` breaks (blocks settle, only the tail re-renders) = 23ms. Fixed the
default chunk to model real LLM output; a break-less `--chunk` remains available
as a single-block worst-case stress.

Also hardened the isolated instance so measurements reflect real cost:
- Wait for the gateway socket to actually connect before measuring (a booting/
  absent backend's reconnect backoff churns the main thread). Exposed via a new
  __PERF_DRIVE__.connected() probe reading $gateway.connectionState.
- Focus emulation + anti-throttle/occlusion flags so a backgrounded perf window
  isn't frame-throttled (no OS focus stealing).
- Generation-guarded the rAF frame recorder so repeated runs don't leave
  overlapping recorders polluting frame intervals.

Baseline re-captured as the median of 5 --spawn runs (darwin-arm64); all three
CI scenarios now green and stable. Absolute values are dev-build (noted in
_meta) — regression guards, not shipped numbers.
2026-07-19 21:30:40 +00:00
brooklyn!
0d2ad3993e
feat(desktop): per-session color override (#66565 layer 2) (#67681)
Add a color picker to the session menu (an Appearance submenu of reusable
ColorSwatches, in both the dropdown and right-click flavors). The pick is a
per-session override that wins over the inherited project color; clearing
falls back to it.

Storage is desktop-local like pins ($sessionColorOverrides persistentAtom),
keyed by the DURABLE lineage id so a color survives auto-compression's id
rotation. Precedence folds into the existing $sessionColorById resolver, so
sidebar rows AND pane tabs pick it up with no changes to either — the payoff
of the shared store. To take this to the TUI later, promote this one atom to
a backend SessionInfo.color field; the resolver and picker stay put.
2026-07-19 16:05:16 -05:00
brooklyn!
1b17015f7a
refactor(desktop): tidy session-color pass (#67671)
- sessionColorFor: drop the no-op `?? undefined` (the map read is already
  string | undefined).
- sessionProjectColor: fix a now-stale doc line — a rootless (no cwd AND no
  git_repo_root) row returns null, not any cwd-less row (repo-root-only rows
  resolve since the grouped-but-grey fix).
- ProjectMenu.applyAppearance: await instead of a .then block; flatten the
  auto-branch's nested ternary.
2026-07-19 15:37:53 -05:00
brooklyn!
3345b3cdfd
bench(desktop): make --spawn work + capture a real baseline (#67670)
- Resolve the vite CLI via vite/package.json `bin` (Vite 8's exports block
  importing vite/bin/vite.js directly — --spawn failed with ERR_PACKAGE_PATH_NOT_EXPORTED).
- Add a post-launch settle so cold-start contention (vite dep pre-bundling,
  first backend-connect attempts) doesn't contaminate the first scenario.
- Drop the raw autolink from the default stream chunk (resolvable URLs trigger
  link-embed DNS lookups unrelated to render cost).
- Replace seed baseline with real numbers from a darwin-arm64 --spawn run.
  keystroke + transcript are clean; stream is a clean single-run capture (the
  isolated backend may not connect, and its reconnect churn inflates frame
  pacing — re-capture on a connected instance for tighter tolerances).
2026-07-19 15:37:35 -05:00
mark
e361c5e204 fix(desktop): support spaced Windows Git paths in review
simple-git's custom-binary validation rejects paths containing spaces, so
the default Windows Git install (C:\Program Files\Git\cmd\git.exe) made
every Review pane git call throw and the pane silently showed 'No diffs'.

The binary is resolved inside the Electron main process from known install
locations or PATH — never renderer/user input — so for spaced paths we opt
into simple-git's supported unsafe.allowUnsafeCustomBinary escape hatch
rather than falling back to PATH (often absent in GUI-launched apps).

Simplified from PR #64713 by @unsupportedpastels; supersedes the 8.3
short-path approaches in #55337/#60156.

Fixes #54888
2026-07-19 12:22:23 -07:00
digitalbase
5f2bfb6631 fix(desktop): scope the cron jobs list to the active profile
Salvaged from #42654 by @digitalbase (earliest report of the leak, June 9):
the desktop sidebar and cron overlay showed EVERY profile's jobs because
GET /api/cron/jobs defaults to profile=all and the desktop never sent the
param — profileScoped() (landed in #67493) routes the backend process but
adds no endpoint filter on local pools.

- hermes.ts: getCronJobs(profile?) appends ?profile= when given; omitting
  the arg keeps the legacy unfiltered path. profileScoped() still rides
  along for process routing.
- use-session-list-actions.ts: sidebar cron refresh passes the sidebar's
  profile scope (concrete profile → own jobs; ALL_PROFILES → 'all').
- app/cron/index.tsx: the cron overlay's refresh uses the same scope so
  the overlay and sidebar (shared $cronJobs atom) always agree.
- Tests: list ?profile= contract in hermes-cron-scope.test.ts; sidebar
  scoping in use-session-list-actions.test.tsx.

Reworked onto current main per the sweeper review: threaded through the
existing profileScoped()/list-param seams instead of the original PR's
pre-refactor call sites (DesktopController has since delegated to
use-session-list-actions).
2026-07-19 12:19:22 -07:00
isfttr
0385e15544 test(desktop): contract test — every cron helper is profile-scoped
Salvaged from #59888 by @isfttr: the profileScoped() fix itself landed
via #67493 (salvaged from the earlier #49948), but this PR contributed a
contract test locking all 9 cron helpers to the active gateway profile —
omitted when none is set (single-profile users unaffected), attached when
one is active. Keeps the multi-profile/remote cron routing from silently
regressing.
2026-07-19 10:17:52 -07:00
Gille
6e676c768c fix(desktop): profile-scope all cron REST calls
Salvaged from #49948 by @helix4u: every desktop cron API call
(list/get/runs/create/update/pause/resume/trigger/delete) now carries
profileScoped(), so global-remote mode routes the request to the profile
the UI is acting for instead of silently hitting the primary backend's
default profile.
2026-07-19 09:57:21 -07:00
nousbot-eng
36f2a966c7
fmt(js): npm run fix on merge (#67491)
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-19 12:52:29 +00:00