Commit Graph

2387 Commits

Author SHA1 Message Date
Brooklyn Nicholson e8ccb4a2ea feat(desktop): ctx.os — the curated OS door for plugins
Fold ctx.notifyNative into a ctx.os namespace so every way a plugin
reaches outside the app window lives behind one attributed door instead
of accreting one top-level ctx method per capability:

- ctx.os.notify — the native-notification door from the previous commit,
  unchanged semantics (plugin kind pref, away-gating, per-plugin throttle).
- ctx.os.openExternal / ctx.os.revealPath / ctx.os.writeClipboard — the
  existing window.hermesDesktop bridge capabilities, now sanctioned and
  result-shaped: each resolves false (never throws) when the bridge or
  member is missing, so a plugin branches on the result instead of
  sniffing the preload surface or crashing on an older shell.

No new Electron surface: everything routes through bridge members the
app already ships; the notification path keeps every existing gate.
2026-08-04 11:33:25 -06:00
Brooklyn Nicholson 62012a5362 feat(desktop): Show earlier pages the DOM, then pulls older history from the store
Show earlier spends the already-materialized DOM budget first and only asks the
session store for another page once that is exhausted, so the click stays cheap
and the store window stays as small as it can be.

Paging has no ceiling: each expand grows the window by one budget page until the
whole transcript is loaded. Branch persistence stays wired throughout —
setMessages is never dropped, so switchToBranch and applyBranchVisibility keep
working on a windowed session.

