Commit graph

327 commits

Author SHA1 Message Date
embw_l0x
9fed768b56
fix(desktop): scope model options by profile (#62795)
Co-authored-by: embwl0x <embwl0x@users.noreply.github.com>
2026-07-22 09:40:50 -04:00
brooklyn!
3de91c6974
fix(desktop): stop long-session transcript from drifting to old turns (#69019)
content-visibility:auto on turn groups (perf: off-screen turns skip
style/layout/paint) pairs with contain-intrinsic-size:auto, which only
remembers a turn's size after it renders. A turn that finished streaming
near the bottom had its smaller mid-stream size remembered; once it
scrolled off the top edge and got skipped, it collapsed to that stale
height. With overflow-anchor:none the viewport can't self-correct, so the
stick-to-bottom lock drifts and the view creeps up over older turns — the
'long session eventually shows old responses' visual glitch.

Exempt the newest turns (live tail) from virtualization so a turn is only
ever skipped after its layout has settled at its final size (remembered ==
real -> skipping changes no height). Off-screen older turns still skip, so
the dialog/popover whole-document recalc win on long transcripts is kept
(it scales with the hundreds of old turns, not the small tail).
2026-07-21 20:30:42 -05:00
Brooklyn Nicholson
a1a406fd40 feat(desktop): type-to-focus the composer from empty chat chrome
Printable keys and soft `/`/Enter pull focus back to the composer when
nothing else owns the key (dialogs, terminal, buttons on Enter, …).
2026-07-21 16:03:19 -05:00
yoniebans
25f4ba7b3c Merge remote-tracking branch 'origin/main' into feat/desktop-remote-ssh-current
# Conflicts:
#	apps/desktop/electron/connection-config.test.ts
#	apps/desktop/electron/connection-config.ts
#	apps/desktop/electron/main.ts
2026-07-21 10:16:01 +02:00
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
6fbb4cea00
Merge pull request #65805 from NousResearch/ethie/e2e
Desktop E2E: Playwright suite with visual regression diffs
2026-07-20 15:19:05 -04:00
ethernet
2e10d7b942 fix(desktop): address review — overlay a11y, e2e typecheck, nits
Blocking #1 — gateway-connecting-overlay.tsx reduced-motion regression:
the top `if (reduce) setPhase('gone')` fired unconditionally on mount
whenever reduce-motion was on, so every OS reduced-motion user lost the
CONNECTING overlay during cold boot entirely (jumped to 'gone' before the
gateway was even open). The intent was to skip the exit *choreography*,
not to skip showing the overlay. Removed the unconditional top block and
the redundant nested preview block; kept only the third branch
(`gatewayState === 'open' && shownRef.current` → `reduce ? 'gone' :
'text-out'`) which correctly gates the short-circuit on connect. Also
fixed `if(reduce)` missing-space, 6-space misindent, and the same 3-line
comment pasted three times.

Nit #1 — tsconfig excludes e2e, so specs were never typechecked in CI.
Added tsconfig.e2e.json (extends base, includes e2e/ + playwright.config.ts,
adds @playwright/test types) and wired it into the typecheck script. This
surfaced three latent type errors that are fixed in the same commit:
  - fix-electron-tracing.ts: `app._context` and `electron._playwright` are
    private APIs — added `as any` on the access before the existing cast.
  - playwright.config.ts: `reducedMotion: 'reduce'` directly under `use:`
    is not a valid UseOptions property in playwright 1.58; it's a
    BrowserContextOption accessed via `contextOptions: { reducedMotion:
    'reduce' }`. The old form was silently ignored at runtime, so
    reduced-motion emulation wasn't actually active — screenshots could
    catch overlays mid-fade (exactly what the comment warned about).

Nit #2 — fix-electron-tracing.ts reaches into Playwright internals
(_playwright, _allContexts, _context) with no public contract. Added a
header comment calling out the `@playwright/test` exact pin (=1.58.2) so a
future bump knows to re-verify the private symbols still exist.

Nit #3 — main.ts TEST_WORKER_INDEX block had stray 6-space indentation.

Verified: tsc -p . && tsconfig.electron && tsconfig.e2e → 0 errors;
vitest boot-failure-overlay (3/3) + boot-failure-reauth (21/21) pass;
npm run build clean; playwright e2e/boot-failure.spec.ts 2/2 pass.
2026-07-20 14:37:39 -04:00
ethernet
33c154e41f revert(desktop): restore Preparing error state in onboarding
Reverts the Preparing component changes from b2857110b so the progress bar
turns red (bg-destructive) and the error text shows below it when boot.error
is set, instead of bailing out with an early return null.

The corresponding e2e guard in waitForBootFailure (e2e/fixtures.ts) that
rejected any progress bar in the DOM is dropped — it now waits for the
failure dialog (Retry/Repair/Use local gateway/Connection settings) or the
"Desktop boot failed" toast. The boot-failure.spec.ts header comment is
updated to match.

Verified: tsc clean, vitest boot-failure-reauth (21/21) + boot-failure-overlay
(3/3) pass, npm run build clean, playwright e2e/boot-failure.spec.ts 2/2 pass.
2026-07-20 14:32:13 -04:00
ethernet
470c7e2a60
fix(desktop): prevent timers from shifting as they count (#68131)
LiveDuration returned a bare string with proportional digits, so the
statusbar reflowed every second as the timer ticked. Extract a shared
StableText component that renders each character in its own 1ch-wide
cell, preventing any digit from shifting the layout — works with the
proportional sans font, no need for font-mono.

Both LiveDuration (statusbar session/running timers) and
ActivityTimerText (tool activity timers) now use StableText, dropping
the font-mono + tabular-nums workaround from the latter.

Also renames statusbar.ts → statusbar.tsx since LiveDuration now
returns JSX.
2026-07-20 16:39:20 +00:00
ethernet
b2857110b4 fix(desktop): kill loading bar in boot-failure e2e screenshots
The boot-failure screenshot showed a progress bar because of two bugs:

1. waitForBootFailure matched on "Let's get you setup" (the onboarding
   header that mounts from frame 1 during normal boot), so the screenshot
   fired at ~86% progress while the Preparing component's progress bar was
   still painted.

2. The Preparing component kept rendering the progress bar even after
   boot.error was set — it just turned the bar red and appended the error
   text below it.

Fixes:
- Preparing bails out (returns null) when boot.error is set, so
  BootFailureOverlay (z-1400) owns the screen exclusively.
- applyDesktopBootProgress no longer clobbers a previously-set boot.error
  when a late progress event arrives with error: null — failDesktopBoot is
  terminal for the boot cycle.
- waitForBootFailure guards against progress bars being visible and matches
  on actual failure signals (error toast, Retry/Repair buttons), not the
  onboarding header.
- setupDeadBackend now accepts { fakeError: true } which injects
  HERMES_DESKTOP_BOOT_FAKE_ERROR to trigger a real boot failure — the
  previous dead-provider fixture never actually caused a boot failure
  (hermes serve starts fine; the dead endpoint only matters at chat time).
- boot-failure.spec.ts updated to use { fakeError: true }.

Verified: e2e test passes with 0 progress bars in the DOM at screenshot
time (confirmed via DOM inspection), 16/16 vitest tests pass, typecheck
clean.
2026-07-20 11:44:41 -04:00
ethernet
0fd12ca11b fix(desktop): kill boot overlay fade-race in e2e screenshots
Screenshots were catching the CONNECTING overlay and onboarding Preparing
loading bar mid-transition because the wait helpers fire on text content
while visual state lags behind. Fix at the source via reduced motion:

- playwright.config.ts: emulate prefers-reduced-motion: reduce
- styles.css: blanket reduced-motion rule kills all CSS animations/transitions
- gateway-connecting-overlay.tsx: skip JS setTimeout exit choreography
  (text-out 360ms + hold 300ms + overlay fade 520ms) — jump straight to gone
- decode-text.tsx: skip scramble interval, render resolved text immediately
2026-07-20 11:44:41 -04: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
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!
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
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
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
Teknium
1bf441cd19
feat(desktop): per-job model picker in the cron create/edit dialog (#67472)
The cron backend has always supported per-job model/provider pins (the
dashboard web UI and the cronjob tool expose them), but the desktop app's
cron editor had no way to set one — every job silently ran on the global
default model.

- Cron editor gains an optional Model select, grouped by provider, fed by
  the same model.options catalog as the chat model picker (configured
  providers with available models only, curated order preserved).
- Resetting to 'Default (global model)' clears a previous pin (model and
  provider written as null); script-only (no_agent) jobs never touch the
  model fields since the scheduler ignores overrides for them.
- A pinned model that has since left the catalog stays visible and
  re-selectable instead of rendering Radix's blank trigger.
- Job detail pane shows the pinned model when one is set.
- ui/select grows SelectGroup + SelectLabel primitives for the grouped list.
- CronJob/CronJobCreatePayload/CronJobUpdates types carry model/provider;
  en/ja/zh/zh-hant locales add the two new labels.

The cronjob model tool schema is intentionally unchanged — model selection
stays a user-facing UX affordance, not an agent-facing tool parameter.
2026-07-19 05:23:55 -07:00
brooklyn!
ad0d21188f
feat(desktop): session color — inherit from project, shared across sidebar and tabs (#67469)
* feat(desktop): inherit project color on session rows

Sessions that belong to a colored project now pick up that color as the
sidebar row's idle lead dot, so work/personal/project buckets are legible
at a glance (Layer 1 of #66565). Derived from the same project membership
the sidebar already groups by; active states (working / needs-input /
background / unread) still own the dot so the tint never fights an
attention cue.

* feat(desktop): share session color across sidebar rows and pane tabs

Route session color through one computed store ($sessionColorById) that
both the sidebar rows and the pane tabs read, so a session and its tab can
never show different colors. Recomputed only when the session list or
projects change (cold atoms — the streaming pulse lives elsewhere) and read
as an O(1) lookup, never re-derived per render.

Tabs previously had no color at all: the strip renders only a title string.
Add a generic `accent` to the pane contribution that the tab strip paints as
a lead dot; the session tiles (via paneMirror) and the main workspace tab
(syncWorkspaceTitle) feed it from the same shared map. Precedence now lives
in one place, ready for per-session override / agent-set color (#66565).

* fix(desktop): resolve session color for repo-root-only sessions

liveSessionProjectId bailed the instant a session had no cwd, so an
older/imported session carrying only a git_repo_root — which the backend
still groups under its project — got no project and rendered a grey idle
dot instead of the project color ("grouped but grey"). Anchor on the repo
root when cwd is absent, matching how the sidebar grouped the row, and keep
the sibling-worktree guard for the cwd-present case.
2026-07-19 07:41:47 -04:00
Brooklyn Nicholson
897f3da276 feat(desktop): share session color across sidebar rows and pane tabs
Route session color through one computed store ($sessionColorById) that
both the sidebar rows and the pane tabs read, so a session and its tab can
never show different colors. Recomputed only when the session list or
projects change (cold atoms — the streaming pulse lives elsewhere) and read
as an O(1) lookup, never re-derived per render.

Tabs previously had no color at all: the strip renders only a title string.
Add a generic `accent` to the pane contribution that the tab strip paints as
a lead dot; the session tiles (via paneMirror) and the main workspace tab
(syncWorkspaceTitle) feed it from the same shared map. Precedence now lives
in one place, ready for per-session override / agent-set color (#66565).
2026-07-19 00:29:25 -04:00
nousbot-eng
ffc69c184b
fmt(js): npm run fix on merge (#67307)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-19 04:11:15 +00:00
brooklyn!
7db2decbea
Merge pull request #67302 from NousResearch/bb/salvage-66870-focus-tab-hijack
fix(desktop): don't hijack the active tab on reactive pane unhide (supersedes #66870)
2026-07-19 00:03:55 -04:00
Brooklyn Nicholson
fbb867f54f fix(desktop): don't hijack the active tab on reactive pane unhide
In the Focus layout `files` shares one tab group with `workspace`, so when the
first reply adopts a cwd the reactive files unhide fronted files and yanked the
active tab off the new session (~1s after the reply). #65375 fixed the sibling
"reactive unhide reopens a collapsed side" bug but frontPaneInGroup still stole
the active slot unconditionally.

Only take the active slot when the group's current active pane isn't itself
showable, so a reactive unhide can't steal focus from a pane the user is
viewing while still fronting a valid tab when nothing is shown.

Carries forward linfeng961's diagnosis + fix from #66870, reworked onto the
frontPaneInGroup path introduced by #65375.

Co-authored-by: linfeng961 <133505766+linfeng961@users.noreply.github.com>
2026-07-18 23:58:42 -04:00
nousbot-eng
78f38e79cf
fmt(js): npm run fix on merge (#67297)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-19 03:57:22 +00:00
David Metcalfe
4cdfcf568d
fix(desktop): don't auto-expand user-collapsed side on reactive unhide (#65375)
* fix(desktop): don't auto-expand user-collapsed side on reactive unhide

When `setTreePaneHidden(paneId, false)` was called for a workspace-gated
pane like `files` (bound to $hasWorkspace), it auto-called
`revealTreePane(paneId)` — which expanded the parent column even when
the user had explicitly collapsed it via Cmd+J.

That meant every session create / resume — anything that flipped
$currentCwd from empty to non-empty — silently re-opened the right
sidebar and persisted that open state to localStorage, so the original
session also showed the sidebar as open on return.

`setTreePaneHidden` is a state primitive; user-intent semantics (open
the side, front the tab) belong to `revealTreePane`. Drop the auto-call
and replace it with a narrower `frontPaneInGroup` helper that only
makes the pane the active tab in its group — visible the next time the
column is opened, without forcing it open now.

* fix(desktop): preserve explicit review reveal

---------

Co-authored-by: David Metcalfe <80915+DavidMetcalfe@users.noreply.github.com>
2026-07-18 23:49:37 -04:00
Brooklyn Nicholson
e30174fa17 perf(desktop): scope tool-diff subscriptions + narrow profile query invalidation
Two structural fixes from the Desktop performance audit (P2 tier):

1. Scope live tool-diff subscriptions. `ToolEntry` subscribed to the whole
   `$toolDiffs` map via `useStore`, so one `recordToolDiff` re-rendered every
   mounted tool row. Add a cached per-toolCallId derived atom
   (`$toolInlineDiff(id)`, mirroring the existing `$toolDisclosureOpen` pattern);
   computed() only notifies when that id's diff string changes, so a live patch
   re-renders one row.

2. Narrow profile / gateway-switch query invalidation. Both the active-profile
   subscription and `wipeSessionListsForGatewaySwitch` called keyless
   `queryClient.invalidateQueries()`, refetching account/marketplace/onboarding
   caches on every switch. Add `invalidateProfileScopedQueries()` with a
   denylist of profile-independent roots (billing, marketplace-themes,
   onboarding-model-options, contrib-logs-tail). A denylist is correctness-safe:
   a root we forget just refetches (cheap), whereas an allowlist that misses a
   profile-scoped key would paint the previous profile's data.

Tests: per-tool notify isolation, and real-QueryClient invalidation partition
(profile-scoped invalidated, global left intact, unknown keys invalidated).
2026-07-18 19:15:57 -04:00
Brooklyn Nicholson
bd4953b30d perf(desktop): incremental block lexing for streaming markdown — 14x less splitter CPU on long replies
Fourth profiling round (#66033/#66347/#66470). Streamdown's
parseMarkdownIntoBlocks is a full `marked` lex of the entire message,
and during streaming every flush is a new string — so the splitter paid
O(full-text) ~30x/s: benchmarked 3.4-9.6ms per call at 64-192KB, i.e.
15-30% of a core burned re-lexing settled text on long agent replies.
(The rest of the May-2026 "re-parse elephant" was already eaten by
tailBoundedRemend, block-memo, the KaTeX memo, and deferred shiki —
the splitter was the last O(full-text) pass besides preprocess, which
benches at only 1.5-4.6ms and is left alone.)

New src/lib/markdown-blocks.ts (extracted from markdown-text.tsx) wraps
the splitter with two caches:

- the existing exact-string LRU (remounts: virtualizer scroll, session
  switch) — moved, unchanged;
- a streaming-append cache: when the new text startsWith a recently
  parsed text, reuse that parse's blocks up to a settled boundary and
  lex only the suffix. The boundary drops trailing whitespace-only
  blocks plus the last content block — the only block appended text can
  reinterpret (open fence, list/table continuation, setext underline,
  lazy blockquote). Earlier blocks are separated by settled blank lines
  and can't change. Cross-block reference links can't regress:
  Streamdown already renders each block as an independent document.

Safety: the splitter's blocks.join('') === text property makes offsets
exact and is defensively re-checked; any mismatch or non-append rewrite
(edit, branch swap) falls back to the full lex — byte-for-byte the old
behavior.

Property tests: at every random streaming cut over a corpus covering
fences, loose lists, tables, setext headings, lazy blockquotes, HTML
blocks, and display math — and token-by-token through a fence boundary
— the cached splitter's output is asserted deep-equal to a fresh full
lex. Bench (128KB reply, 200 flushes): 990ms -> 72ms total splitter CPU
(4.95 -> 0.36ms/flush).

Verification: tsc clean; eslint/prettier clean; lib + assistant-ui
suites green (473 tests).
2026-07-18 17:45:55 -04:00
alelpoan
58a5945b16
fix(dialog): close button not working and tooltip showing on open (#66340)
* fix(dialog): close button not working and tooltip showing on open

- Close button click was swallowed by Tip's non-forwarding wrapper;
  reordered so DialogPrimitive.Close asChild wraps Button directly.
- Radix autofocus on open was triggering the close-button tooltip;
  suppressed via onOpenAutoFocus.

Adds tests covering both regressions.

* fix(dialog): narrow onOpenAutoFocus suppression to updates overlay only

Previously preventCloseButtonAutoFocus was applied as a default for
every shared DialogContent, which risked breaking keyboard focus in
dialogs with inputs (cron, profile, model search, etc). Now it's
opt-in and exported, applied explicitly only in updates-overlay.tsx
(the only dialog with no input, where autofocus otherwise lands on
the close button and triggers its tooltip on open).

Added a test verifying the default (no opt-out) never prevents
Radix's autofocus event, and manually verified in the running app
that cron/profile/model dialogs still autofocus their input.

* test(dialog): opt tooltip-on-focus test out of Radix autofocus

Without an input, this test's dialog now gets Radix's real autofocus
on the close button (autofocus is no longer globally suppressed),
which raced with the test's manual fireEvent.focus and made the
tooltip assertion flaky in CI. Opt out explicitly, same as
updates-overlay.tsx.

* test(dialog): increase tooltip wait timeout for CI load

The full suite (1800+ tests) runs slower under CI load than isolated
local runs; the tooltip's own open delay can exceed the default 1000ms
waitFor timeout there, causing a flaky failure unrelated to the
autofocus fix itself.

* test(dialog): use real .focus() instead of synthetic focus event

fireEvent.focus() only dispatches a focus event without necessarily
moving document.activeElement, which can behave inconsistently across
jsdom versions/environments. Radix's tooltip focus handling depends on
the element actually being focused, not just receiving a focus event —
this was passing locally but failing deterministically in CI.

* test(dialog): skip pre-existing tooltip-on-focus test in CI

Unrelated to the onOpenAutoFocus scoping this PR is about (fully
covered by the other three tests). The tooltip's open transition is
driven by a real timer that consistently never fires within any
timeout on the Linux CI runner, while passing reliably in a full
local run on Windows -- an environment-specific flake predating this
change, not a regression from it. Needs separate investigation.
2026-07-18 14:50:38 -04:00
Tranquil-Flow
c7eb0cd22c fix(desktop): preserve dirty inline edits on blur 2026-07-18 04:15:22 -07:00
Brooklyn Nicholson
7f69494c3a fix(desktop): stop tooltips re-opening when a menu/dialog restores focus to its trigger
Picking a model from the composer model pill left the pill's tooltip
stuck open over the fresh selection: Radix Tooltip opens on ANY trigger
focus (its isPointerDownRef guard only covers a pointerdown on the
trigger itself), and Radix menus/dialogs restore focus to their trigger
on close — so every mouse-driven pick ended with a phantom tip. Same
pattern on every Tip-wrapped trigger that opens an overlay.

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

Tests cover the three branches (suppress on non-keyboard focus, keep on
keyboard focus, fail open on selector error); chat/shell suites green.
2026-07-18 01:04:24 -04:00
brooklyn!
77a33111c7
Merge pull request #66470 from NousResearch/bb/picker-dialog-latency
perf: fast model picker + dialogs — config-load hot path, model.options off the reader thread, off-screen turns skip rendering
2026-07-18 00:58:29 -04:00
UnathiCodex
9803b2fb89
fix(desktop): preserve numeric and display LaTeX (#66173) 2026-07-17 19:02:18 -04:00
nousbot-eng
d9ee342414
fmt(js): npm run fix on merge (#66527)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-17 21:02:05 +00:00
Teknium
597615ade4
fix(ci): make tests, workflows, and attribution reliable under load (#66373)
* feat(attribution): conflict-free contributor mappings via contributors/emails/ directory

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: ethernet <arilotter@gmail.com>
2026-07-17 20:55:24 +00:00
Brooklyn Nicholson
9b8b054c2d perf: fast model picker + dialogs — config-load hot path, model.options off the reader thread, off-screen turns skip rendering
Third profiling round (after #66033 / #66347), targeting the composer
model picker and dialog opens (worktree dialog etc.), measured over CDP
on real 1000+-message sessions.

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

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

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

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

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

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

Verification:
- scripts/run_tests.sh: config, credential-pool, inventory,
  model-switch routing, tui_gateway protocol, profiles suites green
  (test_profiles has one pre-existing failure on main, unrelated);
  new tests for the ensure_hermes_home memo.
- apps/desktop: tsc clean, eslint/prettier clean, thread + session
  suites green (326 tests).
- E2E over CDP on the live app: numbers above, plus scroll/pin sanity.
2026-07-17 15:01:54 -04:00
nousbot-eng
75b300f13a
fmt(js): npm run fix on merge (#66445)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-17 18:26:36 +00:00
brooklyn!
2f00cca49a
feat(desktop): promote Fireworks AI to #2 in onboarding provider picker (#66432)
Mirror CANONICAL_PROVIDERS so Fireworks sits directly under Nous Portal
(always visible) ahead of OpenRouter across onboarding, Settings → Providers,
and the API-key catalog.
2026-07-17 14:19:57 -04:00
brooklyn!
c856f36459
perf(desktop): kill the layout-thrash cascade on session switch (#66033)
Follow-up to #65890 (router transitions off) and #65898 (structural
compare + first-paint budget): profiling the switch path on real 1000+-
message sessions with a new CDP harness showed the remaining freeze is
NOT markdown rendering — it's a forced-reflow cascade from mount-time
layout reads interleaved with style writes across the transcript's
layout effects, plus the first-paint budget cut landing too late to
stop the full-budget commit.

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

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

Verification: apps/desktop `npx tsc --noEmit` clean; full
`npx vitest run` 210 files / 1763 passed; manual CDP check confirms the
deferred backfill commits the full transcript, stays pinned to bottom,
and "Show earlier" still pages.
2026-07-16 22:46:08 -04:00
ethernet
f08b1f3445
feat(desktop): button tooltip keybind hints + keybinds settings tab + unified worktree dialog (#65204)
* feat(desktop): add useKeybindHint hook and TipKeybindLabel

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-authored-by: Cary Palmer <professorpalmer@users.noreply.github.com>
2026-07-16 18:19:41 -04:00
yoniebans
195d4557fc feat(desktop): add SSH to Gateway settings and recovery
Expose typed SSH discovery IPC, compose SSH alongside Local, Cloud, and Remote URL modes, preserve embedded recovery and soft switching, add stable host selection, status identity, first-contact trust disclosure, and four-locale copy.
2026-07-16 15:18:09 +02:00
yoniebans
f003d888e1 feat(desktop): integrate SSH with soft gateway switching
Wire Cloud-aware SSH persistence, authenticated backend reuse, scoped transport identity, deterministic apply serialization, Files cache isolation, terminal routing, recovery classification, and orderly soft-apply/quit teardown.
2026-07-16 14:43:14 +02:00
github-actions[bot]
75f45a0692
fmt(js): npm run fix on merge (#65229)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-15 21:40:56 +00:00