Commit graph

18487 commits

Author SHA1 Message Date
Brooklyn Nicholson
39bde52014 fix(desktop): let the expandable fade match its own surface
The overflow fade hardcoded --ui-chat-surface-background, so inside a code
block it smeared the chat background over the block's own fill. Read an
--expandable-fade-from override with the chat surface as the default.
2026-07-27 21:28:00 -05:00
Brooklyn Nicholson
b7fc36cf3c feat(desktop): strip the header chrome off markdown code blocks
A fenced block carried a bordered card with a "Code · <lang>" title row and a
pinned copy button, which read as an attached artifact next to the reply. Drop
the border and the header: the block is now a tinted slab on --ui-bg-editor
with syntax highlighting and a copy button that reveals on hover. The streaming
glow moves entirely to box-shadow since there is no border left to animate.
2026-07-27 21:28:00 -05:00
brooklyn!
cfd79e5b1b
Merge pull request #73038 from NousResearch/bb/palette-active-theme
fix(desktop): show a check on the active theme and color mode in ⌘K
2026-07-27 21:07:21 -05:00
brooklyn!
7100e8d539
Merge pull request #73041 from NousResearch/bb/tab-strip-scroll-into-view
fix(desktop): keep the active tab and the "+" in view when the tab strip scrolls
2026-07-27 21:04:26 -05:00
Jeffrey Quesnelle
1470022ad8
Merge pull request #67607 from afourniernv/feat/hermes-relay-shared-metrics
feat(observability): integrate NeMo Relay runtime and shared metrics
2026-07-27 22:04:12 -04:00
brooklyn!
b6244959a6
Merge pull request #73040 from NousResearch/bb/context-menu-parity-2
Right-click parity for cron, webhooks, and profile rows
2026-07-27 21:03:32 -05:00
brooklyn!
3d649f3376
Merge pull request #73024 from NousResearch/bb/desktop-cold-start
perf(desktop): cut renderer cold start by keeping shiki/mermaid off the boot path
2026-07-27 21:03:08 -05:00
Brooklyn Nicholson
dfe3a23a60 fix(desktop): scroll the tab strip so a new tab and the "+" stay in view
Opening a tab in a zone with more tabs than fit appended it past the right
edge of the scrolling strip — the new tab and the "+" that created it were
both off-screen, so a second new tab meant scrolling back by hand. Activating
a tab from a keybind had the same problem in the other direction.

The zone header now scrolls its active tab into view on activation, and
scrolls all the way to the end when that tab is the last one so the trailing
"+" comes with it.
2026-07-27 20:57:32 -05:00
Brooklyn Nicholson
9b84c6a046 feat(desktop): right-click actions on the sidebar cron rows
The sidebar cron rows exposed only hover trigger/manage buttons and no
right-click. Add a context menu — trigger now, pause/resume, manage, and
delete — driven against the shared $cronJobs atom so the sidebar and cron
overlay stay in sync.
2026-07-27 20:56:57 -05:00
Brooklyn Nicholson
48368bc4a7 feat(desktop): right-click parity for every panel list row
Teach PanelListRow a menuItems prop that renders BOTH the hover kebab and a
matching right-click menu from one PanelMenuItem[] (via the shared actions-menu
primitive), so a panel row's two menus can't drift. Migrate the cron, webhooks,
and profiles panels onto it — right-clicking a row now opens the same
edit/delete/enable actions as its kebab.
2026-07-27 20:56:48 -05:00
Brooklyn Nicholson
69f72f2b7c fix(desktop): show a check on the active theme and color mode in ⌘K
The theme picker computed `active` per row but nothing rendered it — the
`active?: boolean` field and its trailing check were stripped in 8f73d0d94
and the computation was left behind, so the palette offered no way to tell
which skin was already applied. That matters more than it sounds: the
desktop persists its own skin in localStorage and only seeds (never
applies) the backend's `skin:` at connect, so config and window can
legitimately disagree.

Restore the field and render a trailing check, and mark the root-search
theme rows and both color-mode lists the same way so every appearance
picker in the palette reports its own current value.
2026-07-27 20:56:03 -05:00
Brooklyn Nicholson
1baea21cfb perf(desktop): load shiki and @streamdown/code on first use, not at boot
The chunk split only helps if nothing on the entry graph statically
imports the heavy libs. Three seams did:

- react-shiki: now behind one lazy() boundary (shiki-block.tsx is its only
  static importer; LazyShiki wraps it with a PlainCode fallback).
