Commit graph

16402 commits

Author SHA1 Message Date
Teknium
9fc35e0a31 chore: add contributor email mapping for Dhravya 2026-07-20 00:40:40 -07:00
RenoMG
95c616be20 fix(supermemory): complete self-hosted endpoint routing 2026-07-20 00:40:40 -07:00
Dhravya Shah
24ac26a3da feat(supermemory): support custom base URL for self-hosted servers
The supermemory SDK already honors SUPERMEMORY_BASE_URL, but the raw
urllib call used for session-end conversation ingest hardcoded
https://api.supermemory.ai/v4/conversations, so ingest always hit the
cloud even when pointing at a self-hosted server (e.g.
http://localhost:6767).

Resolve the base URL as config (supermemory.json base_url) >
SUPERMEMORY_BASE_URL env var > https://api.supermemory.ai, strip any
trailing slash, and use it for both the SDK client and the
/v4/conversations ingest endpoint.
2026-07-20 00:40:40 -07:00
kshitijk4poor
244dabbd9c test(cli): mock _cleanup_oneshot_runtime in all _run_and_exit tests
Phase 2c found 3 tests that called _run_and_exit_oneshot without
mocking _cleanup_oneshot_runtime, causing real cleanup (terminal,
browser, MCP, auxiliary) to run in the pytest worker. Add the mock
to all three for test isolation.

Also remove redundant 'import logging' inside _exit_after_oneshot
(already imported at module level, line 729).
2026-07-20 12:59:28 +05:30
kshitijk4poor
97fc8a4a3c refactor(cli): apply /simplify-code findings to oneshot teardown
- Add idempotency guard (_oneshot_cleanup_done) to _cleanup_oneshot_runtime,
  matching cli.py:_run_cleanup's pattern
- Trim _exit_after_oneshot docstring from 22 to 8 lines (per-resource
  ownership enumeration already documented in _run_agent)
- Add comment clarifying cleanup ordering mirrors gateway/run.py, not
  cli.py (oneshot has no _active_agent_ref)
- Clarify session_db.close() comment: agent.close() calls end_session()
  but leaves the connection open

Findings skipped (follow-up scope):
- Extract shared 5-step cleanup helper from cli.py:_run_cleanup (widens
  scope into critical file)
- Extract _hard_exit helper (3 copies across cli.py)
- Extract shutdown_agent_resources helper (touches run_agent.py + cli.py
  + gateway/run.py)
- Test boilerplate dedup (test-only, non-blocking)
2026-07-20 12:59:28 +05:30
kshitijk4poor
2de60a3a7e fix(cli): expand oneshot cleanup to cover all process-global resources
The initial salvage from #43698 only shut down MCP servers and cached
auxiliary clients. The interactive CLI's _run_cleanup() also closes
terminal environments, browser sessions, and interrupts async
delegations — all of which can hold native-extension-backed resources
(aiohttp connectors, websocket clients) that SIGABRT during
Py_FinalizeEx.

Add the missing three sites to _cleanup_oneshot_runtime(), matching the
order in cli.py:_run_cleanup(). Update tests to cover the expanded
cleanup chain.

Credit: @konsisumer (#67768) identified the full cleanup surface.
2026-07-20 12:59:28 +05:30
harjoth
7462546a33 docs(cli): clarify oneshot hard-exit cleanup scope 2026-07-20 12:59:28 +05:30
harjoth
54eea80bf7 test(cli): cover termux oneshot usage file 2026-07-20 12:59:28 +05:30
harjoth
fbfe89871b fix(cli): guarantee hard exit after cleanup interruption 2026-07-20 12:59:28 +05:30
harjoth
b82ffdaa4d test(cli): preserve oneshot usage file through hard exit 2026-07-20 12:59:28 +05:30
harjoth
bfa7a794cb fix(cli): avoid one-shot SIGABRT during teardown 2026-07-20 12:59:28 +05:30
kshitijk4poor
113d9f63b5 fix: Windows guard, dedup recovery, profile-safe paths, clear SO_RCVTIMEO
Follow-up fixes for salvaged PR #67686 (issue #67639):

1. Windows regression: bare 'import fcntl' at module level in entry.py and
   slash_worker.py would crash on Windows (fcntl is POSIX-only). entry.py
   explicitly aims to 'import cleanly on Windows'. Extracted all fcntl/socket
   logic to tui_gateway/_stdin_recovery.py with try/except import guards.

2. fd leak: socket.fromfd() dups fd 0; s.detach() returned the fd number
   without closing it, leaking one fd per recovery call. Changed to s.close()
   (safe — fromfd duped the fd, closing won't close stdin).

3. Code duplication: ~80 lines of recovery loop + diagnostic were copy-pasted
   between entry.py and slash_worker.py. Extracted to shared
   tui_gateway/_stdin_recovery.py (handle_spurious_eof + diagnose_stdin_state).

4. Profile-unsafe path: scanner used Path.home() / '.hermes' / 'plugins' but
   canonical plugin discovery uses get_hermes_home() / 'plugins'. With
   HERMES_HOME pointing elsewhere, scanner missed the actual plugin dir.
   Also gated project plugins behind HERMES_ENABLE_PROJECT_PLUGINS to match
   hermes_cli/plugins.py.

5. SO_RCVTIMEO not cleared: recovery only called os.set_blocking(0, True)
   but a child that set SO_RCVTIMEO would cause the next readline to time
   out and loop. Now clears SO_RCVTIMEO alongside O_NONBLOCK.
2026-07-20 12:48:50 +05:30
x7peeps
01c02c6f8f fix(tui): recover from spurious stdin EOF caused by child O_NONBLOCK flip
Fix #67639

根因分析:
子进程继承 fd 0 (stdin) 后设置 O_NONBLOCK 标志时,该标志作用于共享的
open file description 而非单个文件描述符。这导致 gateway 的下一次 read()
返回 EAGAIN,CPython 的缓冲层将其转换为 b'',表现为 EOF,gateway 因此
意外退出。这不是真正的 TUI 关闭管道,而是子进程修改了共享文件状态。

修复涉及三个 Gap:

Gap 1 — check_subprocess_stdin.py 扫描范围不足:
- 原正则仅匹配 subprocess.run/Popen,扩展到 call/check_output/check_call/
  os.system/asyncio.create_subprocess_exec/shell
- 新增扫描 ~/.hermes/plugins/ 和 ./.hermes/plugins/ 用户插件目录
  (hermes_cli/plugins.py:10-12 定义的插件加载路径)

Gap 2 — Gateway 无自愈能力:
- entry.py 和 slash_worker.py 的 stdin 循环改为 while True + readline()
  模式
- 当 readline() 返回空字符串时,检查 O_NONBLOCK 标志判断是否为虚假 EOF
- 虚假 EOF → 恢复 blocking 模式并继续; 真实 EOF → 正常退出
- 添加恢复频率限制 (10次/分钟),防止无限循环
- 添加 _diagnose_stdin_state() 诊断函数,记录 O_NONBLOCK/SO_RCVTIMEO 状态

Gap 3 — 日志消息误导:
- 原 "stdin EOF (TUI closed the command pipe)" 改为 "stdin EOF (peer closed)"
  或 "stdin spurious EOF (subprocess O_NONBLOCK flip)",附带诊断信息

附带修复: compute_host.py 的 subprocess.check_output 缺少 stdin= 参数
2026-07-20 12:48:50 +05:30
Teknium
39b30bacf7 docs(x_search): comment out reasoning_effort in sample config blocks
The copy-paste config sample had reasoning_effort: low active, which
would silently downshift effort for anyone pasting the block. Keep it
commented like other optional keys. Also add the contributor email
mapping for the salvage.
2026-07-19 23:58:33 -07:00
YAMAGUCHI Seiji
48adc1f602 test: cover X Search reasoning config propagation 2026-07-19 23:58:33 -07:00
YAMAGUCHI Seiji
5befa15aba feat: configure X Search reasoning effort 2026-07-19 23:58:33 -07:00
kshitijk4poor
0144743b21 chore: add contributor email mapping for kshitijk4poor 2026-07-20 12:13:37 +05:30
kshitij
2ae195673e fix: widen metadata-preserve guard to list-of-dicts models form
The dict-form guard from PR #67878 only covered the mapping shape
({model: {context_length: ...}}). The list-of-dicts shape
([{id: model, context_length: ...}]) is also a supported config form
(per _declared_model_ids) and was still being replaced with a flat
list of strings, destroying per-model metadata.

Sibling site for #67841.
2026-07-20 12:13:37 +05:30
kyssta-exe 25470058+kyssta-exe@users.noreply.github.com
311bacb572 fix(model-switch): preserve per-model metadata dict in _save_discovered_models_to_config (#67841)
When custom_providers[].models uses the mapping form to store
per-model metadata (e.g. context_length), _save_discovered_models_to_config
must not replace it with a flat list of strings.  Add a guard that skips
entries whose models value is a dict, preserving the user's curated
metadata.

The regression was introduced by PR #65652, which added the auto-save
helper without considering the dict form.
2026-07-20 12:13:37 +05:30
Floze
cd0219da86 fix(gateway): stop slow restart redelivery loops 2026-07-20 12:11:07 +05:30
Teknium
693f935936
feat(picker): fold Qwen providers into one group row in provider pickers (#67758)
Consolidates the three Qwen provider slugs (alibaba / Qwen Cloud,
alibaba-coding-plan / Alibaba Cloud Coding Plan, qwen-oauth / Qwen CLI
OAuth) under a single 'Qwen' group row in the interactive provider
pickers, matching the existing OpenAI / Kimi / MiniMax / xAI groups.

Display-only via PROVIDER_GROUPS — slug identity, --provider, and
/model <provider:model> paths are unchanged. Because group_providers()
is the shared fold, the CLI 'hermes model' picker, the setup wizard,
and the Telegram /model keyboard all pick up the grouping with no
per-surface changes.
2026-07-19 23:34:03 -07:00
Teknium
0d7fad7b88
fix(config): shipped template no longer enables session auto-reset (#67772)
#60194 flipped SessionResetPolicy's default to mode: none, but
cli-config.yaml.example still shipped session_reset.mode: both. Every
install path (install.sh, install.ps1, docker stage2-hook, hermes
doctor) copies the template verbatim to ~/.hermes/config.yaml, so fresh
installs got an EXPLICIT mode: both that overrides the code default —
users hit 24h-idle resets with 'nothing' in their config enabling it.

- cli-config.yaml.example: session_reset.mode both -> none, comments
  rewritten to describe auto-reset as opt-in
- docs/session-lifecycle.md: appendix example updated to match
- tests/gateway/test_config.py: invariant tests — template seed, absent
  config, and mode-less session_reset block all resolve to mode none;
  explicit opt-in still honored
2026-07-19 23:33:53 -07:00
Drexuxux
1157c636c5 fix(compression): stop the progress floor from splitting a tool group
_find_tail_cut_by_tokens aligns cut_idx away from tool-call/result
boundaries (_align_boundary_backward), and both tail anchors re-align after
moving it. The final statement then raised the result to head_end + 1 so
compression always claims at least one message — without that floor the
caller's compress_start >= compress_end guard turns the pass into a no-op
that re-runs forever.

That raise discarded the alignment. When the floor landed inside a tool
group, the parent assistant(tool_calls) fell in the summarised region while
its tool results started the tail, and _sanitize_tool_pairs dropped those
orphans outright — so the tool output was neither summarised nor kept. It
vanished. That is exactly the silent loss _align_boundary_backward's own
docstring says the alignment exists to prevent.

Two back-to-back tool calls are enough to trigger it on default settings
(protect_first_n=3):

    system, assistant(call_1), tool, tool, assistant(call_2), tool

    aligned cut          = 4   (keeps call_2's group together)
    returned cut         = 5   (floor overrode it)
    summarised region    = [assistant(call_2)]
    tail                 = [tool(call_2)]  -> orphan -> dropped

Sweeping every well-formed block layout up to length 6 (21840 transcripts),
5623 of them — 26% — split a call/result pair this way.

Re-align FORWARD after applying the floor. Forward, never backward: pulling
back would hand return the message the floor just claimed and reopen the
no-op loop. Sliding forward instead moves the cut past the end of the group,
so the whole call/result pair is summarised together and nothing is
orphaned. The same sweep reports 0 violations after the change, and the
progress guarantee is pinned by its own test.
2026-07-20 11:58:50 +05:30
kshitijk4poor
3c72177061 fix(config): widen doctor allowlist to all gateway-bridged top-level keys
Salvage of PR #67447 — the original PR fixed 3 of 7 missing keys.
gateway/config.py reads 4 more top-level keys (stt_echo_transcripts,
reset_triggers, always_log_local, filter_silence_narration) that
produced the same false 'Unknown top-level config key' warning.
Add all 4 and extend the regression test to cover them.
2026-07-20 11:53:10 +05:30
HexLab98
54157da9ee test(config): cover doctor allowlist for Hermes-written root keys
Regression for known_plugin_toolsets / group_sessions_per_user /
thread_sessions_per_user so validate_config_structure no longer
false-positives on keys Hermes owns.
2026-07-20 11:53:10 +05:30
HexLab98
7c2ece53c0 fix(config): whitelist Hermes-owned roots doctor falsely flagged
Hermes writes known_plugin_toolsets via tools_config and bridges
group_sessions_per_user / thread_sessions_per_user in gateway/config,
but doctor treated them as unknown top-level keys. Add them to
_EXTRA_KNOWN_ROOT_KEYS so validation matches keys Hermes itself uses.
2026-07-20 11:53:10 +05:30
Drexuxux
371ee065fd fix(gateway): don't spend a redelivery attempt when the platform is down
The delivery ledger durably records a final response before the send so a
crash between finalize and platform ACK can redeliver it on the next boot.
attempts is that redelivery budget, capped at MAX_ATTEMPTS=3.

sweep_recoverable() claims every dead-owner row and increments attempts
before the caller knows whether it can send. self.adapters only holds a
platform after its connect() succeeded, so when the platform failed to
connect this boot _redeliver_pending_obligations() hits its "adapter is
None" branch and continues WITHOUT sending — but the attempt is already
spent. Three such boots and the row abandons, having never been sent once.

That is the loss the ledger exists to prevent, and the trigger correlates
with the crash that created the obligation: the network trouble that killed
the send tends to still be there on the next boot. Worse, the message stays
lost — once abandoned it is never retried even after the platform recovers.

Reproduced against the real runner with an unconnected adapter:

    boot 1: claimed=1 state='attempting' attempts=1  (0 sends attempted)
    boot 2: claimed=1 state='attempting' attempts=2  (0 sends attempted)
    boot 3: claimed=1 state='attempting' attempts=3  (0 sends attempted)
    boot 4: claimed=0 state='abandoned'  attempts=3  (0 sends attempted)

Let the caller declare which platforms it can send on, and skip claiming
rows for the others. attempts then only ever buys a real send. Rows for a
platform that never returns are still bounded by the stale cutoff, so
nothing accumulates. The parameter is keyword-only and optional — omitting
it keeps the previous claim-everything behaviour for other callers.
2026-07-20 11:52:41 +05:30
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!
919eb30ca7
Merge pull request #67842 from NousResearch/perf/desktop-tool-view-lazy-json
perf(desktop): stop eagerly JSON.stringify-ing every tool's args + result
2026-07-19 23:03:47 -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!
3e23c502f2
Merge pull request #67838 from NousResearch/perf/desktop-resize-raf
perf(desktop): rAF-coalesce pane + console sash resizes
2026-07-19 22:44:14 -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!
7f56f89706
Merge pull request #67824 from NousResearch/perf/desktop-tree-revalidate
perf(desktop): targeted file-tree revalidation (only the changed subtree)
2026-07-19 22:26:01 -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!
7f12d4f890
Merge pull request #67818 from NousResearch/perf/desktop-review-diff-virtualize
perf(desktop): virtualize the review-pane diff (no more full-Shiki freeze)
2026-07-19 21:42:09 -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!
b61c033c0b
Merge pull request #67788 from NousResearch/perf/backend-ttft-request-estimate
perf(agent): drop per-call base64 re-serialization from request-size estimate
2026-07-19 21:09:51 -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
b0f60622aa style(agent): tighten request-estimate comment 2026-07-19 19:54:31 -05:00
Brooklyn Nicholson
c1c4e56e7e perf(agent): drop per-call base64 re-serialization from request-size estimate
Every API iteration computed `total_chars = sum(len(str(msg)) ...)`, which
str()-serializes the ENTIRE history — including base64 images and large tool
results — just to take its length, then called estimate_request_tokens_rough,
which walked the messages a SECOND time (it re-runs estimate_messages_tokens_rough
internally, already computed one line above).

Now derive both from one image-stripped message estimate:
  approx_tokens = estimate_messages_tokens_rough(api_messages)   # once
  request_pressure_tokens = approx_tokens + tools_tokens          # == old value
  total_chars = approx_tokens * 4                                 # log/metric only

request_pressure_tokens is byte-identical to the old
estimate_request_tokens_rough(api_messages, tools=agent.tools or None) (no
system_prompt arg → messages + tools). total_chars only feeds a verbose log and
the pre-api-request hook's request_char_count, so a rough proxy is fine and it no
longer balloons on image turns. On the TTFT critical path for every call.

tests/agent/test_model_metadata.py + test_compressor_image_tokens.py green.
2026-07-19 19:48:12 -05:00