Commit graph

1147 commits

Author SHA1 Message Date
nousbot-eng
09109fec98
fmt(js): npm run fix on merge (#67474)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-19 11:48:44 +00: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!
1d12d610eb
feat(desktop): let inherited projects set color and icon (#67468)
Auto-detected git repos ("inherited" projects) have no projects.db row, so
their menu hid appearance/rename/etc. entirely and they could never be
themed. Add appearance to the auto-project menu: the first color/icon choice
adopts the repo as a real project (folder = repo root, name = its label)
carrying that look, after which it themes in place like any explicit
project. Routes both explicit and auto edits through one setProjectAppearance
helper; the picker closes on adopt so a stale second write can't double-create.
2026-07-19 07:41:22 -04:00
brooklyn!
d1c455acf7
bench(desktop): systematized perf harness; sunset 12 one-off scripts (#67466)
Replaces the dozen ad-hoc measure-*/profile-* scripts (each reinventing the
CDP client — 4 different copies — plus its own arg parsing, stats, output
path, and none with a baseline) with one framework under scripts/perf/:

- lib/cdp.mjs      one CDP client + target discovery + typing + CPU-profile wrapper + DOM selectors
- lib/stats.mjs    percentiles, histograms, CPU-profile self-time ranking
- lib/baseline.mjs load/compare/update baseline + regression gate (new capability)
- lib/launch.mjs   attach, OR spawn a fully ISOLATED instance
- scenarios/*      one module per measurement, registered in scenarios/index.mjs
- run.mjs / serve.mjs, baseline.json, README.md

Isolation solves the long-standing measurement blocker: a running `hgui` held
the Electron single-instance lock, so a second instance quit. `--spawn` /
`perf:serve` launch with their own --user-data-dir (separate lock scope), their
own HERMES_HOME (separate backend/sessions, config seeded from ~/.hermes so it
reaches a chat view without onboarding), and their own --remote-debugging-port.
Synthetic scenarios drive $messages via window.__PERF_DRIVE__, so no LLM credits.

Scenario -> sunset script mapping:
  stream            <- measure-synthetic-stream, profile-synth-stream, profile-long-stream
  stream --real     <- measure-real-stream, profile-real-stream
  keystroke         <- measure-latency, profile-typing, leak-typing
  transcript        <- (new: long-transcript mount cost)
  submit            <- measure-submit, measure-jump
  session-switch    <- profile-session-switch
  profile-switch    <- measure-profile-switch
CPU profiling is now a cross-cutting --cpuprofile flag, not 5 separate scripts.

CI-tier scenarios (stream, keystroke, transcript) need no backend/credits and
are gated against baseline.json (seed values; re-capture with --update-baseline
on a reference device). Backend-tier scenarios are report-only.

perf-probe.tsx gains loadTranscript() for the transcript scenario. No core
files touched; isolation is via CLI args, not env-gated app changes.

Verified: node --check all modules, tsc, eslint, and a unit smoke of the
stats + regression-gate logic. The end-to-end GUI run (which opens a window)
is left to run interactively via `npm run perf -- --spawn`.
2026-07-19 07:41:00 -04:00
Brooklyn Nicholson
7710485c04 feat(desktop): let inherited projects set color and icon
Auto-detected git repos ("inherited" projects) have no projects.db row, so
their menu hid appearance/rename/etc. entirely and they could never be
themed. Add appearance to the auto-project menu: the first color/icon choice
adopts the repo as a real project (folder = repo root, name = its label)
carrying that look, after which it themes in place like any explicit
project. Routes both explicit and auto edits through one setProjectAppearance
helper; the picker closes on adopt so a stale second write can't double-create.
2026-07-19 07:33:26 -04:00
Brooklyn Nicholson
99a599e6f4 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:32:36 -04:00
nousbot-eng
83595f3614
fmt(js): npm run fix on merge (#67419)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-19 10:21:32 +00:00
teknium1
6bb8a0aef1 fix(desktop): drop tts.xai.text_normalization — not honored by the xAI TTS backend
Follow-up to the salvaged #56724: the runtime's _generate_xai_tts reads
voice_id, language, speed, auto_speech_tags, optimize_streaming_latency,
sample_rate, and bit_rate — but never text_normalization, and the xAI
/v1/tts payload builder has no such field. Surfacing it in the desktop
GUI would be a dead knob, so remove it from DEFAULT_CONFIG, constants.ts
(labels/descriptions/SECTIONS), and the ja/zh/zh-hant locale catalogs.
The other six xAI keys are all verified against tools/tts_tool.py.
2026-07-19 03:14:29 -07:00
Carlos Diosdado
2783d4c698 fix(gui): add xAI prefix to all xAI-specific TTS field labels
Consistent naming across the xAI TTS settings section. Speed and
sampleRate are shown only when xAI is the selected provider, so they
get the prefix too.
2026-07-19 03:14:29 -07:00
Carlos Diosdado
5c6499ce4d feat: surface all xAI TTS params in desktop GUI config
- Add speed, auto_speech_tags, text_normalization,
  optimize_streaming_latency, sample_rate, bit_rate to
  DEFAULT_CONFIG tts.xai block (backend schema source)
- Add field labels, descriptions, and section keys in
  frontend constants.ts for all 7 xAI TTS fields
- Update i18n translations (ja, zh, zh-hant)
- Fix stale tts.provider options in web_server.py schema
  overrides (was missing xai, minimax, mistral, gemini,
  kittentts, piper)
2026-07-19 03:14:29 -07:00
laurinaitis
e58534f9d7 feat(desktop): list config-defined command TTS/STT providers in settings
The Settings > Voice provider dropdowns (tts.provider / stt.provider) only offer
the built-in providers plus whatever value is currently set. Custom `type: command`
providers declared in config.yaml aren't selectable — and once you switch away from
one it drops off the list, so you can only return to it by hand-editing config.

enumOptionsFor now merges in the names of any `type: command` entries under the
tts/stt config sections, so local command-backed engines appear alongside the
built-ins and can be switched freely from the UI.

Enumeration mirrors the runtime's own resolution so the dropdown can only offer a
name the runtime would actually honour: the canonical `<section>.providers.<name>`
location plus the back-compat top-level `<section>.<name>` block, the optional
`type:` discriminator, and the built-in-name guard. The guard compares against the
runtime's built-in sets rather than the ENUM_OPTIONS display list, which is not a
substitute — it already omits `deepinfra` (TTS) and `deepinfra`/`local_command`
(STT), so a `providers.deepinfra` command block would otherwise be offered as
selectable while the runtime dispatches to the native backend instead.

- helpers.ts: add commandProviderNames() + the built-in guard; merge for
  tts.provider + stt.provider
- helpers.test.ts: cover both sections, incl. that non-command config blocks
  aren't offered and that built-ins absent from the display list are never
  offered as command providers
2026-07-19 03:14:29 -07:00
Teknium
833bae3203
Merge pull request #67206 from NousResearch/lane/c3-memory-panel
feat(desktop): declarative memory provider panel + built-in fix (salvage #51020, fixes #49513)
2026-07-19 03:00:52 -07:00
Teknium
8b6714556b
feat(desktop): terminal execution backend picker with health probes in Capabilities (#67203) 2026-07-19 03:00:08 -07:00
Teknium
c372c4220b
fix(desktop): truthful per-provider readiness in Capabilities (no more false Ready) (#67201)
* fix(desktop): compute truthful per-provider readiness for Capabilities tool config

Backend: GET /api/tools/toolsets/{name}/config now sends a per-provider
'status' field ('ready' | 'needs_keys' | 'needs_auth' | 'needs_setup')
computed by the new provider_readiness_status() in tools_config:

- env vars declared: all set -> ready, else needs_keys
- Nous-managed rows: Portal login + per-category tool-gateway entitlement
  (MANAGED_FEATURE_COVERAGE_CATEGORY) -> ready, else needs_auth
- post_setup 'xai_grok' rows: Grok OAuth or XAI_API_KEY -> ready, else
  needs_auth
- other keyless post_setup rows: installed-state predicate
  (_POST_SETUP_READY: kittentts/piper/ddgs/langfuse via find_spec,
  agent_browser via _has_agent_browser, cua_driver via PATH probe);
  unknown hooks fall back to is_active as the setup-completed signal
- genuinely-free keyless rows (Edge TTS) stay ready

Existing fields are untouched; 'status' is additive so older desktops
keep working.

* fix(desktop): render server readiness status in Capabilities provider pills

The panel's providerConfigured() heuristic pilled every zero-env-var
provider 'Ready' — including logged-out Nous Subscription rows, xAI TTS
without Grok OAuth, and never-installed KittenTTS/Piper. Render the
backend's per-provider 'status' instead:

- ready       -> existing 'Ready' pill
- needs_auth  -> warn pill 'Needs sign-in'
- needs_setup -> warn pill 'Needs setup'
- needs_keys  -> no pill (env-var fields are the signal)

Keyed rows keep deriving ready/needs_keys from local envState so saving
or clearing a key updates the pill without a refetch. Older backends
without 'status' fall back to the legacy env-var heuristic (narrow
compat path, desktop/runtime update on separate clocks).

Adds the warn tone to the settings Pill primitive and needsSignIn /
needsSetup strings to all locale catalogs (en, zh, zh-hant, ja).
2026-07-19 02:59:55 -07:00
Kshitij Kapoor
73b3a8afe3 fix(bootstrap): download timeouts + BOM upgrade for pre-fix cached scripts (#67193 follow-up)
Two residual gaps in the #67369 salvage of #67214, found during review:

1. No timeout on the download client. Since #67369, mutable branch pins
   hit the network on EVERY run, and the stale-cache fallback only fires
   when download() returns Err. A black-holed connection (captive portal,
   hung proxy, dropped packets) never errors, so the whole bootstrap hung
   at resolve() instead of falling back to the cached script. Verified
   live: a request to a non-routable address now errors at the 10s connect
   timeout instead of hanging indefinitely.

2. The UTF-8 BOM was only written inside download(). Immutable commit-pin
   caches take CachePlan::Reuse and are served untouched forever, and the
   stale-fallback path also re-serves the old file - so a BOM-less .ps1
   cached by a pre-fix installer kept reproducing the #67193 ANSI-codepage
   parse failure on every retry (production builds pin BUILD_PIN_COMMIT,
   so this is exactly the retry population). upgrade_cached_script() now
   BOM-upgrades legacy .ps1 caches in place (atomic tmp+rename,
   best-effort, idempotent) on both reuse paths; .sh untouched.
2026-07-19 14:08:48 +05:30
teknium1
8fe9706da8 fix(bootstrap): make read_decoded_line cancel-safe under tokio::select!
The salvaged helper cleared its line buffer on entry. Inside run_script's
tokio::select! loop, a stdout line arrival cancels the in-flight stderr
read (and vice versa); read_until had already consumed bytes into the
buffer, and the next call's clear() silently dropped that partial line.

Keep partially-read bytes across cancellation (clear only after a full
line is decoded) and emit an unterminated final line at EOF instead of
swallowing it. Adds a cancellation regression test (fails against the
clear-on-entry version) and an EOF-tail test.
2026-07-19 01:21:59 -07:00
HexLab98
acee4f25c7 test(bootstrap): cover CP1252 stderr decode and UTF-8 BOM cache writes
Locks the #67193 invariants: localized PowerShell error bytes survive
decode_console_bytes / read_decoded_line (including CP1252-only 0x91/0x92
punctuation), cached .ps1 files get a single UTF-8 BOM for Windows
PowerShell 5.1 -File, .sh stays BOM-less, and mutable branch caches plan
a refresh with stale-cache fallback.
2026-07-19 01:21:59 -07:00
HexLab98
4ce1994159 fix(bootstrap): decode localized PS stderr and refresh mutable install cache
Windows PowerShell 5.1 emits ParserError text in the console ANSI code page,
but the GUI bootstrap aborted BufReader::lines() on the first non-UTF-8 byte
and Retry kept reusing a poisoned install-main.ps1 for branch pins. Decode
child output with a real Windows-1252 fallback, write a UTF-8 BOM on cached
.ps1 files for -File, and refresh mutable branch/tag caches on each run
(immutable SHAs stay cached).

Fixes #67193
2026-07-19 01:21:59 -07:00
nousbot-eng
e598cef874
fmt(js): npm run fix on merge (#67311)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-19 04:31:52 +00: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
Brooklyn Nicholson
5a6e235833 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.
2026-07-19 00:26:34 -04:00
brooklyn!
fe3e5cf8aa
Merge pull request #67303 from NousResearch/bb/desktop-plugin-i18n
feat(desktop): plugin-scoped i18n — ctx.i18n locale bundles (follow-up to #60638)
2026-07-19 00:25:10 -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 Nicholson
bba18c6008 feat(desktop): expose ctx.i18n on the plugin SDK
Wire the scoped translator into the plugin context alongside storage/rest/
socket, and export usePluginI18n + the bundle types from @hermes/plugin-sdk.
The runtime shim re-exports the barrel automatically, so disk/third-party
plugins get the same door as bundled ones.
2026-07-19 00:05:08 -04:00
Brooklyn Nicholson
6282b5dcda feat(desktop): plugin-scoped i18n registry + usePluginI18n
A plugin ships its own locale bundles and registers them under its id — never
editing core en.ts — exactly like ctx.storage namespaces persistence. The
registry merges repeated registrations and drops a plugin's bundles on
dispose (tracked by the loader like register/socket), resolving through the
shared translator: active locale -> the plugin's own en -> the raw key.

Two consumers, symmetric with core: usePluginI18n(id) for React UI
(re-renders on a locale switch or a late registration) and a module-level t
for handlers/stores.
2026-07-19 00:05:08 -04:00
Brooklyn Nicholson
228655a73a refactor(desktop): share one active→default→key i18n resolver
Collapse the fallback walk out of `translateNow` into a single `translateFrom`
that takes a per-locale message source, so any translator (core catalog or a
plugin's bundles) reuses the exact dot-path walk, interpolation, and
active-locale/English/raw-key chain. Adds `getRuntimeI18nLocale`.
2026-07-19 00:05:08 -04: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
Austin Pickett
43776f109b
fix(desktop): prevent session rotation from stealing focus (#67118)
# Conflicts:
#	apps/desktop/src/app/session/hooks/use-session-state-cache.ts

Co-authored-by: UnathiCodex <theunathi@gmail.com>
2026-07-19 00:00:40 -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
brooklyn!
a5396765a2
Merge pull request #67298 from NousResearch/bb/salvage-66109-action-menu-tooltip
fix(desktop): restore tooltip-wrapped action-menu + dialog trigger clicks (supersedes #66109)
2026-07-18 23:58:35 -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
AmAzing129
84db32484f 🐛 fix(desktop): restore tooltip-wrapped trigger behavior 2026-07-18 23:52:42 -04:00
brooklyn!
667b98b5cf
Merge pull request #67296 from NousResearch/bb/composer-model-removed-fallback
fix(desktop): reseed composer when a sticky manual pick was removed from the catalog
2026-07-18 23:50:01 -04: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
d5decee5ac fix(desktop): reseed composer when a sticky manual pick was removed
A manual composer pick stays sticky across new chats (intended), but if that
model later disappears from the provider/config, every new chat kept trying the
dead model and 404'd. Fall back to the profile default in that one case only.

refreshCurrentModel now consults the model-options cache the composer already
populates (no extra fetch): a manual pick is preserved unless manualPickRemoved()
proves it's gone — provider present, non-empty catalog, model absent. An
unknown/absent provider, an empty (re-auth/unconfigured) list, or a not-yet-loaded
catalog all preserve the pick, so a still-valid selection is never clobbered.
2026-07-18 23:44:40 -04:00
nousbot-eng
73fea7b2b0
fmt(js): npm run fix on merge (#67284)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-19 03:21:18 +00:00
Brooklyn Nicholson
66a7825ebb feat: unify active-project identity in chat status (supersedes #64721)
Surface the session's first-class Project in both chat surfaces: the
Desktop status bar (project name as the workspace label, full cwd in the
tooltip) and the TUI status label + /status output.

One source of truth. The per-profile projects.db is the authority, read
in tui_gateway via _project_info_for_cwd (backed by
projects_db.project_for_path) and threaded through every session.info
emission path the TUI consumes. The Desktop already caches that truth in
$projectTree, so it DERIVES the label from it (projectNameForCwd) instead
of carrying a second per-session $currentProject atom fed from
session.info.

That drops the parallel state #64721 introduced and the entire
reset/reconciliation surface it required (resume, agentless cwd.set,
gateway-switch, fresh-draft): the label is purely derived, so it stays
correct whenever the cwd or the project tree changes. Only explicit,
named projects resolve on both surfaces, so an auto-discovered repo root
keeps the cwd-leaf label everywhere.

Excludes the unrelated markdown shell-fence change bundled in #64721.
2026-07-18 23:08:13 -04:00
nousbot-eng
19bf16c4da
fmt(js): npm run fix on merge (#67258)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-19 02:27:32 +00:00
Brooklyn Nicholson
40160e2a04 perf(desktop): batch sidebar session slices into one profile-DB pass
The sidebar refresh fired three /api/profiles/sessions calls (recents, cron,
messaging), and each one reopened every selected profile's state.db and re-ran
list_sessions_rich + session_count — ~3N DB opens/counts per refresh, on every
turn/broadcast/reconnect.

Add GET /api/profiles/sessions/sidebar: one pass that opens each profile DB
once and runs the three source-scoped queries together (recents scoped to the
active profile; cron + messaging cross-profile), returning the three windows in
one payload. Same read-only projection, 300s active heuristic, and caller-
supplied source taxonomy (recents_exclude / messaging_exclude / source=cron) as
the per-slice endpoint.

Renderer refreshSessions now makes one listSidebarSessions call and distributes
recents/cron/messaging to their stores (cron *jobs* stay a separate getCronJobs
API). Electron splices remote profiles per slice via fetchProfilesSessionSlice
(reusing the proven per-slice merge) so remote correctness is preserved; the
no-remote common case gets the single-open fast path.

From the Desktop performance audit (P1: "Batch sidebar session slices").
2026-07-18 21:45:18 -04:00
teknium1
54f5696bbb
Merge origin/main into lane/c3-memory-panel 2026-07-18 16:26:30 -07:00
brooklyn!
614dc194ea
Merge pull request #67195 from NousResearch/bb/desktop-perf-p2
perf(desktop): scope tool-diff subscriptions + narrow profile query invalidation
2026-07-18 19:21:27 -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
d8b59bd60e test(desktop): raise timeout on markdown-blocks property fuzz
The char-level streaming fuzz runs 12 seeds × 500 growing prefixes
(~6000 full+cached lexes) and first trips the pre-fix boundary at
seed 11 / step 257, so the workload can't shrink without gutting the
guard. The work is bounded but exceeds Vitest's 5s per-test default on
CI workers under parallelism, so give this one test an explicit 30s
timeout instead of weakening coverage.
2026-07-18 18:53:41 -04:00
teknium1
651cff4273
fix(desktop): treat built-in memory as built-in in provider panel (#49513)
Built-in memory (MEMORY.md/USER.md) is controlled by memory_enabled, not
memory.provider — but the desktop dropdown offered 'builtin' as a normal
provider-plugin value and gave it plugin-shaped affordances (config panel,
OAuth connect row), and the empty sentinel rendered as '(none)' even though
built-in memory was active.

- Label the empty memory.provider option 'Built-in only' (all locales).
- Drop the literal 'builtin' option from the desktop ENUM_OPTIONS and the
  backend config-schema select; _normalize_memory_provider_name already maps
  legacy builtin/built-in/none values to ''. A stored legacy literal stays
  visible via enumOptionsFor's current-value passthrough.
- Gate MemoryConnect and ProviderConfigPanel behind a new
  isExternalMemoryProvider() helper so built-in aliases never get
  provider-plugin affordances.
2026-07-18 15:39:39 -07:00
Brooklyn Nicholson
e934ee440e fix(desktop): correct incremental markdown split boundary (setext merge)
The streaming block splitter added in #67154 dropped only the previous
parse's trailing whitespace blocks plus its LAST content block before
re-lexing the appended suffix. That boundary is unsound: a trailing
Setext underline (`-`/`=`) underlines the paragraph ABOVE it, so
appending to it can retroactively merge the previous parse's last TWO
blocks into one.

Minimal repro: cached "…#e\n5\n-" lexes to [ …, "#e\n", "5\n-" ], but
grown to "…#e\n5\n-p2=kj:c" collapses "#e"/"5\n-" into a single block.
The reused settled prefix still contained a stale "#e\n" block. The
`blocks.join('') === text` guard can't detect this because the wrong
split reconstructs the same source string, so the divergence rendered
as mis-split blocks with no fallback.

Fix: drop the last TWO content blocks (skipping whitespace-only blocks
around them) before re-lexing the suffix. The block before the last is
the deepest an append can reach — a Setext underline consumes exactly
one preceding block — and earlier blocks stay fenced off by settled
blank lines, so re-lexing two is sufficient and safe.

Tests: a deterministic regression for the exact prev→grown pair, and a
character-level streaming property fuzz (12 seeds × 500 growing prefixes
over the markdown control alphabet). Both fail on the pre-fix boundary
and pass after. tsc/eslint/prettier clean; markdown-text suite green.
2026-07-18 18:21:58 -04:00
brooklyn!
1310ceb07b
Merge pull request #67154 from NousResearch/bb/incremental-markdown-lex
perf(desktop): incremental block lexing for streaming markdown — 14× less splitter CPU on long replies
2026-07-18 18:18:37 -04:00
teknium1
c84c0c5277
Merge branch 'pr-51020' into lane/c3-memory-panel 2026-07-18 15:13:04 -07:00
nousbot-eng
c34b29d11a
fmt(js): npm run fix on merge (#67152)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-18 21:49:35 +00: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
brooklyn!
73e32f37e7
Merge pull request #66747 from NousResearch/perf/desktop-hot-paths
perf(desktop): cut startup serialization and per-turn REST amplification
2026-07-18 17:42:41 -04:00