21626 Commits

Author SHA1 Message Date
hermes-seaeye[bot]
03fa32c92d fmt(js): npm run fix on merge (#83143)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-10 11:07:34 +00:00
brooklyn!
6735e6a10a Merge pull request #83138 from NousResearch/bb/titlebar-tahoe-fullscreen
fix(desktop): skip titlebar Y nudge on Tahoe and macOS fullscreen
2026-08-10 05:56:55 -05:00
hermes-seaeye[bot]
86d747e00d fmt(js): npm run fix on merge (#83139)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-10 10:55:40 +00:00
Brooklyn Nicholson
4ed6f4f7f1 fix(desktop): satisfy no-extra-boolean-cast in fullscreen guard 2026-08-10 05:50:10 -05:00
brooklyn!
3139a30e52 perf(desktop): multi-tile grids stop lagging — evict leaked session states, index lineage aliases, split the turn journal (#83133)
* fix(desktop): evict settled session states nothing on screen references

Closing a tile never removed its runtime's entry from $sessionStates, so
every tile ever closed parked its full transcript in the map for the life
of the process. Each leftover entry taxes every subsequent stream flush —
the map is spread-copied per delta and the busy/attention/draft projections
walk every entry per publish — so the app got slower the longer it ran,
which users read as "I need to clean my sessions/dbs".

Publish now evicts a settling state when no tile and not the primary view
holds its runtime (transition side effects still fire, so the settle keeps
its unread dot), and closing a tile drops an already-settled state on the
spot. Busy and needs-input states stay: background turns feed the sidebar
dots, and a first publish always lands because a resume can publish a beat
before the surface binds the runtime.

16 tiles streaming in a 2x2 grid with a day's worth of closed-tile residue:
worst-second 34 -> 58 fps, p99 frame 90 -> 28 ms, longtasks 37 -> 0.

* perf(desktop): index lineage aliases per sessions-list reference

lineageAliases scanned the whole recents list per call, and it is called
per cached session state per status projection per message delta — with a
populated sessions DB and a few busy sessions that multiplied out to
millions of row checks a second during streaming. Build the alias index
once per list reference (the list is replaced wholesale, never mutated)
and look aliases up in O(1).

* perf(desktop): journal each in-flight turn under its own storage key

The v1 journal kept every session's tail in one localStorage key, so each
throttled write re-parsed and re-stringified EVERY busy session's snapshot
— a grid of concurrent streams turned that into a whole-store JSON round
trip dozens of times a second, all on the main thread. Per-session keys
make a write O(own tail) no matter how many other sessions are streaming.
A v1 store migrates on first touch; expired/overflow crash residue is
pruned once per renderer.

* perf(desktop): stress the multitab scenario across grid/streaming/DB axes

The one-stack multitab run hid every cost this round of fixes removed: it
drove hook.publish (store only — no journal, no wiring cache), with an
empty recents list and no closed-tile residue. Streaming now routes through
hook.update (the real gateway write path), and the scenario grows axes for
the workloads users actually hit: --zones splits tiles across visible grid
zones, --streaming caps how many sessions are mid-turn (zone leaders
first), --sessions seeds a lived-in recents list, --dead models settled
sessions no surface references. launch.mjs pins HERMES_DESKTOP_CDP_PORT so
a non-default --port survives the app's own dev-CDP flag.
2026-08-10 10:45:43 +00:00
Brooklyn Nicholson
4f9d345956 fix(desktop): skip titlebar Y nudge on Tahoe and macOS fullscreen
Tahoe already aligns traffic lights without the optical translate. In
fullscreen, drop windowButtonPosition in the main process and clear the
right-cluster inset so traffic-light dodge chrome goes away on both sides.
2026-08-10 05:45:29 -05:00
hermes-seaeye[bot]
c08b086013 fmt(js): npm run fix on merge (#83132)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-10 10:44:00 +00:00
brooklyn!
338bca7968 Merge pull request #83130 from NousResearch/bb/hud-snap-pointer
feat(desktop): snap HUD to cursor with global ⌘⇧G
2026-08-10 05:33:55 -05:00
Brooklyn Nicholson
422b256939 fix(desktop): sort hud snap imports for eslint 2026-08-10 05:26:38 -05:00
Brooklyn Nicholson
440e6fb640 fix(desktop): list HUD snap chord in keyboard shortcuts panel
Document ⌘⇧G as a read-only global shortcut active while HUD mode is up.
2026-08-10 05:21:07 -05:00
Brooklyn Nicholson
9c75e4863f feat(desktop): snap HUD to cursor with global ⌘⇧G
Register CommandOrControl+Shift+G in main while HUD mode is open so the
floating bar can jump under the pointer from any app. Tap-to-snap only —
Electron globalShortcut has no keyup for hold-to-follow.
2026-08-10 05:21:05 -05:00
brooklyn!
5a3920b734 Merge pull request #83115 from NousResearch/bb/browser-pane-resize
fix(desktop): in-app browser kept squishing the chat and jamming its resize sash
2026-08-10 05:07:01 -05:00
brooklyn!
75dad8b15c fix(desktop): stop HUD window growing on drag; add corner resize handle (#83091)
* fix(desktop): stop HUD window growing on drag; add corner resize handle

The HUD window is created frame:false + transparent:true + resizable:true.
On Windows, a transparent frameless window silently grows ~1px per
setPosition call (worse at >100% DPI scaling) — every drag of the composer
bar accumulated size drift, and the HUD could end up enormous (reported at
1385x1052 against a 620x320 default). Reading the size back mid-drag
compounds the drift because getSize() returns the already-drifted value.

Fix, mirroring the pet overlay's pattern:
- create the HUD window non-resizable (no system edge resize hot-zone)
- moveBy uses setBounds with a size snapshotted on the first move of each
  drag, so the OS can never accumulate drift (verified: 500 moveBy calls
  with zero size change on Electron 40 / Win11 / 175% DPI)
- add a bottom-right corner resize handle (resize-handle.ts) driving a new
  hermes:hud:set-bounds IPC that flips resizable on for the call, restoring
  the ability to resize a window that is otherwise non-resizable

* fix(desktop): pin HUD drag size in renderer, not main-process globals

The superseding pass drops hudDragWidth/hudDragHeight from main: composer
drag snapshots outerWidth/outerHeight when the hold arms (pet overlay
pattern) and passes them on every moveBy. Adds one test for that contract.

Supersedes #82455.

Co-authored-by: Ringo6107 <199014580+Ringo6107@users.noreply.github.com>

* fix(desktop): keep the HUD solid through a corner resize; drop dead handle state

The resize handle's `resizing` flag only fed a CSS rule that restated the
cursor it already had, so nothing pinned the window mid-gesture: click-through
hands the mouse away the moment the growing edge outruns the cursor. Raise the
composer drag's existing `data-hud-grabbing` instead — one flag for "a gesture
owns the window" — and cover it in click-through's tests.

Also drops the hook's always-true `enabled` param and routes teardown through a
`reset` callback, matching composer-drag.ts and clearing the atom-mirrored-ref
lint rule.

---------

Co-authored-by: Ringo6107 <199014580+Ringo6107@users.noreply.github.com>
2026-08-10 10:05:14 +00:00
Brooklyn Nicholson
33ac4bce56 fix(desktop): satisfy eslint on pane-share-memory test 2026-08-10 04:59:26 -05:00
Brooklyn Nicholson
92ed8be923 fix(desktop): reopen docked tiles at their last split share 2026-08-10 04:52:45 -05:00
Brooklyn Nicholson
d67337ee1a fix(desktop): keep min-width floors on stacked flex zones 2026-08-10 04:52:44 -05:00
Brooklyn Nicholson
7267adafdf fix(desktop): don't let webview guests swallow drag gestures 2026-08-10 04:52:19 -05:00
hermes-seaeye[bot]
54e33c95a6 fmt(js): npm run fix on merge (#83099)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-10 09:47:13 +00:00
brooklyn!
4e46129ffe Merge pull request #83083 from NousResearch/bb/titlebar-controls-y
fix(desktop): titlebar clusters — macOS Y nudge, 24px targets, 13.9px icons
2026-08-10 04:38:36 -05:00
Brooklyn Nicholson
a843bcc940 fix(desktop): sort titlebar import for eslint 2026-08-10 04:30:58 -05:00
Brooklyn Nicholson
d1f2e32b13 fix(desktop): titlebar clusters — macOS Y nudge, 24px targets, 13.9px icons
Left cluster gets a macOS-only translate to sit on the traffic-light row.
All titlebar tools use 24×24 hit areas with 13.9px Codicons (inline size
beats unlayered codicon.css). Clusters share one flex shell with no gap —
buttons abut and the hit target is the spacing.
2026-08-10 04:24:42 -05:00
hermes-seaeye[bot]
92dd230c8c fmt(js): npm run fix on merge (#83078)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-10 09:07:07 +00:00
brooklyn!
b278dcb2d3 Merge pull request #83052 from NousResearch/bb/sidebar-all-profiles
Sidebar: show every profile at once
2026-08-10 03:57:47 -05:00
Brooklyn Nicholson
7e1f4f6f36 fix(desktop): ship the sidebar grouped by date in every scope
The all-profiles scope defaulted to grouping by profile, so "Reset to defaults"
handed back a grouping the user never picked. Both scopes now ship by date, and
a reset clears the scope you are not looking at too — otherwise flipping the
rail restored the customization the reset was supposed to undo.

Hovering a row's PR chip also holds the kebab back now: the chip is a link, and
the button that covers the end of the trailing slot was taking the click.
2026-08-10 03:50:34 -05:00
Brooklyn Nicholson
8de786c7a7 fix(desktop): give every row's trailing metadata one right-aligned slot
The PR and profile chips rendered in the row body, left of the kebab's own
column: they never sat flush right and never handed their space to the kebab
on hover, so a row showing only a PR left a hole where the age would have been.
Both now join the tokens/cost/age figures in the actions slot, and the kebab
covers the end of it — losing whichever item reads last, not the whole slot.
2026-08-10 03:35:57 -05:00
PRATHAMESH75
a1da384c6d fix(gateway): carry desktop_contract when activating a lazy session (#68392)
_live_session_payload() falls back to _fallback_session_info() while a
session's agent is still None (lazy/deferred build). That fallback omitted
desktop_contract, so session.activate returned lazy metadata with no contract
field. Desktop feeds the value straight into reportBackendContract(), where a
missing field reads as contract 0 — a current backend is then falsely flagged
"Backend out of date" on every activate of a live lazy session.

The sibling session.create shape (_lazy_resume_info) was fixed the same way in
#36112; this closes the remaining session.activate gap by advertising
DESKTOP_BACKEND_CONTRACT in the fallback payload.

Adds test_session_activate_lazy_info_reports_desktop_contract pinning the
session.activate path against a lazy (agent=None) session.
2026-08-10 01:34:24 -07:00
teknium1
ad2c7af86a feat(read): jq retrieval hint in notebook output truncation marker 2026-08-10 01:28:57 -07:00
Hermes Agent
a607b76282 Port from lobehub/lobehub#17855: render notebook outputs in read_file ipynb extraction
read_file's .ipynb extraction previously dropped cell outputs entirely,
so a notebook's training logs, tracebacks, and printed results were
invisible to the model. Ported LobeHub's token-efficient conversion:

- stream text and error tracebacks are kept (ANSI-stripped, \r
  progress-bar rewrites collapsed to the final frame)
- execute_result/display_data prefer text/plain over the HTML twin
- base64 images become sized placeholders ([image/png output — 3 KB,
  omitted]); widget state and script-bearing HTML are omitted
- legacy nbformat v3 pyout/pyerr flat-field shapes handled
- per-cell output block capped at 20k chars
2026-08-10 01:28:57 -07:00
Brooklyn Nicholson
6c371e944c feat(desktop): fade the sidebar's scrollbars out until you're in the list
A thumb parked on a list you aren't touching is chrome, not information,
and the sidebar stacks several scrollers so it draws several of them at
once. Fade them in on hover instead, sharing the existing scrollbar
colors and the webkit/Firefox split rather than styling a second kind of
bar. Only the thumb's color changes, so the reserved gutter still keeps
rows from shifting sideways.
2026-08-10 03:24:08 -05:00
teknium1
2278056256 feat(vision): disclose downscale factor and crop offset for coordinate mapping 2026-08-10 01:23:38 -07:00
teknium1
faa188bf52 feat(file-ops): clamp oversized lines in the shell pipeline before transport
ShellFileOperations.read_file previously ran sed -n '{off},{end}p' bare, so
a file with one pathological line (e.g. a 50MB+ minified bundle on a single
line) shipped the entire line across the exec transport before Python's
per-line clamp (_add_line_numbers, MAX_LINE_LENGTH=2000) could trim it.
read_file now pipes through 'cut -b1-{4*max_line_length+1}' so the shell
bounds every line to 8001 bytes before the bytes ever reach Python.

UTF-8 finding: GNU 'cut -c' is byte-based despite its name (verified:
cutting a line of 2-byte 'é' at -c8004 splits a codepoint, leaving a bare
0xC3 lead byte). The transport decodes with errors='replace', so a split
codepoint becomes U+FFFD rather than raising — but a clamp of
max_line_length+1 BYTES would deliver under max_line_length CHARS for
multibyte text, so the Python clamp would never fire and truncation would
be silent. Using 4*max_line_length+1 bytes (UTF-8 max 4 bytes/codepoint)
guarantees any line longer than max_line_length chars still decodes to
more than max_line_length chars, so len(line) > max_line_length always
triggers the existing '... [truncated]' suffix, and any boundary U+FFFD
lands past char max_line_length where the clamp removes it — verified
empirically with fixtures ('é'*4001 splits at the byte boundary yet the
result contains no U+FFFD and ends with the truncated suffix). 'cut -b'
is used explicitly to document the byte semantics.

cut (unlike sed -n p) always newline-terminates its output, which would
grow a phantom empty final line on files without a trailing newline; the
final-page path now probes the last byte (tail -c 1 | wc -l) and strips
the artifact.

read_file_raw is untouched: it is documented as no-per-line-truncation.

Benchmark (50MB single-line fixture, /usr/bin/time -v, median of 3):
  before: 191.1 MB peak RSS, 1260 ms wall
  after:   97.8 MB peak RSS,  490 ms wall
Correctness identical in both arms: monster line returns the clamped
2000-char form + '... [truncated]', offset=2 returns the trailing normal
lines intact.

Tests: 153 passed, 0 failed, 4 skipped across the file-ops suites plus a
new tests/tools/test_read_shell_line_clamp.py pinning the monster-line
clamp, offset-past-monster reads, no-trailing-newline preservation, both
UTF-8 boundary cases, and read_file_raw's exemption. Two existing mocks
asserting the exact sed command string were updated for the pipeline.
2026-08-10 01:23:34 -07:00
joaomarcos
82255fa8ef fix(telegram): reset failed primary transport pool
Retryable primary errors can leave pooled sockets in CLOSE_WAIT while fallback retries continue. Replace and close failed primary generation before fallback selection.\n\nRefs #82920
2026-08-10 13:46:48 +05:30
Brooklyn Nicholson
771b214516 feat(desktop): show every profile's sessions in the sidebar
All-profiles mode listed a flat page of chats and stopped there: the
project tree was the active profile's, grouping and filtering had no
notion of an owner, and each profile lane paged itself against a
separate endpoint. Multi-agent workflows live across profiles, so the
sidebar now treats the owner as a first-class axis.

Group by profile (the default in this scope, with its own persisted
choice so flipping the rail doesn't reset how you read one profile),
filter by profile, and start or import one from the same menu. Profile
groups take the project row's shape rather than a hand-rolled header,
preview the same three sessions a project does, and carry their whole
tokens-and-spend total in the slot the kebab hovers over.

Grouped lanes now rank by the active sort key, before they trim
themselves, so the rows a group hides are the ones the sort ranked last.

Defaults live in one const: the sidebar ships grouped by date, sorted by
recency, with the timestamp pinned — and "Reset to defaults" puts back
exactly that.
2026-08-10 03:13:08 -05:00
Teknium
7e1bfeab88 fix(desktop): read-only keyless plugin rows + backend contract v6
Rework of the salvaged #82828 compatibility layer: keep the crash guards
(optional key, safe filter/search, synthetic React row identity) but drop
the name-addressed toggle fallback — bare names collide across category
dirs (image_gen/fal vs video_gen/fal), which is exactly why the backend
moved to key-addressed toggles (a60b492e07). Keyless rows from a
pre-contract backend now render with a disabled switch and an 'update
your backend' tooltip instead of resurrecting the collision-prone
protocol.

Bump DESKTOP_BACKEND_CONTRACT / REQUIRED_BACKEND_CONTRACT to 6 so the
existing skew toast surfaces the real remedy (one-click backend update)
on session open.
2026-08-10 01:13:08 -07:00
Brooklyn Nicholson
8fdb92f449 fix(desktop): hoist the sidebar's sort key out of the flat list
The sort key was applied where the flat recents list is assembled, so it
did nothing at all once rows moved into groups: picking "cost" while
grouped by project or profile left every lane in the order the backend
sent it. Rank in a store instead, above any one view, so a grouped
surface can order the rows it owns by the same key.
2026-08-10 03:13:08 -05:00
fangliquanflq
03a9c69dc6 fix(desktop): preserve keyless plugin row identity 2026-08-10 01:13:08 -07:00
Brooklyn Nicholson
5b68d2271b feat(profiles): serve a cross-profile project tree and per-profile usage totals
`projects.tree` answers for the backend's own profile, so the grouped
sidebar had nothing to draw once the user asked to see every profile.
Run the same authoritative builder once per profile against that
profile's state.db and merge the results by folder, so one checkout is
one group no matter how many profiles work in it, and the owning profile
rides on each session row where the badge and filter can read it.

Group totals are summed in SQL rather than over the loaded page — a
number that shrank as you scrolled would be worse than no number.

Scope the batched sidebar slices while we're here: cron and messaging
came back cross-profile unconditionally, which is why a concrete profile
showed another profile's Telegram threads and cronjobs.

Closes #65710
Closes #42651
Closes #70629
2026-08-10 03:13:08 -05:00
fangliquanflq
6e19c20d0a fix(desktop): support keyless plugin rows 2026-08-10 01:13:08 -07:00
teknium1
56dc01d904 test: adapt edge-case pagination mock to the sentinel probe
Same stale-mock class as the previous commit — the sweep missed
test_file_operations_edge_cases.py. Verified no bare wc -c mocks
remain anywhere under tests/.
2026-08-10 00:23:45 -07:00
teknium1
ea68bdda92 test: adapt read mocks and fifo guard test to the sentinel probe
The combined [ -f ]/wc -c probe changes the first shell command each
read issues; update the stale mocks that only answered bare 'wc -c'.
The fifo tool-layer test now accepts the merged stat-guard's
success=False note (a fact, not an error) with the shell sentinel
behind it.
2026-08-10 00:23:45 -07:00
Drexuxux
e0b5005985 fix(file-ops): stop read_file blocking forever on non-regular files
The size probe every read path starts with — `wc -c < path` — opens the
path. On a FIFO with no writer, a socket, or a character device that never
reaches EOF, that read never returns, and read_file/read_file_raw/
read_file_bytes all pass no timeout to _exec. The turn wedges until the
process is killed.

The device blocklist in tools/file_tools.py cannot close this: it matches
literal /dev/* names, so it can only ever cover paths someone thought to
enumerate. A FIFO is a file type and can sit at any path.

Gate the probe behind `[ -f ]`, which stats instead of opening, and report
a path that exists but is not a regular file as such. A missing path keeps
its existing not-found handling.
2026-08-10 00:23:45 -07:00
teknium1
0514d67fa6 chore: map contributor email for salvaged commit 2026-08-10 00:23:41 -07:00
isheng-eqi
fc09f1c695 fix(process): reject non-positive wait timeouts; distinguish log offset=0 from default
Two falsy-zero coercions in process_registry (salvaged from PR #60004,
credit @isheng-eqi; the EOF half of that PR landed separately in
893792c99):

- wait(timeout=0): schema says minimum=1 but the handler let 0 fall
  through '0 or max_timeout' to the DEFAULT wait instead of rejecting.
- read_log(offset=0): conflated with the offset-unset default, silently
  returning the TAIL of the log when the caller asked for the head.
  Default is now offset=None; explicit 0 paginates from line one.
2026-08-10 00:23:41 -07:00
Teknium
893792c993 feat(tools): name the dead end — past-EOF and empty-file notes in read_file
A read past EOF returned content '900|' (a phantom line-number prefix
that looks like a real line) and an empty file returned '1|' — both
ambiguous silence: indistinguishable, from inside the model, from a
broken tool, so it re-reads and widens windows. Name the dead end and
its recovery instead: 'offset 900 is beyond the end of the file (412
lines total). Retry with offset <= 412.' / 'File is empty (0 bytes).'
Notes, not errors — a fact about the file is not a failure.

Boundary pinned by test: offset == total_lines still reads (an
off-by-one in a resume hint is a silently corrupted read).

Measured (file-only arm, 3 reps, control vs feature): qwen3.8-max
-18% tokens, -26% tool calls, -17% turns across the two affected
tasks; opus-4.8 flat (within rep noise); accuracy held 1.00.
2026-08-09 23:40:15 -07:00
ethernet
adecaf8086 fix(ci): unbuffer live comment poller output 2026-08-10 02:36:55 -04:00
ethernet
12299ca54c fix(ci): keep review-gated files out of the js-autofix patch
The dep-version-gate ruleset requires a team review for package
manifests, eslint configs, and workflow files. If the autofix patch
contains one of these files, the bot PR waits for that review and
auto-merge stops. The patch step now excludes them, so a bot PR
never gates itself. The eslint check in typecheck.yml still reports
their lint errors.
2026-08-10 02:36:55 -04:00
ethernet
ee0f060a7d fix(ci): start the poller on in_progress, key concurrency per repo
The requested trigger fires when GitHub creates the run. A run from a
first-time contributor waits in action_required, and the poller then
polls a run that never starts until its timeout. The in_progress
trigger fires when the run starts, and it also fires on a re-run.

The concurrency group now contains the head repository. Fork PRs
frequently share a branch name, and two PRs must not cancel the
poller of each other.
2026-08-10 02:36:55 -04:00
Teknium
fd452e26e3 feat(tools): unicode-equivalent filename retry + near-miss suggestions in read_file
NFC/NFD, narrow no-break space (U+202F), and curly quotes render
identically in a terminal — a model retyping a visually-correct path
gets 'file not found' and can never discover the byte mismatch on its
own. On not-found, canonicalize the requested name and compare against
directory entries; exactly ONE equivalent spelling reads transparently
with an explanatory note. Zero or several matches (homoglyph twins)
fall through — never guess between collisions.

Also: difflib.SequenceMatcher >=0.8 fallback in _suggest_similar_files
catches near-miss typos (AGENT.md -> AGENTS.md) that substring scoring
misses entirely.

Measured (file-only arm, 3 reps, control=guard-only vs feature):
unicode task qwen3.8-max 31k->16k tok (-48%), turns 6.7->3.7;
opus-4.8 57k->33k tok (-42%), turns 8.3->5.0; accuracy held 1.00.
near-miss: opus mildly better, qwen flat, no regressions.
2026-08-09 23:35:07 -07:00
teknium1
ddd21abfa8 chore(evals): track results/.gitignore (its own * rule excluded it from the original add) 2026-08-09 23:30:02 -07:00
Teknium
0e63ed1feb feat(tools): stat-based special-file guard for read_file + readtool eval harness
read_file on a workspace FIFO/socket blocked until the exec timeout —
the existing device guard is name-based (/dev/*, /proc/*) and cannot
see an arbitrary special file. Add _special_file_kind(): one os.stat
on the resolved path, refusing FIFO/socket/char/block devices with a
plain note ('no read was attempted') instead of hanging. Host-visible
filesystems only; regular files, dirs, and missing paths unchanged.

Also adds evals/readtool/: an A/B harness that runs the real AIAgent
against hostile-file fixtures (huge lockfile, one-line bundle, FIFO,
NFD filenames, lying extensions) and measures accuracy, turns, tool
calls, and tokens. Measured for this guard (3 reps, file-only arm):
qwen3.8-max fifo task tokens 122k -> 26k (-79%), turns 9.3 -> 5.0;
opus-4.8 tokens 40k -> 23k; accuracy held 1.00 both arms.
2026-08-09 23:30:02 -07:00