Co-authored-by: HexLab <8422520+HexLab98@users.noreply.github.com>
2026-08-04 11:32:11 -06:00
Brooklyn Nicholson a538b1c989 fix(desktop): bound the transcript reaching assistant-ui by render cost (#55191)
An oversized session rebuilt an unbounded runtime repository on every store
update and exhausted the renderer's V8 heap, crash-looping the window. The DOM
budget in thread/list.tsx bounds what PAINTS, but every message was still
normalized into the repository first, so a session only had to be heavy — not
visible — to kill the renderer.

selectTranscriptWindow keeps the tail that fits one render-weight page. Weight,
not message count: measured against a real 1,175-session store, a 400-message
cap disengages on 37 sessions that are heavy but short (one is 133 messages /
1.05MB) while firing on 92 long-but-light sessions that were never at risk.

The cut aligns off branch-group boundaries. useRuntimeMessageRepository records
a group's fork point the first time it sees the group, so a window starting
mid-group would re-parent the surviving branches to whatever happened to
precede them.

Co-authored-by: HexLab <8422520+HexLab98@users.noreply.github.com>
2026-08-04 11:32:01 -06:00
Brooklyn Nicholson 1ed702be73 refactor(desktop): share render weight between the two transcript budgets
messageRenderWeight moves out of thread/list.tsx into lib/render-weight.ts.
The DOM page budget already spends render cost rather than message count —
the store window added next needs the same currency, and one weight function
keeps the two layers from drifting apart.

No behavior change.
2026-08-04 11:31:51 -06:00
seref 5d24594ab3 feat(desktop): expose native OS notifications to plugins via ctx.notifyNative
Desktop plugins can toast in-app (host.notify) but have no sanctioned way to
reach the OS notification pipeline the app's own approval/turn alerts use, so
a plugin surfacing a genuinely notable background event (e.g. a discovery
plugin finding a match) stays invisible once the user steps away from Hermes.

Add a curated per-plugin door instead of exporting the raw dispatcher:

- ctx.notifyNative({ title, body?, silent? }) on PluginContext — attributed
  to the plugin id, routed through dispatchNativeNotification so every
  existing gate applies (master + per-kind prefs, post-connect baseline,
  away-from-app gating, throttle).
- New 'plugin' native-notification kind with its own Settings ▸ Notifications
  toggle (default on), so users silence plugins without losing app alerts.
- New optional `tag` discriminator on the notify payload keys the renderer
  throttle and main-process cross-window dedupe per plugin, so two plugins
  can't collapse each other's session-less notifications.

Consumer: the Index Network desktop plugin wants background opportunity
alerts; anything in ~/.hermes/desktop-plugins gets the same door.
2026-08-04 11:28:20 -06:00
Brooklyn Nicholson 91337e5789 fix(desktop): ⌘1 / ⌃Tab return to the chat from a full-page view
Hitting ⌘1 (or cycling ⌃Tab onto the main tab) while Capabilities /
Messaging / Artifacts covered the workspace looked dead: the workspace
pane was already the zone's active tab behind the page, so fronting it
changed nothing on screen.

activateTreeTabSlot / cycleTreeTabInFocusedZone now return the activated
pane id, and the keybind handlers route back to the loaded session (or
the new-chat draft) when the landing pane is the workspace under a full
page — the same rule openSession already applies.
2026-08-04 11:26:12 -06:00
izumi0uu cab8673ea6 fix(dashboard): reload loopback tabs after stale session-token closes
Loopback dashboard tabs now share one one-shot stale-token recovery path across REST 401s, the PTY socket, the structured event socket, and the shared JSON-RPC gateway wrapper. The shared client exposes only an optional close-event interception hook; the dashboard remains responsible for deciding that loopback 4401 means reload.

Constraint: Current main delegates the web gateway to apps/shared JsonRpcGatewayClient, and #54022 review requires a shared-client-compatible close-code hook plus direct ChatSidebar event-socket coverage.
Rejected: Restore the dashboard's old direct WebSocket implementation | stale against the shared JSON-RPC client and would duplicate transport behavior.
Confidence: high
Scope-risk: moderate
Directive: Keep stale-token policy dashboard-specific; the shared JSON-RPC client should expose close events without learning dashboard auth semantics.
Tested: npm --workspace web test (21 files, 106 tests); focused stale-token tests (5 files, 14 tests); npm --workspace web run typecheck; npm --workspace @hermes/shared run lint; npm --workspace @hermes/shared run typecheck; focused web eslint; git diff --check.
Not-tested: Manual browser smoke test across a real dashboard restart.
2026-08-04 15:26:37 +05:30
hermes-seaeye[bot] 9076adaca5
fmt(js): `npm run fix` on merge (#78271)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-04 07:10:23 +00:00
kshitij be54f28b16 test(desktop): cover minimized/hidden window-state + visibilitychange pause for GlyphSpinner
Regression coverage requested in review of #74357: mock
window.hermesDesktop.onWindowStateChanged (pattern from
persistent.test.tsx) and assert minimized/hidden clears the spinner
interval while restore resumes it; also cover document.visibilityState
hidden/visible via visibilitychange.
2026-08-04 12:30:44 +05:30
Mykola Bashmakov 9a20d7f680 perf(desktop): keep spinner frames out of React commits
Advance the existing animated status glyph through its DOM text node instead of React state, and pause its timer for hidden panes or inactive windows. Cover frame advancement, zero update-phase commits, and timer suspension with behavior tests.
2026-08-04 12:30:44 +05:30
kshitij 9d76d48d0a fix(lint): import sort + eslint-disable for timer-handle ref clear in effect
CI-caught: cron-jobs-section had an extra blank line between sorted imports; use-message-stream's visibility-flush effect assigns flushHandleRef.current=null inside a useEffect (legitimate timer-clear, not an atom mirror) — eslint-disable-next-line per the rule's documented convention.
2026-08-03 20:29:58 +05:30
kshitij aece98c5f3 refactor(desktop): shared pulse beat + fully-gated cron peek (simplify folds)
Two findings from the simplify pass on the final trio diff:

- status-pulse: one pause controller + one aligned period timer shared by all StatusPulse instances (ref-counted), instead of N x (document/window/bridge listeners + unsynchronized 5s wakes) — a sidebar can show dozens of pulsing dots. Pause still cancels in-flight animations so the compositor sleeps immediately.
- cron-jobs-section: the runs-peek effect created its interval even while the pane was hidden (callback no-oped but the timer still woke the renderer every 8s/60s per expanded job). Early-return when hidden — visibility is already in the dep array, so becoming visible restarts load + timer.
2026-08-03 20:29:58 +05:30
kshitij e2a2149df4 style(desktop): restore alphabetical import order in agents/index.tsx 2026-08-03 20:29:58 +05:30
Daisuke Suzuki 52fb96de4b perf(desktop): pause hidden-pane timers in agents view, cron sidebar, and floating pet
Partial pick of the surviving renderer hunks from #75395 (perf commit
6502e441d plus fixup 3fbbc9c1d): gate the 500ms subagent now-ticker and
the cron sidebar 1s ticker/run-poll on usePaneVisible, and skip the
legacy floating-pet poll while the document is hidden. Dropped hunks
(electron/main.ts, vitest.setup.ts/config) intentionally excluded.
2026-08-03 20:29:58 +05:30
Mykola Bashmakov 7700597a17 perf(desktop): stop scroll and status loops in busy sessions 2026-08-03 20:29:58 +05:30
M S 416b56b7eb fix(desktop): flush queued deltas on window focus 2026-08-03 20:29:58 +05:30
brooklyn! c2c95f5339
Merge pull request #77713 from NousResearch/bb/review-76744-fold
fix(desktop): stop the inflight dump sandwiching structured mid-turn rows
2026-08-03 08:39:39 -05:00
brooklyn! b536f0697d
Merge pull request #77644 from NousResearch/bb/review-75967
fix(desktop): keep a mid-turn reply on screen when its session is reopened
2026-08-03 08:16:10 -05:00
kshitij c4ac62a7ee fix(desktop): cancel the pending commit-cost measurement rAF
Follow-up to #77652: each runFlush registered a fresh requestAnimationFrame and never cancelled it. Chromium parks rAF callbacks for hidden renderers, so a long hidden stream at the 33ms floor accumulates thousands of parked closures that all fire in the first frame on refocus (all but one no-oping through the stale-frame guard). Track the pending handle, cancel it before requesting a new one (only the newest flush's measurement matters), and cancel on unmount.
2026-08-03 18:45:38 +05:30
Brooklyn Nicholson 7a4d047e37 refactor(desktop): one live-tail vocabulary for transcript reconciliation
Two fixes landed overlapping helpers on the same statement: the mid-turn
reply guard grew `isLiveProjectionRow` / `hasStreamedContent`, while the
inflight-dump guard grew `isLiveTailRow` / `hasStructuralParts`. Two
definitions of "is this row live" and "does it carry content" in one
function is how the next change silently reshapes one of them.

Collapse to a single module-level pair. `isLiveTailRow` now covers pending,
stream ids, inflight projections and sealed interim rows, so the reply guard
also stops treating an interim row as committed history; `hasStreamedContent`
is defined in terms of `hasStructuralParts`. Both text-extension checks route
through `isStrictAnswerTextExtension` rather than a bare `startsWith`.

Also hoists the live-tail lookup out of an inline IIFE and fixes the lint
warnings it carried.

Co-authored-by: 686f6c61 <github@00b.tech>
2026-08-03 08:13:57 -05:00
686f6c61 dbefd27ffb fix(desktop): require structure-bearing row for live-tail same-turn carry
Structure-only same-turn carry used (live(previous) || live(message)), so a
new live text-only assistant at a compression-rewritten ordinal could inherit
reasoning/tool parts from an unrelated historical structured row.

Require the structure-bearing cached row itself to be live-tail (pending /
assistant-stream-* / interim). Add regressions for non-extending live dump
carry and the compression graft rejection.

Addresses salvage path on #76744 / #76444.
2026-08-03 08:08:47 -05:00
686f6c61 2ecc1db276 fix(desktop): scope inflight dump suppression to the live turn tail
Only skip/graft structure for the current live assistant (stream id,
pending, or after the latest user), not completed historical tool rows.
Require live-tail identity for same-turn structure carry. Align journal
overlay with strict answer-text extension.

Addresses review + CI on #76744.
2026-08-03 08:08:47 -05:00
686f6c61 53398ff485 fix(desktop): do not sandwich structured mid-turn rows with inflight dump
Skip pure-text inflight.assistant projections when the transcript already
has reasoning/tool-call structure, and only overlay journal answer text
on strict extension.

Fixes #76444
2026-08-03 08:08:47 -05:00
kshitij 1f692a8be2 refactor(desktop): hoist the reference-line matcher; drop dead textWithoutImageRefs
Follow-up to #77653: textWithoutReferenceLines built a fresh /g RegExp per call and hand-managed lastIndex — but it runs on both sides of every message comparison in the reconcile loops. An anchored non-global regex has no shared-lastIndex hazard and can be hoisted to module scope. Also removes textWithoutImageRefs, whose last production consumer #77653 replaced (kept IMAGE_REF_LINE_RE for extractImageRefs), and retargets its now-stale comment.
2026-08-03 18:37:31 +05:30
Brooklyn Nicholson 34f0427f7f fix(desktop): stop a finished reply rendering twice after history catches up
When a turn's reply commits under its own id, the settled local
`assistant-stream-*` row shifts one assistant ordinal earlier, so ordinal
pairing finds nothing at its slot and re-appends it — the same answer twice.

Drop a settled stream row only when the authoritative transcript already
carries that exact text. Keying `isPendingAssistant` on the explicit pending
flag alone would also have fixed this, but it discards the sibling case in
the same report: a reply that finished locally before the gateway committed
it, where the local row is the only copy that exists.

Co-authored-by: Dolverin <59100064+Dolverin@users.noreply.github.com>
2026-08-03 07:57:33 -05:00
Brooklyn Nicholson b6f15f546d fix(desktop): keep a mid-turn reply on screen when its session is reopened
Switching sessions while a turn streams (or right as it completes) could
leave the assistant reply missing until restart. Resume merges stored
history with the gateway's `inflight` projection, whose assistant row is
text-only and often an empty `assistant-stream-${sessionId}` shell; both
reconcile paths then dropped the local pending row that held the only copy
of the streamed text, reasoning and tool calls.

A shared pair of guards replaces the ad-hoc comparisons at all three sites.
`localPendingSupersedes` accepts the cached row only when it is the same
reply further along — an empty shell it has content for, or text it strictly
extends — so a longer unrelated row can no longer hijack an ordinal or reuse
a stream id, and a retained `inflight.error` snapshot is never mistaken for
an empty shell. `withAuthoritativeTurnState` then takes content from the
renderer while liveness, row id and reactions stay the backend's call, so a
settled shell cannot leave a finished reply spinning.

Co-authored-by: arimu1 <19286898+arimu1@users.noreply.github.com>
2026-08-03 07:52:37 -05:00
Lex 82fd574bad perf(desktop): stop idle chat re-renders — memo ChatView, stable tile props, gated adapter re-sync
Re-derive of PR #38470 on today's main (its target file desktop-controller.tsx no longer exists after the contrib/ refactor; the three surviving ideas are applied at their new homes):

- incremental-external-store-runtime: the dep-less setAdapter effect ran every render; gate on [runtime, store] — behavior-preserving because __internal_setAdapter early-exits on identical store.
- ChatView is now memo()d, and session-tile hoists its inline arrow props to useCallbacks/module constants so the memo actually holds.
- Render-count regression test (mocked Thread) proves an unrelated parent re-render no longer re-renders the chat shell.

Credit: idea and original implementation by @hdd69 in #38470.
2026-08-03 18:17:24 +05:30
rerdi92 1306ac0897 fix(desktop): un-break the .btn-arc rule — '*/' inside a CSS comment ended it early
The comment above .btn-arc contained 'bg-*/', whose */ terminated the comment mid-sentence, leaving 'text-* variant utilities. */ .btn-arc {' as an invalid prelude — CSS error recovery can drop the whole .btn-arc rule. Reword so no */ appears inside the comment.

Extracted from #59352 by @rerdi92 (the rest of that PR — a month-stale icons.ts rewrite and a chunk-size warning-ceiling bump — is superseded/masking and was not salvaged).
2026-08-03 18:16:46 +05:30
kshitij 5ffbea81e9 fix(desktop): escalate gateway reconnect on elapsed time, not attempt count
With the full-jitter backoff (300ms base) six attempts can elapse in ~9s,
so the old RECONNECT_ESCALATE_AFTER=6 attempt threshold raised the
recoverable boot error during a brief post-boot blip — breaking the
'a remote that drops post-boot keeps looping with NO boot.error' contract.
Escalate after RECONNECT_ESCALATE_AFTER_MS (45s, matching the old
deterministic 1->15s ladder's calibration) elapsed since the first failed
reconnect of the episode. Reset on clean open, manual/wake reconnect, and
soft switch, preserving the reset-on-success path.
2026-08-03 18:10:30 +05:30
Hermes Coder ed66ff17d8 fix(desktop): full-jitter backoff on gateway WS reconnect loops
All three desktop reconnect loops (primary gateway boot, secondary
multi-profile gateway pool, plugin event socket) used bare exponential
backoff with no jitter. After a gateway restart every disconnected
client redials on the exact same schedule, so the reconnect attempts
land in lockstep instead of spreading out -- a burst that can starve
the gateway's file descriptors while it's still coming back up.

Add reconnect-backoff.ts implementing AWS-style full-jitter backoff
(random delay in [0, min(cap, base * 2^attempt))) and wire it into all
three call sites in place of their local Math.min/2**attempt math.
Manual reconnect paths already reset the attempt counter and bypass
the timer entirely -- unchanged.
2026-08-03 18:10:30 +05:30
Paulo Nascimento 2ebe175dc1 fix(desktop): sort reference-kinds import per lint gate 2026-08-03 18:10:23 +05:30
Paulo Nascimento fed10fa245 test(desktop): cover wire reference normalization edges 2026-08-03 18:10:23 +05:30
Paulo Nascimento 3060beea07 fix(desktop): dedupe optimistic user turns for all wire references, not only images 2026-08-03 18:10:23 +05:30
Adolanium b9b0505cfc fix(desktop): measure adaptive stream flush through the deferred commit frame
scheduleDeltaFlush's adaptive floor is driven by lastFlushCostRef, but
runFlush only timed flushQueuedDeltas(), the synchronous store write.
While a session streams, syncSessionStateToView defers the $messages
publish (React commit + Streamdown re-parse) to its own rAF, so the
measured cost stayed near zero and the floor collapsed to the fixed
33ms path no matter how expensive the real commit was.

runFlush now records the write cost as a fallback, then extends the
measurement through a rAF registered after the view-sync one: it runs
in the same frame right after the deferred commit, and the rAF
timestamp marks frame start so only in-frame work is counted, not the
vsync wait. A stale callback from before a newer flush is ignored, and
a hidden renderer that never fires rAF keeps the write-cost fallback.
2026-08-03 18:10:18 +05:30
kshitijk4poor dd08277104 test: expect omit_messages in the tile-delegate resume call shape
Two more call-shape-pinning tests (cold tile resume, default-profile
resume) assert session.resume's exact params; the delegate passes
omit_messages: true like every other Desktop resume call site.
Swept all 5 desktop test files that reference session.resume/activate:
380 of 381 files green (the one failure is a pre-existing locale-
dependent number-grouping test that fails identically on clean main).
2026-08-03 13:45:08 +05:30
kshitijk4poor 67fb0d7c2e test: expect omit_messages in the queue-drain resume call shape
Two queue-drain tests added on main pin session.resume's exact params;
the drain path now passes omit_messages: true. Assertion-only update.
2026-08-03 13:45:08 +05:30
Daniel Blank cd41454dfc fix(gateway): let Desktop omit duplicate transcripts on session resume
Salvage of #69926: omit_messages support ported from the PR's
tui_gateway/server.py base onto the post-split methods_session.py
layout. When a Desktop client passes omit_messages=true on
session.resume / session.activate, the RPC returns messages: [] with
messages_omitted: true and an accurate message_count, skipping the
potentially multi-megabyte compression-lineage serialization over the
WebSocket; Desktop hydrates the transcript via the authenticated REST
route in parallel.

The PR's bundled cron-outputs endpoint and codex quiet-timeout bump
were dropped from this salvage as unrelated (invited back separately).
2026-08-03 13:45:08 +05:30
kshitijk4poor e1843c7d08 style: fix the type-import sort position in pet-gallery.test.ts
perfectionist/sort-named-imports orders 'type GatewayRequest' by its
name, so it belongs before loadPetGallery (eslint error, not warning).
2026-08-03 13:41:12 +05:30
kshitij b0089e8bfa perf(desktop): skip store update when pet metadata is unchanged
mergePetInfoMeta now returns the same object reference when all fields
match, and callers skip setPetInfo on reference equality. Without this,
every 15s poll and window-focus refetch fired a nanostores set with a
new-allocated object, triggering a React re-render of FloatingPet even
when nothing changed — a regression from the old samePetRevision guard
which returned without calling setPetInfo.
2026-08-03 13:41:12 +05:30
kshitijk4poor cb0226d4ae docs: update stale samePetRevision comment reference
The helper was extracted and renamed to hasPetSpriteForMeta +
mergePetInfoMeta; the pet.changed comment still cited the old name.
2026-08-03 13:41:12 +05:30
embwl0x 1573829a7c fix(desktop): avoid repeated pet spritesheet fetches 2026-08-03 13:41:12 +05:30
Johnny 51defb9e29 fix(desktop): adapt right pane probe to per-cwd status 2026-08-03 13:39:33 +05:30
Johnny 1051434325 perf(desktop): isolate right pane layout work 2026-08-03 13:39:33 +05:30
kshitij 372494fdf9 style(desktop): eslint --fix import order in sessions-section test
Review follow-up on the #75714 salvage: perfectionist/sort-imports
would fail lint CI; autofixed.
2026-08-03 10:00:20 +05:30
StanleyStetson 416f91d271 fix(test): export VirtualSessionListProps and assert sessions array identity change 2026-08-03 10:00:20 +05:30
StanleyStetson 09ed1ac950 fix(desktop): memoize sidebar flatRows and row renderers to prevent scroll jitter (fixes #73629)
Fix direction inspired by PR #73674 by @drbronson with added Vitest component unit tests.
2026-08-03 10:00:20 +05:30
hermes-seaeye[bot] d79a6f0e3e
fmt(js): `npm run fix` on merge (#77300)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-03 03:44:44 +00:00
Brooklyn Nicholson 9e373b3828 fix(desktop): paste rich text with images as text, not blank attachments
Copying a Discord thread (or any rich-text selection with images) attached
one or more blank thumbnails and dropped the message text entirely.

Two causes. The clipboard's `text/html` was scraped for inline
`<img src="data:…">` regardless of whether the copy carried its own text —
and what Discord ships beside each image embed is a 32x5 blurhash
placeholder, which is exactly the blank attachment. Then, because any image
blob short-circuited the paste handler, the prose that came with it never
reached the composer.

Inline HTML images now only count for an image-only copy, and are ignored
below a thumbnail-sized floor so spacers and trackers don't attach either. A
mixed paste attaches its real images and still inserts its text.

Also registers pasteAndMatchStyle in the Edit menu — Cmd+Shift+V had no menu
entry, so the chord was never translated into an editor command anywhere in
the app.
2026-08-02 22:28:28 -05:00
Brooklyn Nicholson 17bdc99db4 feat(desktop): reload a tab from its right-click menu
Right-click any tab and pick Reload: the pane's content remounts in
place — effects re-run, state resets, measurements are retaken — while
the tab keeps its slot and every other tab is untouched.

A per-pane epoch atom keys the contribution inside the zone body, so
reload never rewrites the layout tree. Both tab menus offer it: the
zone strip menu (tool panels, the file tree, a fresh draft's main tab)
and the session tab menu (tiles + the loaded main tab).
2026-08-02 22:08:09 -05:00
Jason Chai 5b73cfcc98 fix(desktop): bound review workspace scans 2026-08-03 00:08:30 +05:30
Jason Chai c5829fbb34 fix(desktop): account for transcript payload size 2026-08-03 00:08:30 +05:30
Brooklyn Nicholson 63ff4b87b6 fix(install): sync the lockfile engines mirrors with the manifests
`hermes desktop` still failed with EBADENGINE demanding Node >=26 after
#76562, on a machine whose `apps/desktop/package.json` already said
`^20.19.0 || >=22.12.0`. #76562 fixed the manifest but not its mirror in
`package-lock.json`, and `npm ci` reads engines from the lockfile:

    package.json  apps/desktop -> {'node': '^20.19.0 || >=22.12.0'}
    package-lock  apps/desktop -> {'node': '>=26.0.0'}      <- what gated

Chasing that exposed a second, pre-existing problem: the floor #76562
declared was too generous. Running the real `npm ci` against the whole
workspace on Node 22.21.1 fails on a transitive dependency —

    npm error notsup Not compatible with your version of node/npm:
      react-router@8.3.0
    npm error notsup Required: {"node":">=22.22.0"}

react-router 8.3.0 (a direct dependency of both `apps/desktop` and `web`)
declares `>=22.22.0`, which is tighter than Vite's `^20.19 || >=22.12` and
excludes all of Node 20. So `>=20.0.0` promised support the tree cannot
deliver: an install on Node 20 or early 22 passed the installer's gate and
then died inside `npm ci` on someone else's package.

All four engine declarations now state the floor the dependency tree
actually has, `>=22.22.0`: root `package.json`, `apps/desktop/package.json`,
and both of their `package-lock.json` mirrors. The installer gates move with
them (`node_satisfies_build` in install.sh, `Test-NodeVersionOk` in
install.ps1) so a too-old system Node is replaced with the managed one
*before* npm runs, and the failure a user does see names hermes-agent rather
than a transitive package. NODE_VERSION stays 22 — latest-v22.x is 22.23.2,
comfortably above the floor.

The invariant test gains the case that would have caught the mirror drift on
its own: the desktop assertion now pins the tightest floor a dependency
actually declares, and the managed-runtime check compares majors, since
install.sh fetches latest-v{major}.x rather than {major}.0.0.

Verified with real `npm ci --dry-run` over the full workspace:
- node 22.23.2 (what install.sh provisions) -> 1258 packages
- node 26.5.1                               -> 1189 packages
- node 22.21.1 (below the floor)            -> EBADENGINE naming
  hermes-agent, i.e. our own manifest, not react-router
2026-08-01 23:28:28 -05:00
Brooklyn Nicholson 8e08a4a16e fix(install): restore installs — engines floor no shipping toolchain can meet
Fresh installs and `hermes update` both fail at the first `npm ci`:

    npm error code EBADENGINE
    npm error notsup Required: {"node":">=26.0.0","npm":">=12.0.0"}
    npm error notsup Actual:   {"node":"v24.15.0","npm":"11.12.1"}

`.npmrc` sets engine-strict=true, so `engines` is a hard gate on every
install. The floor was raised to npm >=12 — but no Node release bundles
npm 12: Node 26 ships 11.17.0, 24 ships 11.16.0, 22 ships 10.9.8. The
requirement is unsatisfiable by any stock toolchain, so the installer
provisions a Node and is immediately unable to install with it.

engines.npm becomes `<11.10.0 || >=11.17.0`. That still excludes the band
the strictness was actually for: npm 11.10-11.16 honor `min-release-age`
but ignore `min-release-age-exclude`, both set in .npmrc, so they apply the
14-day gate to packages we exempted. Verified rather than assumed — npm
11.12.1 fails `ETARGET ... vite@8.2.0 with a date before 7/18/2026` while
11.17.0 installs it.

engines.node returns to >=20.0.0 and the toolchain floor to Node 22.
Nothing in the tree needs 26: Vite 8.2.0 declares `^20.19.0 || >=22.12.0`
and Electron 40 declares `>=12.20.55`. Requiring 26 force-migrated every
working install for no dependency reason. apps/desktop drops to Vite's own
floor for the same reason; the desktop bundle builds clean on Node 22.

install.sh gained a second gate: a system Node was accepted on version
alone, so a machine with Node 24 + its bundled npm 11.16.0 (the bad band)
passed the check and then failed `npm ci`. npm_supports_npmrc() now rejects
that band and installs the managed Node instead.

CI, Docker and nix are hermetic and keep pinning Node 26 / npm 12 — they
provision their own toolchain, and both satisfy the relaxed range.

tests/test_engines_satisfiable.py encodes the invariants that would have
caught this: the npm floor must be met by an npm some shipping Node
bundles, the node floor by the runtime install.sh provisions, the desktop
floor by its own build toolchain, and the lockfile mirror must match.
Restoring the broken values fails 5 of them with the reason stated.

Verified end-to-end (real downloads, temp HERMES_HOME):
- fresh install: managed node v22.23.2 / npm 10.9.8 -> npm ci, 209 packages
- existing managed tree (v22.22.3 / npm 10.9.8) -> npm ci, 209 packages
- node 26.5.1 + bundled npm 11.17.0 -> npm ci, 208 packages
- system npm 11.12.1 (the reported case) -> EBADENGINE, recovery provisions
  a managed tree and retries green
- apps/desktop `npm run build` on Node 22 -> dist built, assert passes
2026-08-01 22:56:52 -05:00
hermes-seaeye[bot] 8bfd5af3bc
fmt(js): `npm run fix` on merge (#76547)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-02 03:33:09 +00:00
brooklyn! 85c8956ec7
Merge pull request #76459 from NousResearch/ethie/bundled-node-path-windows-layout
fix(runtime): managed Node/uv resolve first everywhere; require Node 26
2026-08-01 22:09:56 -05:00
Brooklyn Nicholson f979e47eba fix(desktop): the branch-bar path copy confirms in place
The glyph sat at the far end of the row instead of beside the path it
copies, and it fired a toast for a one-word confirmation. It's the shared
CopyButton now — same inline checkmark as every other copy in the app —
and the path label sizes to its content so the pair reads as one unit.
2026-08-01 21:46:19 -05:00
Brooklyn Nicholson 8de9c16b7c Merge branch 'main' into ethie/bundled-node-path-windows-layout
#76499 landed the npm floor as >=11.17.0 on a Node >=20 baseline. This
branch takes the other half of the same constraint: the vendored Node 26
tree now installs npm 12 into itself, so the toolchain satisfies the
stricter floor rather than the manifest relaxing to meet the tarball.

Resolved package.json + package-lock.json to node >=26.0.0 / npm >=12.0.0
and refreshed npm_engine.py's illustrative range to match. website/'s
mirror keeps #76499's >=11.17.0 — it is not a root workspace and builds
on its own Node.
2026-08-01 21:27:26 -05:00
ethernet 22df1840d6 fix(desktop): restore localStorage in jsdom tests under Node 26
Node 26 defines its own `localStorage` accessor on the global object,
which returns `undefined` unless the process was started with
`--localstorage-file` (hence the "localStorage is not available because
--localstorage-file was not provided" warning now printed by every
worker). In the jsdom environment `globalThis` IS the window, so that
accessor shadows jsdom's Storage and every `localStorage.getItem(...)` in
a test throws "Cannot read properties of undefined".

CI caught this on the Node 26 bump: `check:test:ui` failed with 22
errors across session.test.ts, terminals.test.ts, model-settings and
onboarding stores — all storage-backed. Reproduced locally against
nodejs_26 (12 failures in src/store/session.test.ts alone) before fixing.

vitest.setup.ts now installs a real in-memory Storage on both globalThis
and window when the global resolves to undefined, before any test module
reads it. Guarded on `typeof === 'undefined'` so Node < 26 and any future
runtime that provides a working Storage keep jsdom's own implementation.

Verified under nodejs_26: the full `--project ui` lane is 378 files /
3268 tests green (was 22 failures).
2026-08-01 21:43:47 -04:00
Brooklyn Nicholson e1ccd674c0 fix(desktop): apply the stale-installer marker guard to both hand-offs
Both hand-off sites pre-write the update marker: the in-app Update button
(applyUpdates) and the Windows bootstrap-recovery path
(handOffWindowsBootstrapRecovery). Either one can strand a user on a
pre-#74782 staged installer, and the recovery path is worse — it fires
when the install is already unhealthy, so a refused claim there wedges
the very repair meant to heal it.

Route both through stagedUpdaterSupportsPrewrittenMarker and log the skip
so the reason is visible in desktop.log instead of looking like a missing
write.

Also document on copy_self_to_hermes_home that its --update no-op is what
lets an installer-protocol change strand the entire installed base on a
binary that predates it — the root enabler of this class of bug.
2026-08-01 20:43:08 -05:00
Brooklyn Nicholson 5b3b761404 fix(desktop/windows): don't pre-write the update marker for stale installers
copy_self_to_hermes_home no-ops during --update, so the hermes-setup.exe
staged by a user's ORIGINAL install orchestrates every later update
forever. Installers predating #74782 have no self-PID exclusion in
UpdateMarkerGuard::acquire, so when the desktop pre-writes the marker
naming that very updater (#59313), the updater reads its own claim as a
foreign live owner and aborts:

  Another Hermes update is already running (PID <itself>, started 1s ago)

mapped to the "Hermes is still running. Close all Hermes windows" screen.
Retry relaunches the desktop, which pre-writes a fresh marker naming the
next updater, which refuses itself again — an unbreakable loop. The
always-live PID also defeats the staleness self-heal in
readLiveUpdateMarker, and the update that would replace the stale binary
is precisely the one being refused, so there is no route out.

Gate the pre-write on the staged installer's mtime, which faithfully
stamps the installer generation (the binary is written at install/repair
time). Anything staged before the self-adopt fix skips the pre-write and
lets the updater write its own claim; the hand-off itself is untouched,
because that stale binary is the only updater those users have and it
works fine once allowed to acquire.

Unreadable mtime counts as unsupported: skipping the pre-write only loses
anti-respawn hardening, while a wedged updater can never update again.
2026-08-01 20:43:01 -05:00
brooklyn! eca996aa33
Merge pull request #76401 from NousResearch/bb/composer-link-open
Act on composer directive chips from a hover pill
2026-08-01 20:29:14 -05:00
hermes-seaeye[bot] 777512b760
fmt(js): `npm run fix` on merge (#76498)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-02 01:23:45 +00:00
Brooklyn Nicholson 5ba2564ca0 feat(desktop): act on composer directive chips from a hover pill
A directive chip (`@url:`, `@session:`) reads as the thing it points at and is
coloured like one, but a composer is an editor — a click inside the
contenteditable only places the caret, so there was no way to actually act on
the reference.

Hovering a chip whose kind has an action now floats a pill above it that runs
it: `@url:` opens in the browser, `@session:` opens the session as a tab. It's
a small registry (`DIRECTIVE_ACTIONS`), so a new actionable kind is one entry,
not another watcher.

The pill portals to `<body>` and anchors to the chip's rect, so it can't end
up inside the submitted draft, and it re-anchors on scroll and resize rather
than stranding itself over a reference that moved or was deleted. The press is
swallowed before it reaches the editor — mousedown in a contenteditable moves
the caret, and the edit composer reads a blur as "cancel".

Listeners bind to `document`, not the editor: the edit composer's
contenteditable isn't reliably attached when the effect first runs, so an
editor-bound listener never fired there. A document listener that reads the
editor lazily works in both composers, and each instance filters to its own
editor so one chip never shows two pills.
2026-08-01 20:21:28 -05:00
ethernet 713a983e4a feat(runtime)!: require Node 26 across all installers, heal, and upgrade paths
Hermes now pins its toolchain to Node 26 everywhere. Every path that
installs, accepts, heals, or upgrades a Node runtime moves from the old
22-default / `^20.19 || >=22.12` floor to a single rule: Node >=26.

Installers:
- scripts/install.sh — NODE_VERSION=26; node_satisfies_build() collapses
  the two-branch Vite floor to `major >= 26`; user-facing messages updated.
- scripts/install.ps1 — $NodeVersion=26; Test-NodeVersionOk likewise;
  winget fallback switches OpenJS.NodeJS.LTS -> OpenJS.NodeJS (26 is
  Current, not LTS — the LTS manifest would reinstall a too-old Node).
- Dockerfile — node_source stage node:22-bookworm-slim -> node:26 (digest
  pinned, amd64 sha256:9e6f...bf73).
- nix/ was already on nodejs_26 (lib.nix, npm-12-0-2.nix); the checks.nix
  wrapper check ratchets from `>= 20` to `>= 26`.

Heal/upgrade paths:
- scripts/lib/node-bootstrap.sh — HERMES_NODE_TARGET_MAJOR default 22->26
  and HERMES_NODE_MIN_VERSION default 20->26, so heal_managed_node,
  _nb_install_bundled_node, and the fnm/proto/nvm/brew rungs all target 26
  and stop accepting an on-PATH Node below it. Both remain env-overridable.
- hermes_constants.py — _HERMES_NODE_TARGET_MAJOR fallback 22->26, which
  drives the Windows heal path's latest-v26.x download.

Version gates:
- package.json engines.node >=20 -> >=26; apps/desktop engines
  `^20.19.0 || >=22.12.0` -> `>=26.0.0`.
- CI setup-node: all five workflows 22 -> 26.
- Docs describing Hermes's own toolchain updated (windows-native, docker,
  acp, nix-setup, contributing). Skill docs describing third-party tools'
  own requirements are untouched.

Termux still installs via `pkg install nodejs` best-effort (nodejs.org
ships no Android tarballs); that path was never version-gated.

Verified: bash -n on both shell scripts, PowerShell AST parse of
install.ps1, latest-v26.x index resolves (node-v26.5.1), and the install
test suite — 18 tests across the 5 install/runtime test files — passes.
2026-08-01 21:17:51 -04:00
brooklyn! baec57de66
Merge pull request #76429 from NousResearch/bb/composer-placeholder
fix(desktop): the composer hint stops acting like text you typed
2026-08-01 20:14:06 -05:00
ethernet 3bed7d4ae7 fix(desktop,install): keep bundled Node ahead of system Node on Windows
Two paths let a pre-existing system Node win over the Hermes-managed one.

The desktop backend spawn built its managed-Node PATH entry as
`<home>/node/bin` only. That is the POSIX layout install.sh produces;
install.ps1 unpacks portable Node straight into `%LOCALAPPDATA%\hermes\node`
with node.exe at the root and no `bin\`. On Windows the entry therefore
pointed at a directory that does not exist, and the backend fell through to
whatever Node was already on PATH.

main.ts already had the correct platform-ordered list, behind a "keep this
in sync with iter_hermes_node_dirs()" comment on a second copy of the rule.
The two copies had drifted. Export the ordering from backend-env.ts and have
main.ts consume it so there is one source of truth on the Node side (the
Electron main process cannot import hermes_constants.py, so a mirror is
unavoidable — but one mirror, not two).

install.ps1 appended the node dir to the persisted User PATH instead of
prepending it. The session PATH was already prepended correctly, so this only
bit later processes: any shell opened after install, and a standalone
hermes-setup.exe run that inherits User PATH rather than a curated env, both
resolved a system Node ahead of the bundled one.

Not a bug, for the record: update.rs's prepend list omits the same Windows
root, but it inherits PATH from the desktop, which supplies the correct
entries — so it is redundant rather than broken, and no installer rebuild is
needed for this fix.

Tests: managed dirs lead with the platform-native layout while always
offering both shapes, empty without a home, and every managed dir outranks
the inherited PATH on darwin and win32. The three existing tests that pinned
`entries[1]` by index asserted the old single-dir shape and now assert the
relationship instead.

install.ps1 has no behavioral test here: CI has no PowerShell host, and
AGENTS.md bans source-reading tests (the neighbouring
test_install_ps1_node_path_for_npm.py predates that rule).
2026-08-01 20:09:35 -04:00
Brooklyn Nicholson 93ec02bf79 fix(desktop): keep the hint off the text during IME composition
Input events are deliberately skipped for the duration of an IME composition
(they carry uncommitted preedit text), so nothing clears the empty marker
until compositionend — the hint kept painting behind the hiragana the user was
composing. Drop the marker as composition starts; the normalizer restores it
if composition ends with nothing committed.

Taking the hint out of the text flow fixed the displacement half of #75960 on
its own — preedit now starts at the field's left edge either way — but the
overlap needed this too.

Co-authored-by: Ryuichi Natori <to-na@users.noreply.github.com>
2026-08-01 16:56:48 -05:00
brooklyn! 97971643ab
Merge pull request #76417 from NousResearch/bb/kanban-model-picker
Pick a kanban task's model and thinking depth from the board
2026-08-01 16:55:01 -05:00
Brooklyn Nicholson 414e5af114 fix(desktop): the composer hint stops acting like text you typed
The empty-composer prompt was painted with an inline `::before`, which puts a
real box in the contenteditable's text flow. Click an empty composer and the
caret lands past the hint instead of at the field's left edge, and a hint that
wraps — a narrow composer, a long locale string — makes the empty composer two
lines tall. It is a hint, so it now sits out of flow: absolutely positioned,
clipped to one line, unselectable and untouchable by the pointer. Measured in
Chromium against the built stylesheet, the caret lands at the left edge for a
wide composer, a narrow one, and a Japanese hint alike.

Backspace on a fresh `@folder:` chip had a second, related problem. Committing
a completion empties the typed token's text node rather than removing it, and
`Range.insertNode` splits the line around the caret, so the chip ends up
between zero-length text nodes. Those read as content: the atomic chip-delete
declined, Chromium's own backspace bounced between the leftovers, and the chip
took extra presses to remove — leaving a `"\n"` draft with the hint still
hidden behind it. Emptiness, the chip-delete, and the DOM normalizer now all
step over that litter.

The stylesheet owns the rule now that it needs `position: relative` on the
editor, so the utility-class constant both composers imported is gone.
2026-08-01 16:34:46 -05:00
Cad from Arca f3cb7c0e01 style(desktop): satisfy mapped-profile test lint 2026-08-01 14:30:16 -07:00
Cad from Arca 6d3cb23d24 fix(desktop): reject reserved remote profile names 2026-08-01 14:30:16 -07:00
Cad from Arca e2c6f2ebc4 test(desktop): cover mapped non-default SSH profile 2026-08-01 14:30:16 -07:00
Cad from Arca d6be88fbc8 fix(desktop): map SSH profiles to remote profiles 2026-08-01 14:30:16 -07:00
Flownium 4ebdbadef8 fix(desktop): close wake indicator with main window 2026-08-01 14:30:11 -07:00
Flownium 53b8f44e75 feat(desktop): add notch wake indicator 2026-08-01 14:30:11 -07:00
Teknium ff8df5822b fix(desktop): derive font-setting save base from the config cache, not a mirrored ref
The desktop lint rule added after the original PR bans mirroring reactive
values into refs via useEffect. Rework the setting to seed from draft-null
state, guard profile switches by stale-config identity, and derive the
save base + rollback value from the shared config record instead of
latestConfigRef/lastSavedRef/seededRef.
2026-08-01 14:30:08 -07:00
Zeraphim 261c67b2f8 fix(desktop): cover pending agent terminal font cleanup 2026-08-01 14:30:08 -07:00
Zeraphim 0399711bec feat(desktop): add terminal font picker 2026-08-01 14:30:08 -07:00
baoyu0 131aee9260 feat(desktop): add configurable terminal font family (terminal.font_family in config.yaml)
Adds a new config option terminal.font_family that lets users customize the
CSS font-family for the desktop app's embedded xterm.js terminal.

Previously the font was hardcoded in use-terminal-session.ts:
  'JetBrains Mono', 'Cascadia Code', 'SF Mono', Menlo, Consolas, monospace

Now the value from config.yaml (terminal.font_family) is threaded through:
  useHermesConfig → PersistentTerminal → TerminalTab → useTerminalSession

When font_family is empty or unset (default), the built-in fallback is used,
preserving backward compatibility. Users with Nerd Fonts installed (e.g.
CaskaydiaCoveNerdFont) can now set:

  terminal:
    font_family: 'CaskaydiaCoveNerdFont', 'JetBrains Mono', monospace

Closes: #terminal-font-config
2026-08-01 14:30:08 -07:00
Brooklyn Nicholson defee936e9 fix(desktop): keep the composer's model menu pixel- and behaviour-identical
Two regressions from moving Edit Models into the shared catalog:

The row opened its own separator group, so the composer showed two rules in
the trailing block where it had always shown one. Edit Models now renders
inside the host footer's group — same single separator, same order.

The panel also read the catalog with a non-reactive getQueryData. With no
model in the session store yet (a fresh draft), currentPickerSelection falls
back to the catalog's reported current, and a cache peek never repaints once
that resolves — the menu could sit on a stale or empty selection. Back to a
live useQuery on the same key, which React Query dedupes against the menu's
own subscription rather than double-fetching.
2026-08-01 16:25:13 -05:00
Brooklyn Nicholson d5d3086b41 fix(desktop): let the model catalog own curation, not each host
The board listed every model while the composer honoured the user's Edit
Models shortlist, because visibility arrived as a prop each caller opted into.
Curation is one stored preference, so a per-caller opt-in guarantees the two
surfaces eventually disagree about what "my models" means — the exact drift
extracting the menu was meant to end.

ModelCatalogMenu now reads $visibleModels itself and renders the Edit Models
row, so every picker shows the same shortlist and offers the same way to
change it. The composer's footer keeps only Refresh Models. No plugin surface
is involved: the menu is core and the plugin just mounts it.

Also restores the session-scoped catalog fetch the extraction dropped. The
composer's query key must carry its session id — the app invalidates the
session-scoped key on model changes, so a global key would have gone stale
mid-conversation.
2026-08-01 16:19:37 -05:00
Brooklyn Nicholson 602fc5f9f5 feat(kanban): pick a task's model and thinking depth from the board
The desktop board had no model control at all: a task ran whatever the profile
you assigned it to happened to be configured with, and the only way to point
one task elsewhere was `hermes kanban set-model` or the browser dashboard's
flat provider:model select.

Adds a Model row to New Task and to the task drawer, rendering the composer's
own picker via the SDK — same search, same provider groups, same submenu — so
the board and the chat bar cannot drift. Unset reads "Profile default" and
changes nothing; a pin reads "provider: model · High" with an inline clear.

Presets are read-only here: picking a model seeds the depth from what you last
used for it, but a per-task choice never rewrites what the composer opens at.
Fast mode is omitted rather than shown-and-ignored — it's a live-session
request parameter with no worker-spawn equivalent.

The New Task dialog opts out of DialogContent's clip: the dialog publishes
itself as the portal container for popovers opened inside it, so its
overflow-y-auto cropped the menu at the dialog's edge. The general fix is in
flight as #75600; this override is scoped to one dialog to avoid conflicting
with it and disappears when that lands.
2026-08-01 16:10:25 -05:00
Brooklyn Nicholson b35c34d58c refactor(desktop): make the composer's model picker a reusable primitive
The model menu — search, provider grouping, -fast family collapse, keyboard
selection, the per-row thinking/effort submenu — was welded to the chat
composer's session writes, so any other surface wanting a model picker had to
fork it and drift.

Splits rendering from meaning. ModelCatalogMenu owns the catalog and the
navigation; a ModelMenuController decides what a selection DOES. The composer
is now one controller over it, keeping its session scoping, sticky manual
pick, preset restore, MoA presets, and rollback-on-failed-write intact.

ModelEditSubmenu becomes pure: it reports edits instead of performing them.
It previously called setCurrentReasoningEffort and config.set inline, so any
non-composer host would have silently retargeted the user's live chat when
they picked an effort. Its default effort is passed in rather than read from a
store, which is what lets it render outside a session at all.

Exported through the SDK so plugins consume the real component instead of a
copy. The composer's existing behaviour suite passes unchanged against it.
2026-08-01 16:10:13 -05:00
ethernet f88ed6c717 fix: fix @nousresearch/ui version, update to npm 12 2026-08-01 17:05:42 -04:00
Brooklyn Nicholson 9175b05b40 fix(desktop): ship example plugin off by default
Match kanban — inventoriable in Settings ▸ Plugins, no statusbar chrome until opted in.
2026-08-01 15:46:36 -05:00
Brooklyn Nicholson a09ad04653 refactor(desktop): share the composer's floating pill treatment
The micro-action strip owned this skin inline: a full-radius hairline pill
on the composer's own fill behind a blur, sized to `--composer-control-size`.
It's the right look for anything that floats over the composer, so lift it
into `composer-dock` next to the other shared composer surfaces and have the
strip compose it with its own width cap and disabled state.
2026-08-01 15:45:42 -05:00
hermes-seaeye[bot] c5be6e7792
fmt(js): `npm run fix` on merge (#76404)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-01 20:45:40 +00:00
brooklyn! 7e271d96fc
Merge pull request #76290 from NousResearch/bb/coding-cwd-copy
Copy the worktree path from the branch bar
2026-08-01 15:36:35 -05:00
Teknium 090d146479 fix(desktop): stop pullRemotePins from reverting fresh local pin toggles
A local pin/unpin fires reconcile synchronously via the
$pinnedSessionIds listener — before any PATCH exists — so
pullRemotePins() read the still-stale server row and immediately
undid the user's action: !row.pinned && heldLocally reverted a fresh
pin, and row.pinned && !heldLocally re-pinned a fresh unpin, after
which the push pass saw nothing to write and no PATCH ever fired.

Fence local intent ahead of the pull:

- Run the push pass first, so pending/unconfirmed record the local
  intent before the page is read, then pull.
- Skip the pull for ids still in `pending` (row not yet resolved) —
  local intent awaiting its PATCH is newer than any loaded page.
- Update mirrored bookkeeping before mutating the pin store inside
  the pull, so the re-entrant reconcile doesn't echo adopted state
  back as redundant PATCHes.

Regression tests cover both directions (fresh pin over a stale
pinned=false row, fresh unpin over a stale pinned=true row) plus the
deferred-pin case where the row loads stale after the toggle. All
three fail against the previous reconcile order.

Fixes #74570
2026-08-01 10:52:21 -07:00
chelsealong 64c1db961f fix(desktop): close the same handoff race in bootstrap recovery
handOffWindowsBootstrapRecovery() writes the update marker
unconditionally, same as applyUpdates() before the previous commit.
It's reachable during boot whenever resolveHermesBackend reports
bootstrap-needed, which a relaunch mid-update can plausibly trigger
on Windows -- clobbering a live updater's marker through this second
path. Apply the same updateHandoffConflict() guard here: refuse to
spawn a second updater when one is already alive, and quit instead
so the live updater can finish and restart us.
2026-08-01 10:47:45 -07:00
chelsealong 8e06b30cd8 fix(desktop): refuse a second update hand-off while one is already live
writeUpdateMarker unconditionally overwrites HERMES_HOME/.hermes-update-in-progress
before every hand-off. If the user retries "Update" while a prior updater is
still alive and parked (e.g. waiting for the desktop to exit), the retry's
pre-write clobbers the still-running updater's claim, so the older updater is
no longer recorded as the owner even though it's actively mutating the
checkout. A second updater can then run concurrently over the same tree.

Add updateHandoffConflict() to check for a live foreign marker owner before
spawning a new updater, and refuse the hand-off (surfacing an "update already
running" message) instead of overwriting the marker.

Ref: #75778
2026-08-01 10:47:45 -07:00
Brooklyn Nicholson a71f20dd18 feat(desktop): copy the branch-bar worktree path on hover
Tiny copy glyph next to the hover-revealed cwd. Same reveal as the path,
copies the real absolute path via the existing file-actions helper.
2026-08-01 11:37:44 -05:00
brooklyn! 9b1d8341b2
Merge pull request #75988 from NousResearch/bb/cwd-focus-and-display
Session workspace tracks focus — and paints as ~/…
2026-08-01 02:53:44 -05:00
Brooklyn Nicholson 5657c4a541 fix(desktop): sort display-path import for eslint 2026-08-01 02:46:40 -05:00
Brooklyn Nicholson 47d7b7fb1d fix(desktop): keep review/files rails at their declared size
All-fixed splits used to promote the last track to flex-grow and drop its
max clamp. Review and files both declare maxWidth, so ⌘G/⌘J ballooned them
and sash overrides only set a basis that grow still expanded past.
2026-08-01 02:45:47 -05:00
Brooklyn Nicholson 883076ccd7 feat(desktop): ⌘N/⌘T keep the focused session's project
resolveNewSessionCwd now inherits the focused chat's workspace when you
aren't drilled into a sidebar project, so a new tab or draft stays in
the same repo as the chat you were looking at.
2026-08-01 02:32:28 -05:00
Brooklyn Nicholson 3707741d9f fix(desktop): statusbar cwd follows the focused session
Workspace indicator was stuck on the primary $currentCwd while timers
and context already tracked focus. Resolve from the focused runtime
slice, then the stored session row, with a mid-switch ownership gate.
Path tips across chrome use displayPath (~/).
2026-08-01 02:32:28 -05:00
Brooklyn Nicholson 5b4c57a7ff feat(desktop): shared path display collapses home to ~
One paint helper for UI chrome: /Users/x/y → ~/y (also /home and
C:\Users). Copy/reveal still use the real absolute path.
2026-08-01 02:32:28 -05:00
brooklyn! 57b1eb8c4d
Merge pull request #75975 from NousResearch/bb/terminal-session-link
Link terminal tabs to the session you're working in
2026-08-01 02:22:30 -05:00
brooklyn! bfc014e3a8
Merge pull request #75966 from NousResearch/bb/dither-tail-only
fix(desktop): thinking indicator can no longer appear mid-transcript
2026-08-01 02:18:12 -05:00
Brooklyn Nicholson c450fb931d feat(desktop): switching sessions re-selects the terminal tab in its cwd
A $currentCwd listener in the terminal store picks the user tab whose
live shell cwd (restoreCwd, falling back to launch dir) matches the
session's workspace. Selection only: no tab is created, closed, or
revealed; detached sessions and unmatched cwds leave the rail alone,
and an already-matching active tab keeps focus.
2026-08-01 02:15:02 -05:00
Brooklyn Nicholson eb545ddea9 feat(desktop): ⌘-click closes a terminal rail tab
Same gesture the pane tabs already carry — isMetaClose hoists from
pane-tab.tsx into lib/middle-click.ts beside its sibling middle-click
gesture, so the two surfaces share one predicate instead of drifting.
2026-08-01 02:15:02 -05:00
Brooklyn Nicholson ba75633334 fix(desktop): never show the thinking indicator anywhere but the thread tail
A turn that ended without its message.complete (turn crash, reconnect gap,
steer race) left its streaming bubble pending:true forever. The next user
message then landed after it, stranding a live dither indicator
mid-transcript.

Three layers:
- session.info running=false (the agent loop's finally-block signal, the
  only settle edge those paths still emit) now finalizes the streaming
  bubble via the same math as Stop.
- A fresh submit settles any leftover pending bubble before appending the
  new user message, and drops a stale streamId so the new turn seeds fresh.
- AssistantMessage renders the loading/stall indicator only on the thread's
  last message, so no upstream state bug can ever paint one mid-transcript.
2026-08-01 02:10:56 -05:00
Brooklyn Nicholson 90e6fe4f55 fix(desktop): terminal selection mirror yields to chat copy
mirrorSelection called textarea.select() whenever xterm had a scrap,
which replaced any chat highlight so ⌘C copied the wrong thing. Only
claim the document selection while the terminal is focused and nothing
outside it is highlighted.
2026-08-01 01:49:38 -05:00
Brooklyn Nicholson 1d97c035ef fix(desktop): drag-select and ⌘C work on user bubbles again
User bubbles are buttons, so the global user-select:none rule killed
text selection. Right-click-to-react and click-to-edit also ate a live
highlight. Prefer selection when one exists.
2026-08-01 01:49:38 -05:00
brooklyn! 85148f79f7
Merge pull request #75937 from NousResearch/bb/win-icon-size
fix(desktop): make the Windows app icon match native icon size
2026-08-01 01:41:24 -05:00
Brooklyn Nicholson e524310118 fix(desktop): render the ico truly full-bleed
The first regeneration kept a ~5% transparent margin around the icon
plate (94.9% coverage). Windows expects the plate itself to be the icon
edge — scale the artwork's rounded plate to span the canvas exactly.
2026-08-01 01:33:05 -05:00
brooklyn! 41e55679ee
Merge pull request #75848 from NousResearch/bb/toggle-terminal-persist
Toggle any pane wherever you put it, and keep the header hidden
2026-08-01 01:32:36 -05:00
Brooklyn Nicholson 80c86c4949 fix(desktop): make the Windows app icon match native icon size
The shipped artwork bakes in the macOS-style ~10% transparent margin
(content covered only ~80% of the canvas), so the taskbar/titlebar icon
rendered visibly smaller than neighboring Windows apps, which draw
full-bleed.

- Regenerate assets/icon.ico full-bleed (~95% coverage) from the same
  art, with the standard 16-256px frames. This feeds both the exe stamp
  (set-exe-identity via rcedit) and the installer.
- On Windows, resolve the BrowserWindow icon from the full-bleed ico
  (resources/icon.ico, shipped via extraResources) before falling back
  to the padded apple-touch PNG.

macOS is untouched: the dock icon and icon.icns keep the padded art,
which is correct there.
2026-08-01 01:26:46 -05:00
Brooklyn Nicholson feaa325033 Merge origin/main into bb/toggle-terminal-persist
main reworked the same surface while this was open, so three hunks needed
deciding rather than accepting.

Logs became summon-only (#75862): the contribution only exists while $logsOpen
is on, docked as its OWN zone beside the terminal instead of a tab in its
strip. That supersedes the static logs pane and the bindToolPaneCollapse call
here — main already registers logs' closer/opener directly, so both were
dropped in favour of its version.

main also added a ⌘K "Toggle terminal" row reading $terminalTakeover, and
gave logs back a 7.5rem minHeight under a comment claiming the terminal's
sizing rule. Both are the bugs this branch fixes, so they move onto the
shared behaviour: the palette row reads isPaneVisible/togglePaneVisible like
every other pane toggle, and logs loses the floor so the comment is true —
the sash folds its zone to the rail instead of stranding a sliver.
2026-08-01 01:25:33 -05:00
Brooklyn Nicholson 0b8a3582c6 fix(desktop): cap the changed-files card and fade its overflow
A turn that rewrote twenty files grew a twenty-row card, so the summary
that is supposed to close the turn became the thing you scroll past to
reach the composer. Cap the rows at ~5 and let the clipped edge fade,
the way every other overflow in the app reads.

The horizontal padding moves onto the scroller so a row's hover fill
still bleeds to the card's edge instead of stopping at a scroll gutter.
2026-08-01 01:24:05 -05:00
Brooklyn Nicholson 15b0b95413 refactor(desktop): one edge-faded scroller for the whole app
The kanban drawer had grown the only edge-aware masked scroller in the
tree, and the next surface that wants one would have copied it. Lift it
to components/ui as FadeScroll, export it on the plugin SDK, and leave
kanban's ScrollFade as a name its call sites already pass `max` to.

The mask math comes out as two pure functions. jsdom's CSS parser drops
any gradient containing calc(), so a rendered mask-image can't be read
back off the style attribute -- edgeMask/scrollEdges are testable for
real where the component's inline style is not.
2026-08-01 01:23:59 -05:00
Brooklyn Nicholson 73b8847d7b fix(desktop): toggle every pane off the tree, not off its own boolean
The terminal fix was only one instance. An audit of the other pane toggles
found ⌘G and ⌘J diverging the same way, proven with a probe: with review
stacked behind files in the right column, or either pane inside a minimized
zone, the store reads open while nothing is on screen, so the press
re-asserts a value it already held and the key does nothing.

isPaneVisible / togglePaneVisible replace the tool-panel-only pair and now
back every toggle. Close still routes through closeTreePane, so each pane
keeps its own semantics: a tool panel collapses to its rail, files and
review close through their store, anything else is dismissed.

files and review were bound with a closer and no opener, so the boolean went
stale as soon as anything but the toggle revealed them. bindPaneVisibility
moves into the tree store beside bindToolPaneCollapse, documents the two as a
pair, and both panes now pass both halves. Keeping the binding in the store
also means the tests drive the real function — the earlier copy in the test
file passed with the fix reverted, which is how the missing opener survived
the first pass.

setTreePaneHidden keeps its quiet path: a reactive unhide (a cwd arriving)
must not front or un-minimize over what the user is looking at. Only user
intent goes through the reveal path.
2026-08-01 01:17:45 -05:00
Brooklyn Nicholson 5826450d17 fix(desktop): declare @assistant-ui/core as a direct dependency
Twenty-odd desktop files import `@assistant-ui/core` directly, but it was
never in `dependencies` — it resolved only because react 0.15 pulled core
0.3.2 and npm hoisted it to the workspace root. react 0.14.24 wants core
^0.2.19, which nests under `react/node_modules`, so the undeclared imports
stop resolving and the build fails on `@assistant-ui/core/internal`.

Declare it so resolution doesn't depend on hoisting luck either way.
2026-08-01 01:03:53 -05:00
Brooklyn Nicholson 41e4b96233 fix(desktop): restore reactive message and composer runtime hooks
The 0.15 migration swapped the `useMessageRuntime()` hook for a plain
`useAui().message` accessor read. The hook subscribes and re-renders on
change; the accessor does not, so message components paint once and then
go stale until the window is reloaded. `useComposerRuntime().subscribe`
was widened to `aui.subscribe` the same way.

Restores the 0.14 call form, including the test mock whose added
`getState` stub let the API break through CI unnoticed.
2026-08-01 00:58:32 -05:00
Brooklyn Nicholson 031b0a6786 fix(desktop): pin @assistant-ui/react back to 0.14.24
The 0.15 bump rode along with the npm audit pass in #75037, but it was
not required by any advisory — `npm audit` reports 0 vulnerabilities with
0.14.24 pinned. It is a breaking major that costs us a working transcript,
so take the API stability instead.
2026-08-01 00:58:32 -05:00
brooklyn! c74f4c5335
Merge pull request #75890 from NousResearch/bb/disk-full-toast
Toast when a send fails because the disk is full
2026-08-01 00:36:21 -05:00
Kewe63 16e66e721f fix(desktop): break renderer-led reinstall loop on transient backend stalls
Issue #74874. The renderer's 'Repair' button treated every transient
backend GIL stall (event loop stalled ... ws ready frame send failed)
as a fatal backend fault, asking the bootstrap to force-reinstall +
restart, which then stalled again for the same reason — looping the
user through 30+ minutes of reinstall cycles.

Distinguish 'venv is genuinely broken' from 'backend is just transiently
stalled' before honouring a repair request. Probe the live backend
process (exitCode === null && signalCode === null) and an in-flight
repair-attempt counter:

  attempt <= 3 AND primary alive   → soft restart (skip installer)
  attempt <= 3 AND primary dead    → soft restart (verify before reinstall)
  attempt >  3                    → hard reinstall (escalate)

Counter resets on a clean backend.ready so a later, unrelated failure
episode starts at attempt 1. The guard is a pure helper (decideBootstrap
Repair in electron/bootstrap-repair-guard.ts) so the decision logic is
unit-tested in isolation; main.ts only wires the existing flag and
counters to it.

Refs #74874
2026-07-31 22:33:53 -07:00
zakhounet 0ee9723b52 fix(desktop): make the non-Windows updater bypass an explicit policy
resolveUpdaterBinary() picked up a staged hermes-setup on every platform, so a
macOS binary predating the update hand-off protocol took over the update, held
the marker, and had its `hermes update` child refuse its own parent. The in-app
Update button then failed for good, with no route -- update, re-download or
reinstall -- back to a capable binary (#74836).

Move the decision into a pure resolveStagedUpdaterBinary() helper in
updater-process.ts and return null off Windows. The installer self-copies into
HERMES_HOME on every platform (paths::installer_dest,
bootstrap::copy_self_to_hermes_home), so finding that binary on macOS or Linux
is expected rather than leftover junk: declining to hand it an update is a
policy decision, and the comments now say so instead of describing the binary
as Windows-specific.

Cover the resolver in updater-process.test.ts: Windows accepts a staged
hermes-setup.exe, macOS/Linux return null even when hermes-setup exists, and
Windows returns null when nothing is staged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 22:33:53 -07:00
chenbin d649973751 fix(desktop): skip hermes-setup binary on macOS/Linux
resolveUpdaterBinary() returned a path on macOS if hermes-setup existed
in HERMES_HOME, routing macOS into the Windows-style quit→hand-off→rebuild
update dance. A stale hermes-setup (e.g. from 2026-06-08, predating the
applyUpdatesPosixInApp path) permanently breaks the in-app Update button.

The Tauri hermes-setup binary is a Windows-specific mechanism; macOS and
Linux use applyUpdatesPosixInApp instead. Always return null on non-Windows
so the native drag-and-drop updater is used on those platforms.

Fixes #74836
2026-07-31 22:33:53 -07:00
teknium1 087b2230c4 fix(desktop): accept scheme-less host:port in the remote gateway URL field
Users pasting a Tailscale IP or LAN host as 'host:port' (no http://) hit
either a hard 'URL is not valid' error in the main process or, worse, a
silent dead probe in the renderer: the ^https?:// gates in the settings
and first-run forms never fired, so the field sat idle with no feedback.

- normalizeRemoteBaseUrl() (electron/connection-config.ts) now prepends
  http:// when the input has no scheme:// prefix; explicit non-http
  schemes (ws://, ftp://) still reach the protocol check and get a clear
  rejection.
- New renderer twin coerceRemoteUrlScheme() (src/lib/remote-url.ts),
  wired into both probe gates (gateway-settings.tsx and
  first-run-remote-form.tsx) so the debounced /api/status probe, sign-in,
  test, and save all see the coerced URL.
- Tests for both sides (electron/connection-config.test.ts,
  src/lib/remote-url.test.ts).
2026-07-31 22:28:43 -07:00
Brooklyn Nicholson 69902c203e fix(desktop): toast when a send fails because the disk is full
Map ENOSPC / SQLITE_FULL / "disk full" error strings through notifyError to a
clear free-space toast, and fire it from rejected prompt.submit, gateway error
events, and terminal failure frames so a full disk never looks like silence.
2026-08-01 00:27:17 -05:00
brooklyn! 0454b370d1
Merge pull request #75862 from NousResearch/bb/cmdk-on-off
fix(desktop): put Toggle terminal on the ⌘K on/off pattern
2026-08-01 00:21:11 -05:00
brooklyn! e25aa3a338
Merge pull request #75873 from NousResearch/bb/logs-cmdk-only
Make the logs pane cmd+K-only — never auto-opened, never a standing tab
2026-08-01 00:18:59 -05:00
Brooklyn Nicholson 75fb139977 fix(desktop): make the logs pane summon-only via the command palette
The logs pane no longer exists as standing chrome: it isn't registered in
any default layout or preset, never rides the terminal strip as a secondary
tab, and never opens automatically. The contribution is registered only
while the palette's "Toggle logs" command has it summoned, and closing it
(toggle, tab ✕, ⌘W) removes it from the registry and the tree entirely —
including sweeping it out of persisted layouts from before it was
summon-only. The open state is session-only, so a fresh boot always starts
without it.
2026-08-01 00:09:43 -05:00
Brooklyn Nicholson 6f756272f8 fix(desktop): stop the agent terminal falling back to a white background
useAgentTerminal hardcoded resolveSurfaceColor('#ffffff'), so any read that
misses the surface token — a pre-paint mount — flashed a white slab in dark
mode. The user terminal already passes the palette's own background; the
agent mirror is a copy of it that lost that detail.

Pass the same fallback, so both siblings degrade to their mode's surface.
2026-08-01 00:06:48 -05:00
Brooklyn Nicholson 1a2eee0d63 feat(desktop): paint the integrated terminal with the transcript background
The terminal borrowed --ui-editor-surface-background, the same token the
preview pane, diff gutters, and pane tabs wear. Nothing could retint the
terminal without moving all of them, and it was pinned to the editor surface
rather than the conversation it sits beside.

Give it --ui-terminal-surface-background, aliased to the chat surface, and
point every layer that paints the terminal at it: the pane wrapper, the
persistent overlay, the rail, the instance chrome, the xterm host, and the
probe that feeds xterm's canvas. One knob, and a skin can now move the
terminal alone.

No visual change today — both tokens resolve to --ui-bg-chrome.

The two xterm host divs carried an identical class string; hoist it to
HOST_CLASS so the user terminal and the agent mirror can't drift again.
2026-08-01 00:06:42 -05:00
Brooklyn Nicholson 3572424402 fix(desktop): put Toggle terminal on the ⌘K on/off pattern
The terminal row still lived under Go to as a one-way open. It never showed
live state and couldn't hide. Move it through paletteToggle next to logs,
yolo, status bar, and layout edit so every binary ⌘K toggle shares the same
underlined on/off note.
2026-07-31 23:59:36 -05:00
Brooklyn Nicholson 16431b8ab2 test(desktop): cover the tool-panel toggle and hidden-header regressions 2026-07-31 23:49:28 -05:00
Brooklyn Nicholson aca571ef49 feat(desktop): let a tool panel shrink to its collapsed header
The terminal and logs carried a 7.5rem minHeight, so dragging the seam down
jammed against a floor with a sliver of unusable terminal still showing.

Tool-panel zones now floor at COLLAPSED_ZONE_PX (the h-7 header strip)
instead of the generic 80px, and releasing at that floor minimizes the zone
rather than persisting the sliver — it folds to its collapsed header,
vertical rail or horizontal strip depending on the parent axis. The sliver
size is never written, so restoring returns the height it had before.
2026-07-31 23:49:28 -05:00
Brooklyn Nicholson 4802d2d88d fix(desktop): keep a hidden zone header hidden
Two independent clobbers threw the choice away, so "hide header" never
survived a close/reopen cycle.

normalize() deleted headerHidden whenever a zone dropped to one pane, on
the grounds that a lone zone is headerless by default. It is — but the flag
is the user's standing preference for that zone, not a redundant value, and
dropping it meant the bar returned the moment a pane rejoined (close the
stacked logs pane, toggle it back).

Re-adoption then pinned headerHidden false unconditionally. That is right
for a pane arriving somewhere new, where zero chrome leaves no handle to
drag or close, and wrong for a zone whose bar the user deliberately hid.
Adoption now carries the destination zone's own setting, read before the
insert since insertAtGroup pins the flag itself on a center drop.
2026-07-31 23:49:11 -05:00
Brooklyn Nicholson de46fb0365 fix(desktop): make the terminal toggle work wherever the terminal sits
Dragging the terminal to the bottom stacks logs into its zone, and every
tool-panel toggle broke in that stack.

Boot revealed instead of leaving the tree alone: bindPaneCollapse ran
setPaneCollapsed(id, !open), and `false` there fronts the pane. Logs binds
last, so it stole the active tab from the persisted tree. Ctrl-` then asked
to collapse a terminal that was no longer the active tab, the shared-zone
branch declined by design, and the key did nothing until the stack was
broken up. Boot now only ever collapses.

The toggles also asked the wrong question. !$terminalTakeover.get() flips a
boolean that has no idea what is on screen, so once anything else moved the
pane the press spent itself re-asserting a value the store already held.
toggleToolPane derives from the tree instead, and ctrl-`, Cmd+J's terminal
fallback, the statusbar button and the logs palette row all route through
it. The terminal cycle/close keys drop the same stale boolean.

bindPaneCollapse moves into the tree store as bindToolPaneCollapse so the
boot rule is testable against the real function rather than a copy.
2026-07-31 23:48:56 -05:00
brooklyn! e08870f62c
Merge pull request #75831 from NousResearch/bb/coding-row-hitbox
Composer coding strip opens the diff only from the branch and the counts
2026-07-31 23:43:35 -05:00
brooklyn! 4579838fd3
Merge pull request #75836 from NousResearch/bb/session-reap-notify
A reclaimed session tells the client instead of vanishing
2026-07-31 23:11:32 -05:00
Brooklyn Nicholson 54ddc96b69 fix(desktop): keep the coding strip's layout byte-identical
The hit targets are display:contents buttons now, so the branch label and the
counts stay the same flex children of the row with the same classes; the glyph
button fills the existing 3.5 leading slot. Only the hover background is gone.
2026-07-31 23:09:23 -05:00
Brooklyn Nicholson 9824e9585c fix(desktop): drop cached state for a reclaimed session
Evict the runtime the backend just reclaimed instead of waiting for a
resume to 404, and refresh the lists whose ended_at moved. The stored
row is untouched, so reopening resumes from the DB.
2026-07-31 23:04:45 -05:00
Brooklyn Nicholson 6b3d8cf2f1 fix(desktop): open the review pane from the branch and diff counts only
The composer's coding strip made the whole bar a button, so a click anywhere
along it — including the dead space between the branch and the counts — opened
the review pane. Only the two things that name the diff are clickable now: the
branch glyph + label, and the ahead/behind + ±lines cluster. The strip itself
is inert and no longer paints a hover state.
2026-07-31 23:00:57 -05:00
Brooklyn Nicholson 696c2e8265 feat(desktop): open Plugins from the command palette
Settings Plugins was missing from cmd+K Settings group. Add the deep-link
row next to the other non-config tabs so typing plugins lands on the page.
2026-07-31 22:49:07 -05:00
brooklyn! 4b60979dc1
Merge pull request #75583 from NousResearch/bb/hide-kanban-worker-sessions
Kanban worker runs stop appearing as chats in the sidebar
2026-07-31 14:09:01 -05:00
hermes-seaeye[bot] 17e5f7244a
fmt(js): `npm run fix` on merge (#75582)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-31 18:54:27 +00:00
Brooklyn Nicholson e41d2029b7 fix(sessions): keep kanban worker runs out of the session lists
Exclude the source from the desktop sidebar and project tree, the TUI
resume picker, session_search, and the CLI session listings.
2026-07-31 13:53:04 -05:00
brooklyn! 890e3c34c4
Merge pull request #75563 from NousResearch/bb/logs-tab-close
Close the terminal and logs tabs like any other tab
2026-07-31 13:44:49 -05:00
Brooklyn Nicholson 2286ee58c5 fix(desktop): drop imports left unused by the shared close routing 2026-07-31 13:36:53 -05:00
Brooklyn Nicholson c25e4fe687 fix(desktop): close the terminal and logs tabs like any other tab
Two things made a tool panel tab feel unclosable.

Cmd-W was a dead key over the terminal and the logs pane. The keyboard
close ladder resolved its target with focusedSessionGroup, which only
matches zones hosting a CHAT strip, so a focused tool panel fell through
every rung and Cmd-W emptied the main tab instead. Add a tool rung that
resolves through the same hover/focus ladder the number keys use.

Right-click Close was missing or inert. The zone menu's target was only
resolved by the tab strip's own onContextMenu, so a right-click anywhere
else in the zone (pane body, collapsed rail, edit veil) reused the
PREVIOUS target -- landing on the uncloseable workspace dropped Close
from the menu entirely. Resolve the target on the zone instead, so every
surface that opens the menu names the chip under the pointer.

Close on a tool panel now takes the tab out of the strip and syncs its
owning store, so the ctrl-backtick toggle and the Cmd-K row stay
truthful and bring the pane back; the toggle's open path reveals
(un-dismiss + re-adopt) rather than un-collapsing a pane that has left
the tree.
2026-07-31 13:21:57 -05:00
Brooklyn Nicholson cfae306ab6 Revert "fix(desktop): tool panel tab ✕ can close and re-open"
This reverts commit dd48d9a816.
2026-07-31 13:07:51 -05:00
ethernet 6ecd335aa8
Merge pull request #75037 from NousResearch/sec-fixes
fix(sec): patch vulnerable deps + add publication-age floors and npm script allow-list

Co-authored-by: Kingsley Wong <7207924+datanerdie@users.noreply.github.com>
Co-authored-by: viky <vikyw89@gmail.com>
Co-authored-by: FT_IOxCS <237263164+ft-ioxcs@users.noreply.github.com>
Co-authored-by: 方明元 <fmy3@qq.com>
Co-authored-by: Yorkstone Supplies <58149681+sycamoregroupltd@users.noreply.github.com>
Co-authored-by: Steven Cuz Leath <Steven.Leath@gmail.com>
Co-authored-by: Kyle French <248366920+Dadmin88@users.noreply.github.com>
Co-authored-by: Eugeniusz Gilewski <egilewski@egilewski.com>
Co-authored-by: Christopher Gara <79837758+christopherrobin88@users.noreply.github.com>
Co-authored-by: LironTTG <147833337+LironTTG@users.noreply.github.com>
Co-authored-by: Austin Porada <bbasketballer75@gmail.com>
Co-authored-by: cresslank <9219265+cresslank@users.noreply.github.com>
Co-authored-by: Ion Mudreac <mudreac@gmail.com>
Co-authored-by: martinramos002 <262243228+martinramos002-bot@users.noreply.github.com>
Co-authored-by: Sensie-Agents <agents@joinsensie.com>
Co-authored-by: alexwill87 <173086651+alexwill87@users.noreply.github.com>
Co-authored-by: BullishMomentum56 <218643122+BullishMomentum56@users.noreply.github.com>
Co-authored-by: pintadoai <240097310+pintadoai@users.noreply.github.com>
Co-authored-by: Alfred Sahlberg <dinmail@gmail.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: Richard Ham <richard.ham@live.com>
Co-authored-by: jrcrittenden <jrcrittenden@gmail.com>
Co-authored-by: 峯岸 亮 <1920071390@campus.ouj.ac.jp>
Co-authored-by: Marcus Martini <6473852+napoleonmm83@users.noreply.github.com>
2026-07-31 14:07:32 -04:00
brooklyn! 0324849fe4
Merge pull request #61173 from NousResearch/bb/desktop-kanban
feat(desktop): Kanban — the founding plugin on the desktop SDK
2026-07-31 13:00:10 -05:00
Brooklyn Nicholson dd48d9a816 fix(desktop): tool panel tab ✕ can close and re-open
The logs (and terminal) tab ✕ dismissed the pane from the layout but
never synced the owning store — so the ⌘K toggle was stale and its open
listener called setPaneCollapsed, a no-op when the pane isn't in the
tree. The tab was gone with no way back short of a layout reset.

Route the tab ✕ through closeCollapsePane (dismiss + store sync) so the
toggle stays truthful, and make bindPaneCollapse's open listener call
revealTreePane (un-dismiss + re-adopt) instead of setPaneCollapsed.
2026-07-31 12:54:41 -05:00
ethernet 9c85d68bcc fix(js): update vite 2026-07-31 13:42:03 -04:00
ethernet fabc2d7d33 fix(js): hoist eslint shared devDeps to workspace root 2026-07-31 13:42:03 -04:00