Commit graph

2110 commits

Author SHA1 Message Date
Brooklyn Nicholson
8ccb4c2cee perf(desktop): scope background-throttling opt-out to live streaming
The process-wide disable-background-timer-throttling /
disable-backgrounding-occluded-windows switches plus a static
backgroundThrottling: false on every chat window pinned each renderer's
document.visibilityState to 'visible' for the life of the window. Every
visibility-gated backstop poll and clock tick in the renderer became an
always-on timer: an idle, minimized Hermes burned ~20% CPU around the
clock, on battery too.

Throttling is now a runtime dial. A small controller (stream-throttle.ts)
rides the merged hermes:active-work reports the quit guard already
receives: while any turn is in flight every chat window gets
setBackgroundThrottling(false) — a live answer keeps painting while
blurred, occluded, or minimized, exactly as before — and once all turns
settle (plus a 5s trailing window so the final flush lands at full
cadence) Chromium's default throttling returns and hidden windows go
quiet.

disable-renderer-backgrounding stays: process priority only, no timer
semantics, and it keeps hidden streaming fast.
2026-07-31 00:38:11 -05:00
brooklyn!
cdca247424
Merge pull request #75180 from NousResearch/bb/composer-cut-placeholder
The placeholder comes back when you clear the composer
2026-07-30 23:54:19 -05:00
Brooklyn Nicholson
0b4bd3c7c7 fix(desktop): the placeholder comes back when you clear the composer
Select-all + Cut emptied the text and left the composer blank — no draft,
no prompt. Delete had the same hole.

The placeholder is painted on `:empty`, and a cleared editor keeps a
scaffolding <br> so the contenteditable can't collapse to a sliver. Those
two facts collide: the moment the break lands the editor has a child,
`:empty` goes false, and the prompt never comes back.

CSS can't infer emptiness on its own either. A text node is invisible to
selectors, so `one<br>` and a lone `<br>` are the same shape — a structural
rule like `:has(> br:only-child)` paints the placeholder straight over the
user's text. The code that empties the editor is what knows, so it marks
the root and the condition reads `:is(:empty, [data-empty])`.

Both writers that reshape that root maintain the marker through one helper:
the normalizer, and renderComposerContents for a restored draft or an undo.
The message-edit composer shares the slot and the rule, so it takes the
same shared class instead of drifting on its own copy.