- diff-lines.tsx: useShikiHighlighter hook extracted to a lazily-loaded
  syntax-diff.tsx (plain DiffBody fallback — same output Shiki's own
  pre-resolve state showed); codeToTokens becomes a dynamic import that
  runs only once a highlightable diff is on screen.
- @streamdown/code: statically ran createJavaScriptRegexEngine() and built
  full language registries at module scope on every launch. It now loads
  via useCodePlugin() on first markdown mount and swaps into the plugin
  table when it lands; fenced code renders plain until then, identical to
  the delay fallback users already see during streaming.

Interleaved A/B vs main (prod build, warm V8 cache, median of 4, two
alternating rounds): FCP 1100->940ms, DOMContentLoaded 618->542ms,
nav-to-interactive 1312->1123ms on the quiet round; same direction with
larger gaps under load. ~160-190ms off every renderer boot metric.
2026-07-27 20:44:35 -05:00
Brooklyn Nicholson
6fb5d2d89c perf(desktop): split heavy lazy-only libs out of the renderer entry chunk
codeSplitting:false inlined every lazy()/dynamic import into the entry, so
the whole 28.5 MB bundle — including 19 MB of shiki grammars and 3.1 MB of
mermaid that nothing rendered — was parsed and evaluated on every cold
start. The original single-chunk rationale (#38888: electron-builder OOMs
scanning shiki's thousands of default chunks) doesn't require ONE chunk,
just few: rolldown advancedChunks groups keep the file count at ~180.

Ordering matters and is the subtle part: shared foundations (react, hast/
mdast utils, lodash-es/d3 commons) must match BEFORE the heavy groups,
because rolldown merges an unmatched shared module INTO the heavy chunk
that uses it — and then the entry statically imports 19 MB of shiki just
to reach react, putting the whole chunk back on the boot path.

Statically-reachable boot graph: 26.9 MB -> 7.7 MB.
2026-07-27 20:44:35 -05:00
Alex Fournier
347252a296 Merge upstream main into feat/hermes-relay-shared-metrics 2026-07-27 18:29:17 -07:00
hermes-seaeye[bot]
71e7eb3c16
fmt(js): npm run fix on merge (#73023)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 01:20:15 +00:00
brooklyn!
fe71e32a87
Merge pull request #72893 from NousResearch/bb/desktop-activity-grouping
Collapse a turn's tool activity into one grouped, live-ticking line
2026-07-27 20:11:28 -05:00
brooklyn!
094a558635
Merge pull request #73015 from NousResearch/bb/layout-aware-keybinds
fix(desktop): resolve keybinds through the active keyboard layout
2026-07-27 20:05:51 -05:00
Brooklyn Nicholson
83cc5831fd fix(desktop): resolve punctuation keybinds through the active layout
Letter chords now follow `event.key`, but punctuation was still anchored
to the physical QWERTY position, so the shipped punctuation defaults stayed
unreachable on a remapped layout. On Dvorak `mod+.` (command center) reads
the physical V key, and `mod+,` / `mod+/` land on `w` / `[`.

Take `event.key` for unshifted punctuation too. Shift stays excluded because
a shifted `event.key` is the shifted glyph ("?" for "/") and combos are
anchored to the unshifted token. Digits stay physical: AZERTY types "&" on
the unshifted "1" key, so `event.code` is what keeps `mod+1` bound. Glyphs
we do not ship as tokens — Option output, dead keys, non-Latin scripts —
fail both checks and fall back to the physical code.

The punctuation set derives from CODE_TO_KEY so the two cannot drift.
2026-07-27 19:57:51 -05:00
LeonSGP43
b2bfab4837 fix(desktop): honor layout-aware letter keybinds 2026-07-27 19:54:55 -05:00
Brooklyn Nicholson
bc8933042f fix(desktop): stop the enter animation pinning opacity over the stylesheet
Two identical tool rows, one above the other, rendered at two different
opacities — and no amount of hovering would even them out.

The enter animation fills forwards, so its final keyframe is held in the
animation origin of the cascade for as long as the element lives, above the
author stylesheet. Naming `opacity: 1` there didn't just end the fade, it
permanently overruled the resting opacity of every element the sheet dims.
Transcript scaffolding is dimmed exactly that way, so a row kept whichever
opacity it happened to mount with: full if it animated in during the turn,
faded if it was rehydrated or remounted past its one-shot key. Same for
thinking headers, which is why "Thought" never matched the rows near it.

Leave the end opacity out of the keyframe. It animates up to whatever CSS
asks for and keeps answering to it, hover included.

Then close the way the surfaces drifted in the first place: the fade named
each one in its own selector, so the live status line — added later, and
neither tool nor thinking nor prose — matched none of them and sat a shade
brighter than the rows either side. One `data-conversation-scaffold` mark
now carries it, and every surface opts in.
2026-07-27 19:54:47 -05:00
Alex Fournier
147e451cc8 Merge upstream main into feat/hermes-relay-shared-metrics 2026-07-27 17:54:42 -07:00
Jaaneek
d83e858507 chore(xai): drop noisy default_headers comment 2026-07-27 17:47:28 -07:00
Jaaneek
5cffc53194 fix(xai): send Hermes-Agent User-Agent on chat/completions
Direct tool HTTP calls already identified as Hermes-Agent, but the main
OpenAI-SDK chat path still sent OpenAI/Python. Set Hermes-Agent/<ver> for
api.x.ai clients (xai + xai-oauth) so normal text traffic is attributed correctly.
2026-07-27 17:47:28 -07:00
hermes-seaeye[bot]
d89512107e
fmt(js): npm run fix on merge (#73004)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 00:46:46 +00:00
brooklyn!
e56480057c
Merge pull request #73003 from NousResearch/bb/tooltip-skip-delay
fix(desktop): zero Tip skipDelayDuration so the hover delay actually sticks
2026-07-27 19:38:51 -05:00
Brooklyn Nicholson
7a10e48e2f fix(desktop): one weight and one gap for transcript scaffolding
The scaffold rows shared a colour but not a weight: tool summaries and the
ticker rendered medium against the reply's normal-weight prose, which read
as emphasis on the quietest lines in the column.

Spacing had the matching problem. The block-gap rule listed which *pairs* of
blocks qualified, so the live status line — neither tool, thinking, nor
prose — fell through every branch onto its own half-size margin. And because
a streaming turn is sealed into several bubbles as it goes but rehydrates
into fewer, two blocks are siblings inside one bubble or split across two
depending on when you look; the flex gap between bubbles is half the block
gap, so the rhythm tightened and relaxed as a turn settled. Cover every
top-level block with one rule, keep prose-to-prose on paragraph rhythm, and
top the between-bubble gap up to match.
2026-07-27 19:35:04 -05:00
Brooklyn Nicholson
46df6ca705 fix(desktop): keep a run live between sequential calls
A run counted as live only while one of its calls was unresolved, which is
false for the instant between one sequential call finishing and the next
arriving — and for a string of commands that instant is most of the run. It
settled and re-opened between every call, so the ticker unmounted and came
back at the top of its reel instead of scrolling, and the summary flipped to
past tense while work was still going.

Live is now "the turn is working and nothing follows this run", with the
tail bound still settling a run the agent has moved past. The summary takes
its present-tense clause from the most recent call when none is pending —
the same call the ticker is showing. The stall spinner stays out of the way
while a run narrates, rather than stacking a second timer under it.
2026-07-27 19:35:04 -05:00
Brooklyn Nicholson
0b0e53cee1 fix(desktop): retire the drafting label when the model moves on
`tool.generating` names the tool whose arguments are streaming, and nothing
ever closed that claim: there is no stop-drafting event, and a draft can be
abandoned without reaching `tool.start` when a mid-stream retry drops a
partial call or a guardrail blocks the tool. Enumerating the ways a draft
ends left those holes open, so "Editing" sat under the transcript for the
rest of a multi-iteration turn.

Invert the rule — the claim only covers what the model is emitting right
now, so any other output from that session retires it. Stopping the turn
clears it too, and a `tool.generating` that arrives after the stop is
ignored on the same condition `mutateStream` already drops late tool rows.
2026-07-27 19:34:54 -05:00
Brooklyn Nicholson
ca605174cf fix(desktop): zero Tip skipDelayDuration so every tip waits its delay 2026-07-27 19:31:55 -05:00
Alex Fournier
ba4f5b893a Merge upstream main into feat/hermes-relay-shared-metrics 2026-07-27 17:28:56 -07:00
Teknium
373632e338 fix(state): recover from read-only database files at startup
Port from Kilo-Org/kilocode#12508: a stray read-only state.db / -wal /
-shm (sudo run, restored backup, copied dotfiles) previously killed
SessionDB init with an opaque 'sqlite3.OperationalError: attempt to
write a readonly database' raised from deep inside _init_schema —
naming no file and no fix — and the obvious wrong 'fix' (deleting the
-wal) silently loses committed transactions.

New preflight_db_writability() runs before the first connection on both
DB open paths (SessionDB.__init__ and hermes_cli.kanban_db.connect):

- files inside the Hermes home tree are repaired with chmod u+rw (the
  safe scope: Hermes owns them, and the OS makes chmod fail on files
  the user doesn't own, which bounds the repair exactly);
- anything else (root-owned files, read-only mounts, custom paths)
  fails fast with an error naming the exact file and the exact chmod
  command, plus an explicit 'do NOT delete the -wal' warning;
- WAL sidecars are never deleted or truncated — once writable, the
  normal open path checkpoints committed frames into the DB.

Proven live on main first: chmod 444 state.db -> SessionDB() raises the
opaque readonly error. With the fix: in-home DBs self-heal; out-of-home
DBs get the actionable message. Sabotage run confirms the integration
tests fail without the wiring (2 failed / 10 passed).
2026-07-27 17:27:38 -07:00
brooklyn!
f800aa3cae
Merge pull request #72995 from NousResearch/bb/edit-composer-submit
fix(desktop): send a message edit when the arrow is clicked
2026-07-27 19:27:34 -05:00
hermes-seaeye[bot]
c89d21189a
fmt(js): npm run fix on merge (#72992)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 00:21:21 +00:00
Brooklyn Nicholson
c880672846 fix(desktop): send a message edit when the arrow is clicked
Clicking the edit composer's send arrow did nothing — the edit only went
through via revert. On macOS a <button> takes no focus on mousedown, so the
arrow-click blurred the contenteditable, the blur's 80ms timer cancelled the
edit and tore down the assistant-ui composer core, and the click's send() then
threw "Composer is not available" against the dead core. submitEdit had already
set submitting=true, so the arrow wedged and only revert worked.

Guard focus on the send button with onPointerDown preventDefault, the same way
the restore button does, so the click never blurs the editor. Also wrap the
send() and the blur-timer cancel() in the #49903 unguarded-core swallow so a
raced teardown can never wedge the arrow or leak an uncaught renderer error.
2026-07-27 19:17:36 -05:00
brooklyn!
67e592b015
Merge pull request #72987 from NousResearch/bb/context-menu-parity
Mirror every desktop kebab menu to right-click
2026-07-27 19:12:04 -05:00
Brooklyn Nicholson
53a81cfe50 fix(desktop): give tool rows the scaffold colour they were meant to share
TOOL_HEADER_TITLE_CLASS was byte-identical to SCAFFOLD_LABEL_CLASS apart
from the colour — secondary (74%) against the scaffold's 64% — so a "Ran wc
-l" row sat visibly brighter than the "Thought for 1s" line above it. Same
for the trailing duration: tertiary against scaffold meta.

The primitive existed but only the thinking header and run summary were
routed through it. Point the tool row at it too and delete the duplicates,
so there is one place left to change. Search hit titles keep the brighter
grey under their own name: they are result content, not scaffolding.
2026-07-27 19:11:48 -05:00
Alex Fournier
e23ef9f312 Merge upstream main into feat/hermes-relay-shared-metrics 2026-07-27 17:09:18 -07:00
brooklyn!
66a94dc177
Merge pull request #72985 from NousResearch/bb/tooltip-delay
fix(desktop): delay Tip hover-open by 200ms
2026-07-27 19:08:52 -05:00
Brooklyn Nicholson
a67b2d93cd fix(desktop): paint the live status line as scaffolding
The drafting/stall status row kept its own type and colour — text-sm at
muted-foreground/70, with the hint at /55 — so "Editing" while the model
drafts a call rendered a full step larger than the "Explored 3 files" line
it turns into a moment later, in a different grey.

Route it through the scaffold label token so the whole left column reads as
one kind of line. The timer keeps its midground tint: that belongs to the
live-signal cluster with the dither block, not to the scaffolding.
2026-07-27 19:07:24 -05:00
Brooklyn Nicholson
84a25212de feat(desktop): mirror every kebab menu to right-click
Wire the shared actions-menu into the remaining kebabs so right-clicking a
row opens the same actions as its ⋯ button: project rows (appearance moves
to a submenu via the new shared ProjectAppearancePicker), worktree lanes,
the composer branch bar, and settings credential rows.
2026-07-27 19:04:45 -05:00
Brooklyn Nicholson
0d14864b94 refactor(desktop): extract a shared kebab + right-click actions-menu primitive
Promote the session row's inline MenuKit device into components/ui/
actions-menu.tsx: one ActionsMenu (kebab) + ActionsContextMenu (right-click)
pair driven by a single items(kit) render function, so a row's dropdown and
its context menu can't drift. Refactor the session menu onto it and let
StatusRow forward ref/onContextMenu so any row can host a context menu.
2026-07-27 19:04:39 -05:00
Brooklyn Nicholson
705b20a57d fix(desktop): delay Tip hover-open by 200ms so tips stop flashing 2026-07-27 19:02:23 -05:00
teknium1
cf0c42fa0b fix(agent): never replay chain-of-thought in the active-turn redirect checkpoint
A /steer redirect during a thinking phase serialized the streamed
reasoning into the persisted assistant checkpoint ('Reasoning shown
before the interruption: ...'). An assistant turn exposing its own
chain-of-thought reads to Anthropic's output classifier as
reasoning-injection/prefill jailbreak, so every subsequent call on the
session deterministically returned 'Provider returned an empty
response' — and because the checkpoint is persisted and replayed, no
retry, nudge, or empty-recovery branch could ever escape it. Four
sessions were permanently bricked this way in the week of Jul 21-27
(42+ blocked calls; every reasoning-free checkpoint that week was
untouched — same mechanism as the prefill.json incident).

Class fix: streamed reasoning is now display-only state. The
_current_streamed_reasoning_text accumulator is removed entirely
(producer in _fire_reasoning_delta, resets, and init), so no future
path can serialize CoT into replayable content. The checkpoint keeps
only the visible response text; the model regenerates its reasoning on
the retried turn. Invariant documented in _apply_active_turn_redirect.

Regression tests: CoT never appears in either checkpoint shape,
reasoning-only interrupts produce a bare checkpoint, reasoning deltas
stay display-only.
2026-07-27 16:58:55 -07:00
Brooklyn Nicholson
b1f4b95761 fix(desktop): measure reasoning per block instead of per turn
The timer registry hands every caller of a key the same origin, and every
reasoning block in a turn was keyed `reasoning:<messageId>`. So the second and
third blocks measured from the first one's start and each reported the running
total as its own duration — the "6s, 6s, 16s" down a single turn.

Key per block, and move the measurement into `useMeasuredDuration`, which
keeps the number beside the origin that produced it. The thread virtualizes,
so the component that watched a block finish is usually gone by the time
anyone scrolls back to read it; component state forgot the duration on
unmount and the row fell back to having none.

A block that genuinely was never watched running — history from an earlier app
session, or reasoning that arrived already complete — still has no duration to
report, and now says "Thought" rather than sitting in the present tense at a
turn that ended.

Also drops the run summary's aggregate +N/−M: a run can no longer contain a
file edit, so it was always zero. Each edit carries its own count on its card.
2026-07-27 18:50:20 -05:00
Brooklyn Nicholson
f08b0e5606 Merge remote-tracking branch 'origin/main' into bb/desktop-activity-grouping 2026-07-27 18:42:49 -05:00
Brooklyn Nicholson
a3c46ac09e refactor(desktop): give live tool runs a one-line ticker
A live run was the settled view under a CSS max-height: the real rows,
capped at ~6.75rem, with an escape hatch that let an open diff lift the cap
entirely. So it was neither a single line nor an honest expanded block, and a
run could balloon mid-turn.

Split the two presentations instead. Live, a run is its summary plus a
one-line reel that slides each finished action up and out, so a turn touching
thirty files reads as one line ticking over in place. Settled, the summary is
the whole of it until opened. Cards — file edits, clarify, image_generate —
leave the run entirely and stay on screen where they happened, since the diff
or the question IS the point of the turn.

Drops useToolWindow, the bounded-window threshold, the scroll container, the
fade mask and the :has([data-tool-open]) escape hatch, all of which existed to
make "the real rows, but shorter" work.

Also unifies transcript scaffolding on one colour: thinking headers painted
--ui-text-secondary and tool summaries --ui-text-tertiary under a shared
opacity, which is two greys for one kind of line. Both now render through
ScaffoldRow at a token pitched between them.
2026-07-27 18:42:46 -05:00
Alex Fournier
d7b8a63eb7 Merge upstream main into feat/hermes-relay-shared-metrics 2026-07-27 16:24:46 -07:00
hermes-seaeye[bot]
96db67b849
fmt(js): npm run fix on merge (#72967)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-27 23:12:24 +00:00
brooklyn!
a19cfa4531
Merge pull request #72960 from NousResearch/bb/desktop-statusbar-toggle
feat(desktop): let the status bar be hidden
2026-07-27 18:10:28 -05:00
Brooklyn Nicholson
6ba2f3e644 fix(desktop): say a sub-second reasoning block was brief
The elapsed timer counts whole seconds, so reasoning that finished
inside one rendered as "Thought for 0s" — accurate and useless, and on a
turn with several short blocks it repeats down the transcript. Drop the
number below a second and say it was brief instead.
2026-07-27 18:10:27 -05:00