#74815 fixed the draft this stashed; the placeholder is a separate seam.
2026-07-30 22:54:20 -05:00
hermes-seaeye[bot]
b1858f33a1
fmt(js): npm run fix on merge (#75159)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-31 03:08:09 +00:00
brooklyn!
ab158e8088
Merge pull request #75127 from NousResearch/bb/close-last-tab
desktop: closing the last main tab lands on New session, and middle-click works on a real mouse
2026-07-30 21:58:43 -05:00
Brooklyn Nicholson
193e5f84f7 fix(desktop): the main tab can be closed by gesture and menu
The tab strip decided the close gesture from the `uncloseable` flag, which the
workspace sets to keep its pane in the tree — so the one tab whose close now
does something couldn't be ⌘-clicked or middle-clicked, and its right-click
menu had no Close.

Read the gesture off the pane's registered closer instead, with the workspace
registering closeWorkspaceTab. An atom rather than a lookup, since that closer
comes from a wiring effect that lands after the strip's first paint.
2026-07-30 21:43:15 -05:00
Brooklyn Nicholson
c7b021ca48 fix(desktop): closing the last main tab lands on New session
The workspace pane can't leave the tree, so "close the main tab" only ever had
one answer wired: shift the next stacked session in. With main as the only tab
there was nothing to shift and ⌘W dead-ended on the tab the user was looking
at.

closeWorkspaceTab is now the one answer for every entry point — stacked
session still wins, and with nothing stacked main drops to a fresh New session
draft. A blank draft and a full-page view stay no-ops: a blank draft already
IS the post-close state.
2026-07-30 21:43:15 -05:00
Brooklyn Nicholson
463fbf5b16 fix(desktop): middle-click works on a real three-button mouse
Chromium on Windows and Linux answers a middle press inside a scroller by
starting the autoscroll pan, and the mouseup that ends the pan never becomes
an auxclick. Every surface carrying the gesture — tab strips, the session
list, the terminal rail — is a scroller, so middle-click only ever worked on
macOS, where autoscroll doesn't exist.

Arm on pointerdown, spend on the pointerup over the same element (press one
tab, release on another and nothing happens), and cancel the middle mousedown
on every press so the pan widget can't appear on a surface that owns the
button. One helper, four call sites.
2026-07-30 21:43:14 -05:00
Brooklyn Nicholson
4d6589c69c fix(desktop): stop ⌥-click spraying cursor escapes into the terminal
⌥-drag is the app's force-selection gesture over mouse-mode TUIs, but
xterm's default alt-click-moves-cursor claims the same click and emits one
cursor left/right escape per column of travel. Shells that don't consume
them echo the raw `^[[D` burst into the buffer. One gesture, one meaning.
2026-07-30 20:51:40 -05:00
Brooklyn Nicholson
0cec9896a1 fix(desktop): open links clicked in the integrated terminal
Both of xterm's link paths activate through `window.open()`, which the
window's setWindowOpenHandler denies, so ⌘-clicking a URL did nothing but
log "Opening link blocked as opener could not be cleared" — and the OSC 8
path fronted that dead end with a raw confirm() dialog. Route both through
the desktop bridge, the path every other external link in the app takes.

⌘-click on macOS, Ctrl-click elsewhere, matching VS Code's integrated
terminal, Terminal.app, and iTerm2. A bare click stays with the selection so
a misclick on a URL can't launch a browser.
2026-07-30 20:51:09 -05:00
hermes-seaeye[bot]
5d6aae02bf
fmt(js): npm run fix on merge (#75055)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-30 22:06:42 +00:00
brooklyn!
3a2b332985
Merge pull request #74938 from NousResearch/bb/rail-own-worktree
fix(desktop): a session's coding rail follows its own worktree
2026-07-30 16:54:37 -05:00
Andrew Fiebert
c9de69c6d5 fix(desktop): keep queued prompts bound to their origin session
ChatView migrated tip-keyed composer queue/draft entries onto queueSessionKey
whenever the two ids differed. queueSessionKey is route-driven and can flip to
Session B a frame before the store selection leaves Session A, so migrate
re-homed A queue entries onto B and the idle ChatBar auto-drained them into
the wrong chat.

Gate migrate on same-conversation lineage only (tip to root). Also honor
lineage when background queue drain decides selected/busy, so a root queue key
is not treated as idle/offscreen while the compression tip is still working.
2026-07-30 15:08:31 -04:00
Brooklyn Nicholson
8f4ab7ad2c fix(desktop): coding rail reads only its own worktree
The row fell back to the global $repoStatus whenever repoPath was blank,
painting the main pane's branch and ± onto a tile whose cwd hadn't
resolved yet. The fallback bought nothing — the primary computed is keyed
to $currentCwd, which is empty in exactly that case — and cost a rail
showing a tree the session was never in.
2026-07-30 12:27:23 -05:00
Brooklyn Nicholson
c48d9a9c6d fix(desktop): mark tile and branch runtimes as background
A tile and a branched session each live in their own worktree and render
from their own SessionView slice, so neither is the main pane's session.
Pass foreground: false at both call sites.
2026-07-30 12:27:23 -05:00
Brooklyn Nicholson
dd762d07bb fix(desktop): only the foreground session may write the composer atoms
applyRuntimeInfo unconditionally mirrored a runtime's cwd, branch, model
and usage into the global composer atoms. Every tile create and session
branch called it, so opening a session in another worktree re-pointed the
MAIN pane's coding rail at that tile's repo — and persisted it, so the
wrong workspace cwd survived a restart.

Collect the patch first, then mirror it once behind a `foreground` gate.
Background callers still get the full patch for their own session state;
they just stop publishing into state they don't own.
2026-07-30 12:27:16 -05:00
brooklyn!
8defb9fd60
Merge pull request #74833 from NousResearch/bb/status-stack-seam
fix(desktop): fuse the status stack to the composer again
2026-07-30 12:06:36 -05:00
Austin Pickett
1fd7548b49 test(desktop): query the profile row kebab by its own label
main now labels each panel row's kebab with the row's name
(menuLabel={profile.name}), so the hardcoded "Actions" default this test
relied on no longer exists. The name alone is ambiguous — the row-select
button carries it too — so match the menu trigger via `expanded`.

Neither side conflicts textually, so this only surfaced once main merged in.
2026-07-30 12:31:38 -04:00
Austin Pickett
95571de9d7 fix(desktop): fold delete dialog into shared, level up name field, test the view
Addresses review on #73013.

1. Manage Profiles used a hand-rolled delete Dialog next to the shared
   DeleteProfileDialog in the same folder. That copy missed the active-
   profile re-home fix (f764b0400): deleting the profile the gateway is
   on stranded it on a dead backend. Switch to the shared dialog, which
   owns the deleteProfile call and re-homes to default. Drops
   handleConfirmDelete, the deleting state, and the now-unused Dialog*
   imports.

2. The name field regressed to a plain Input during the create-dialog
   dedup, losing live slugging. Level both shared dialogs up to
   SanitizedInput sanitize={slug} so every entry point gets the behavior
   Manage Profiles had — the sanitize primitive means callers never
   validate-then-reject.

3. Nothing rendered ProfilesView, which is how the drift got in. Add a
   behavior test: create dialog exposes SOUL.md, deleting the active
   profile re-homes to default, deleting a non-active one does not.
2026-07-30 12:31:38 -04:00
Austin Pickett
4d9b7718d9 fix(desktop): use shared create-profile dialog on Manage Profiles page
The Manage Profiles page had its own local CreateProfileDialog/
RenameProfileDialog copies that predated the shared dialogs in
create-profile-dialog.tsx / rename-profile-dialog.tsx. The local
create copy lacked the SOUL.md textarea, so New Profile from the
sidebar rail and New Profile from Manage Profiles rendered different
modals.

Delete both local duplicates and reuse the shared self-contained
dialogs (they own the createProfile/renameProfile/updateProfileSoul
calls), so both entry points show the same modal including SOUL.md.
2026-07-30 12:31:38 -04:00
Brooklyn Nicholson
77bdf932fc fix(desktop): fuse the status stack to the composer again
pb-2 on the in-flow stack wrapper opened an 8px gap under the card and
broke the shared seam the dock card is built for.
2026-07-30 07:47:02 -05:00
hermes-seaeye[bot]
b4f8c491d3
fmt(js): npm run fix on merge (#74827)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-30 12:46:25 +00:00
brooklyn!
fd90ef77be
Merge pull request #74815 from NousResearch/bb/composer-clear-parity
Clearing the composer empties it whichever way you do it
2026-07-30 07:36:28 -05:00
brooklyn!
466e6402f6
Merge pull request #74806 from NousResearch/bb/ref-parity
A sent reference renders as the chip the composer showed
2026-07-30 07:32:22 -05:00
hermes-seaeye[bot]
84d71fb88f
fmt(js): npm run fix on merge (#74814)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-30 12:31:53 +00:00
Brooklyn Nicholson
570337c099 fix(desktop): clearing the composer lands on the same empty state every way
Select-all + Delete cleared the composer; select-all + Cut left it holding
a draft. Both produce identical DOM, so the split was in the reader.

An emptied editor keeps a placeholder <br> \u2014 scaffolding
normalizeComposerEditorDom adds so the contenteditable doesn't collapse to
a sliver, not a line the user typed. composerPlainText read it as "\n",
and syncDraftFromEditor (session swap, pagehide) skipped the
normalize+sanitize its rAF twin runs. Cut's residue reached that reader;
Delete's went through the flush path and got cleaned.

Fixed where the two disagree rather than at each call site: an editor
holding nothing but its placeholder break reads as empty. A real
Shift+Enter break, a trailing break after text, and a nested lone <br>
are all unchanged \u2014 the exemption is scoped to the editor root by its slot
marker. syncDraftFromEditor now normalizes and sanitizes like the flush
path, so both readers see one truth.
2026-07-30 07:29:31 -05:00
brooklyn!
3eebb99ca8
Merge pull request #74804 from NousResearch/bb/tab-close-stay-right
Closing a tab leaves you on the one that fills its slot
2026-07-30 07:23:00 -05:00
brooklyn!
81aacdef4d
Merge pull request #74802 from NousResearch/bb/dismissed-projects-cmdk
Dismissed projects stay out of ⌘K
2026-07-30 07:17:01 -05:00
Brooklyn Nicholson
0c4a5d70f5 fix(desktop): a sent reference renders as the chip the composer showed
A reference whose value is backtick-quoted — `@url:` always, and any path
with a space — arrived in the sent bubble as a bare `@url:` followed by a
markdown code span. The composer showed a chip; sending it produced two
wrong things.

user-message-text scans inline code BEFORE handing the remaining text to
DirectiveContent, so it claimed the directive's quoting as a code span and
split the reference down the middle. Directives win that overlap: the
backticks are syntax the composer wrote, not something the user typed as
code.

The pattern itself lived in three identical copies (composer hydration,
sent bubble, and the one this fix needed), plus a fourth copy of the kind
list. They agreed today by luck. reference-kinds already owns what a
reference LOOKS like, so it now also owns what one IS: WIRE_REFERENCE_KINDS
and referenceRe(), a fresh matcher per call because a shared /g regex
carries lastIndex between callers.
2026-07-30 07:15:44 -05:00
brooklyn!
0567613497
Merge pull request #74790 from NousResearch/bb/worktree-diff-scope
fix(desktop): scope coding rail + review pane per worktree
2026-07-30 07:15:21 -05:00
Brooklyn Nicholson
ec1645a5ba fix(desktop): stay on the tab that fills a closed slot
Closing an active layout tab always selected the previous neighbor, so
focus jumped left every time. Prefer the right neighbor instead (left
only at the end) — same rule terminals and the preview rail already use.
2026-07-30 07:12:29 -05:00
brooklyn!
383829df2e
Merge pull request #74782 from HexLab98/fix/74761-update-marker-self-pid
fix(installer): adopt desktop-prewritten update marker (#74761)
2026-07-30 07:10:01 -05:00
Brooklyn Nicholson
ce6ecf306e fix(desktop): hide dismissed projects from ⌘K too
Remove-from-sidebar only filtered the overview; the command palette still
listed every auto project. Share one filter so both surfaces stay in sync.
2026-07-30 07:09:40 -05:00
Brooklyn Nicholson
87e14d1f2d fix(desktop): unclog lint on worktree-scoped rail PR
Perfectionist import order on the changed-files card and coding-status
tests, plus the padding blanks eslint wants in the new review scope tests.
2026-07-30 07:08:19 -05:00
Brooklyn Nicholson
2e3e9c1765 fix(desktop): scope coding rail + review pane per worktree cwd
Session tiles each live in their own worktree, but the coding rail's
branch/±LoC and the review pane both keyed off the main pane's global
cwd — and every refresh only re-probed that one tree. Cache status per
cwd, register on-screen rails, and pin the review pane to the surface
that opened it.
2026-07-30 06:21:55 -05:00
brooklyn!
14db1a99e2
Merge pull request #74781 from NousResearch/bb/pane-context-menus
Stop right-click showing a lone Select All on bare surfaces
2026-07-30 06:01:51 -05:00
HexLab98
67083c6dde test(installer): pin own-pid adopt for desktop-prewritten update marker
Regression for #74761: acquire must succeed when the marker already
names this process (desktop writeUpdateMarker raced ahead), and still
clean up on Drop.
2026-07-30 17:57:13 +07:00
HexLab98
160586ff8d fix(installer): adopt desktop-prewritten update marker with our own pid
Since #50238 the desktop writes .hermes-update-in-progress with the
spawned updater's PID before UpdateMarkerGuard::acquire runs. Without a
self-PID exclusion, live_marker_owner treated that as a foreign live
owner and every in-app desktop update aborted into a relaunch loop
(#74761). Treat our own PID as adoptable; keep refusing foreign live
updaters.
2026-07-30 17:57:13 +07:00
Brooklyn Nicholson
dba7bef5ce fix(desktop): stop right-click showing a lone Select All on bare surfaces 2026-07-30 05:55:30 -05:00
Brooklyn Nicholson
1afe076dbf fix(desktop): use the native Menlo/Monaco mono stack for code 2026-07-30 05:48:20 -05:00
Brooklyn Nicholson
b7ee610dc9 fix(desktop): drop the doubled inset around highlighted code blocks 2026-07-30 05:41:00 -05:00
brooklyn!
937222f4ec
fix(desktop): keep a mid-turn model pick painted in the composer (#74759)
The gateway now queues a model switch made during a turn and applies it
at the next turn start (#74756), but the desktop still bounced the pill
back to the old model: the post-switch refetch answered with the model
still running and repainted over the pick.

Skip that refetch when the switch was deferred — the apply publishes
session.info when it lands, and that is what re-syncs every surface.
An older gateway that still refuses with 4009 keeps the pick too rather
than rolling back and toasting at a user who did nothing wrong; it is
what the next turn runs anyway. Real failures still roll back and report.

The 4009 predicate lives beside the other gateway-compat probes in
lib/gateway-rpc.
2026-07-30 10:34:48 +00:00
brooklyn!
206eda50a5
Merge pull request #74734 from NousResearch/bb/tab-context-menu
Every tab strip gets the standard right-click tab menu
2026-07-30 05:23:06 -05:00
brooklyn!
dd4eadcf79
A finished turn ends on its changed files (#74732)
* feat(desktop): open the review pane on a given file

toggleReview is a toggle, so it can't back a "take me to the diff" affordance
-- pressing it when the pane is already up hides the thing you asked to see.
revealReview is the open-only half (toggleReview now calls it for its own open
branch), and openReviewForPath goes one further: refresh, then select the file.

A tool reports the path it wrote absolute while git reports repo-relative, so
the two are matched on the tail. fileEditPath is exported for the same reason
-- the caller needs the same path the tool row derives.

* feat(desktop): derive a turn's changed files from its tool parts

A finished turn already carries everything the summary needs: each file-edit
tool part holds the path it touched and the inline diff it produced. Folding
those into one row per file, with repeat edits to a file summed, means the card
costs no extra git probe.

Only landed edits count -- a call still running has no result, and a failed one
changed nothing.

* i18n(desktop): copy for the changed-files card

* feat(desktop): close a turn on its changed-files card

A turn that edited files now ends with a summary panel: one row per file with
its +/-, a Review action opening the diff pane, and a row click opening that
file's diff.

It rides only the newest message. The card describes a working tree, and that
tree has moved on by the next turn -- so rather than leaving a trail of stale
cards down the transcript, sending the next message retires it. While the turn
is still streaming the selector returns a stable empty list, so the tool rows
narrate the edits and the delta stream never re-renders the card.
2026-07-30 10:11:58 +00:00
hermes-seaeye[bot]
3b47e0c436
fmt(js): npm run fix on merge (#74751)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-30 10:11:53 +00:00
brooklyn!
9650f555d0
Merge pull request #74668 from NousResearch/bb/composer-attach
Attaching a file, folder, or link works like a picker, not a syntax
2026-07-30 05:01:45 -05:00
brooklyn!
19556db610
Merge pull request #74747 from NousResearch/bb/loading-bg
fix(desktop): match zone loading bg to chrome
2026-07-30 04:57:18 -05:00
brooklyn!
1ee75fc473
Merge pull request #74746 from NousResearch/bb/chat-widget-chrome
Inline chat widgets share one shell
2026-07-30 04:54:33 -05:00
Brooklyn Nicholson
7c59101c4a fix(desktop): give every tab strip the standard right-click tab menu
Right-clicking a tab that has no domain menu of its own — the main tab on a
fresh draft, the file tree, a terminal — fell through to the zone strip's
menu, which offered Split right/down/left/up and Hide header. Session tabs
never showed it (SessionTabMenu stops the event), so the split menu only ever
appeared on the surfaces least likely to want it.

ZoneMenu now renders the same verbs a session tab's menu does — Close, Close
others, Close to the right, Close all — over the shared ActionsContextMenu
kit, so both menus stay identical, above the strip's own header/minimize
toggles.

The Split actions were the only caller of splitTreeZone -> splitGroupZone, and
Move was the only caller of adjacentGroup; both chains are removed along with
the now-unused direction strings in every locale. Everything else the menu did
is still reachable: Move by dragging the tab, Hide header by double-tapping
the strip (and from a session tab's Hide tab bar), Minimize from the header
chevron.
2026-07-30 04:54:11 -05:00
Brooklyn Nicholson
c999dc2e8e refactor(desktop): one .ref class, and the theme owns every accent
A reference had two styling systems: a Tailwind class string assembled in
TypeScript (`directiveChipClass`) and a separate `link-chip` for prose links,
each carrying its own color-mix(). Same concept, three appearances.

Now every inline reference — a composer chip, a sent message's mention, a
markdown link, a completion row's glyph — is `class="ref"` plus
`data-ref="<kind>"`, and styles.css owns the accent. No hex or color-mix()
ships from a component, so a skin restyles all of them at once.

Keying the accent on `[data-ref]` alone rather than `.ref[data-ref]` also lets
the popover's icon column take a kind's hue without inheriting its inline-text
layout.
2026-07-30 04:52:06 -05:00