Commit graph

18596 commits

Author SHA1 Message Date
brooklyn!
e4564586bc
Merge pull request #73218 from NousResearch/bb/focused-zone-tabs
fix(desktop): make the tab verbs follow the focused zone like ⌘1-9
2026-07-28 03:14:03 -05:00
brooklyn!
fd39696ccf
Merge pull request #73216 from NousResearch/bb/desktop-thin-scrollbars
fix(desktop): thin chrome scrollbars without platform chunk
2026-07-28 03:08:06 -05:00
Brooklyn Nicholson
8fcad214ae chore: kick CI 2026-07-28 03:05:46 -05:00
brooklyn!
63841210d5
Merge pull request #73121 from NousResearch/bb/desktop-dev-cdp-port
feat(desktop): let the agent inspect the desktop app it's developing
2026-07-28 03:03:26 -05:00
Brooklyn Nicholson
ed67b20888 fix(desktop): make the tab verbs follow the focused zone like ⌘1-9
⌘1…⌘9 resolves its tab strip through $activeTreeGroup (the interacted
zone), but ⌘W, ⌘T, ⌘⇧T and the strip's "+" all hardcoded the workspace
pane's group. In a layout with a second chat zone the number keys worked
and every other tab verb acted on main instead — and that zone's strip
had no "+" at all.

Adds focusedSessionGroup() to the tree store, resolving the same zone
$activeTreeGroup names when it hosts a chat strip and falling back to
the workspace otherwise, so focus parked in files/terminal can't make ⌘W
close the file tree. ⌘W closes through it, unanchored openSessionTile()
(⌘T / ⌘⇧T) docks into it, and the "+" renders on any chat strip, noting
its zone on pointerdown so the tab lands where it was clicked.
2026-07-28 03:00:13 -05:00
brooklyn!
1cd8f8c37e
Merge pull request #73180 from NousResearch/bb/composer-popout-tabs
Composer pop-out is scoped to its layout zone
2026-07-28 02:58:33 -05:00
Brooklyn Nicholson
ef8f2e701c fix(desktop): thin chrome scrollbars without platform chunk
Chromium 121+ prefers scrollbar-width over ::-webkit-scrollbar and
ignores the latter when both are set, so our themed thumb never painted
and mac got the chunky platform "thin" bar. Gate standard scrollbar-*
for non-webkit engines only; ship a 4px webkit thumb on .scrollbar-dt
and portal menus.
2026-07-28 02:57:16 -05:00
Brooklyn Nicholson
88b6a7a49c fix(desktop): measure dock proximity against the chat surface, not the window
The dock target is the docked composer at the bottom-center of its own
surface. In a split, the viewport's bottom-center is somewhere else, so
dragging onto the real dock never registered.
2026-07-28 02:50:06 -05:00
Brooklyn Nicholson
df25d77131 fix(desktop): scope composer pop-out to its layout zone
Pop-out was one flag and one position for the whole window, and every
keep-alive-mounted tab re-clamped that position against its own rect and
wrote it back. So floating the composer in one pane floated it in every
pane, and N surfaces raced for one value with the last writer winning — a
drag in one tab was lost in the next.

State is now keyed by layout group, the scope users actually mean: tabs in
a zone share a float, a split zone beside them keeps its own. Within a
zone the stored position is intent, and each surface derives what it
renders through the pure clampPopoutPosition against its own rect.

Re-placing is gated on pane visibility so a live drag can't force a reflow
in every background tab, and runs pre-paint so a revealed tab never shows
a stale frame. Storage is written on release rather than per drag frame,
dead zones are pruned against the live tree, and the pre-zone value seeds
one first read so an existing float survives the upgrade without leaking
into zones split later.
2026-07-28 02:50:02 -05:00
Brooklyn Nicholson
25ff775136 feat(desktop): let every chat surface float its composer
popoutAllowed was hardcoded false for session tiles, so only the primary
thread could pop out — a tab or a split had no way to undock its composer.
Secondary windows stay docked; that gate was the load-bearing one.
2026-07-28 02:49:51 -05:00
Brooklyn Nicholson
73e3e0860e feat(desktop): expose a pane's layout zone to its content
PaneGroupContext, alongside PaneVisibleContext: the zone hands each pane
the group id it's rendered in, so state that belongs to a stack of tabs
rather than to a pane can key off it — and follows a pane dragged between
zones, since the provider is whichever zone renders it.
2026-07-28 02:49:46 -05:00
brooklyn!
5771a6ebe6
Merge pull request #73200 from NousResearch/bb/tip-after-model-pick
fix(desktop): stop the model-pill tooltip popping open after a mouse pick
2026-07-28 02:41:16 -05:00
hermes-seaeye[bot]
9a5f102d0e
fmt(js): npm run fix on merge (#73197)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 07:27:44 +00:00
Brooklyn Nicholson
914c3f46c6 fix(desktop): stop the model-pill tooltip popping open after a mouse pick
Picking a model closed the menu and then flashed the pill's tooltip over
the fresh selection. The existing focus guard gated on `:focus-visible`
alone, which does not separate a mouse pick from a Tab: the dropdown
autofocuses its search field and keyboard-navigates its rows, so Chromium
is already in keyboard modality when the menu restores focus to the
trigger, and the guard never fired.

Track the device behind the last real interaction and use it to qualify
`:focus-visible`. A mouse pick reports `pointer` and stays silent; Tab
focus still opens the tip.
2026-07-28 02:27:11 -05:00
brooklyn!
a9c9467dd8
Merge pull request #73178 from NousResearch/bb/dupe-message-id
fix(desktop): stop a duplicate message id from crashing the workspace pane
2026-07-28 02:19:12 -05:00
Brooklyn Nicholson
14790234ff fix(desktop): stop a duplicate message id from crashing the workspace pane
A repeated id in the transcript reached assistant-ui's MessageRepository,
which throws on the second link and takes the whole workspace pane down to
the contribution error boundary — the pane the user is actually working in.

Dedupe where the repository export is built, so no upstream transcript bug
can crash the pane, and close the journal merge path that produced one: a
resume that replays a still-journaled turn appended rows the base already
held by id.
2026-07-28 02:03:05 -05:00
brooklyn!
4da7b9ee02
Merge pull request #73169 from NousResearch/bb/queue-drain-semantics
fix(gateway): a queue drain never becomes a live-turn correction
2026-07-28 01:58:31 -05:00
brooklyn!
48b21acb90
Merge pull request #73164 from NousResearch/bb/pins-out-of-lists
fix(desktop): keep pinned sessions out of the unpinned sidebar lists
2026-07-28 01:54:08 -05:00
Brooklyn Nicholson
ab68c5efec fix(gateway): a queue drain never becomes a live-turn correction
The desktop's queue promises "run this AFTER the current turn", but the
promise broke on a race the user can't see: a drain that fired when the
client observed idle while the server was still unwinding the turn landed
in _handle_busy_submit, which applied busy_input_mode — redirecting or
interrupting the live turn with text the user explicitly queued. That's
why force-sending the queue felt like a dice roll: the same gesture
steered, interrupted, or queued depending on a millisecond settle race.

prompt.submit now carries queued:true on every fromQueue drain (composer
auto-drain, send-now, background drain), and the gateway's busy path
honors it by forcing queue semantics — never steer, never redirect,
never interrupt. Lose the race and the text simply waits its turn, which
is what queueing meant all along.

Covered on both sides: a gateway test proving a queued drain cannot touch
redirect/steer/interrupt on the live agent, and the desktop drain tests
now assert the flag rides every prompt.submit shape (direct, background,
resume-retry).
2026-07-28 01:48:08 -05:00
hermes-seaeye[bot]
f2a4452c8a
fmt(js): npm run fix on merge (#73165)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 06:47:12 +00:00
Brooklyn Nicholson
c3a3814885 fix(desktop): keep a worktree lane whose only sessions are pinned
Filtering pins out of the project tree was deleting the lane along with
its last row, so pinning your only chat on a branch made the worktree
vanish from the sidebar — and a project whose sessions were all pinned
fell through to the "no sessions" empty state.

A lane is structure: it exists on disk and you can still start work in
it, which is why the git-worktree enhancer injects lanes that never had
a session at all. Keep emptied lanes, count a lane (not a row) as
project content, and teach the live overlay's prune to drop only the
lanes IT emptied.
2026-07-28 01:45:59 -05:00
Brooklyn Nicholson
7806e6a9a8 fix(desktop): keep pinned sessions out of the unpinned sidebar lists
A pin belongs to the Pinned section and nowhere else, but only the flat
recents list filtered them — the project overview, an entered project's
lanes, and the messaging platform sections all still rendered a pinned
row a second time.

Filter every unpinned group on a shared predicate that matches the live
id AND the durable lineage-root pin id, so a compression tip rotation
can't leak the row back in. A platform's "load more" count discounts its
pinned rows too, or it promises rows that never appear.
2026-07-28 01:39:20 -05:00
brooklyn!
cef85482fc
Merge pull request #73158 from NousResearch/bb/tiny-model-switches
fix(desktop): shrink model-visibility switches to xs
2026-07-28 01:38:56 -05:00
brooklyn!
e95ce5a0e1
Merge pull request #73146 from NousResearch/bb/steer-transcript-scaffolding
fix(agent): keep interrupt-checkpoint scaffolding out of the steered transcript
2026-07-28 01:36:15 -05:00
Brooklyn Nicholson
69a4f65164 fix(desktop): use xs switch size in model-visibility dialog 2026-07-28 01:32:24 -05:00
hermes-seaeye[bot]
c80b199f36
fmt(js): npm run fix on merge (#73155)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 06:29:04 +00:00
brooklyn!
4289a934b2
Merge pull request #73147 from NousResearch/bb/session-open-in-place
desktop: open sessions where they already are
2026-07-28 01:20:53 -05:00
brooklyn!
befff02e9b
Merge pull request #73150 from NousResearch/bb/session-tab-slots
desktop: ⌘1–9 maps to visible tabs
2026-07-28 01:18:52 -05:00
brooklyn!
ff06b47ab0
Merge pull request #73143 from NousResearch/bb/floating-panes
feat(desktop): floating pane placement
2026-07-28 01:18:36 -05:00
Brooklyn Nicholson
b6c7df6cb6 fix(desktop): sort session-row imports for eslint 2026-07-28 01:13:30 -05:00
Brooklyn Nicholson
319bcedfea desktop: open sessions where they already are
Notifications, ⌘K, switcher, session picker, cron/command-center, artifacts,
and @session refs all go through openSession. Plain click/Enter focuses the
existing tile or main; ⌘-click/⌘-Enter opens a new tab; ⇧⌘ still pops a window.
2026-07-28 01:13:30 -05:00
Brooklyn Nicholson
e67e4604db desktop: shared openSession door for focus-or-route
Centralize open-in-place / tab / window so every surface can jump to an
already-open tile or main tab instead of forcing main.
2026-07-28 01:13:30 -05:00
Brooklyn Nicholson
f65fa01926 desktop: ⌘1–9 slots map to visible tabs, not raw pane indices
Focus layout keeps `files` in the workspace group's panes while chrome-
hidden. Slot keys walked that raw array, so ⌘2 landed on the first session
chip and every later number was off-by-one (especially after a ⌘W main-tab
shift). Index the same shown filter the strip paints, and cycle the same.
2026-07-28 01:12:43 -05:00
Brooklyn Nicholson
589fd23947 test(desktop): cover floating pane adoption and live drag
Adoption: a floating contribution stays out of the tree while a docked
sibling in the same pass is adopted, so the exclusion is specific rather
than a broken run.

Live DOM: mounts FloatingPanes and drives real pointer + resize events —
anchored spawn, drag, persistence across remount, edge-riding on resize,
the titlebar floor, collapse, and the no-drag button opt-out.
2026-07-28 01:11:12 -05:00
Brooklyn Nicholson
aad7f128bb feat(sdk): expose FloatingAnchor to plugins
Plugins previously had to fake a floating pane by registering into a
chrome slot and rendering position:fixed themselves, which broke inside
any transformed ancestor and reimplemented drag, clamping, and
persistence each time.
2026-07-28 01:11:12 -05:00
Brooklyn Nicholson
f78521785f feat(desktop): render floating panes above the layout tree
Adds placement: 'floating' — the one non-tiling pane role. Adoption skips
those contributions so one can never become a track that steals width
from a zone; the tree renders them as fixed cards instead, draggable by
the header, with position and collapse persisted per pane id.

The card reuses HUD_SURFACE (command palette, session switcher) rather
than hand-rolling a second float surface.
2026-07-28 01:11:05 -05:00
Brooklyn Nicholson
6dd1c6c062 feat(desktop): floating pane geometry
Pure clamping/anchoring rules for a pane that lives outside the layout
tree: keep 48px grabbable at either horizontal edge, hard-bound the top
below the titlebar so the drag handle stays reachable, pin top-left
rather than invert when the card outgrows the viewport, and let an
edge-anchored card ride its edge as the window resizes.

No DOM, so the rules are testable directly.
2026-07-28 01:10:59 -05:00
brooklyn!
c8ec2a6f3c
Merge pull request #73141 from NousResearch/bb/figma-mcp-oauth
fix(mcp): Figma remote OAuth via DCR allowlist defaults
2026-07-28 01:05:28 -05:00
Brooklyn Nicholson
c883367bd2 fix(agent): keep interrupt-checkpoint scaffolding out of the steered transcript
A mid-stream steer persists an interrupted-turn checkpoint so the model knows
its reply was cut off. That scaffolding — "[This response was interrupted by a
user correction.]" and the "Visible response before the interruption:" header —
was written straight into message content, so every reload painted the raw
machinery as an assistant bubble (and merged it into the preceding tool-call
bubble). Steered transcripts became unreadable.

Reuse the existing display/replay split instead of inventing new surface:
- Carry the scaffolded form in the server-only api_content sidecar (the exact
  bytes replayed to the provider), keep content the user's/agent's real words.
- When nothing reached the screen there is no clean form, so mark the row
  display_kind=hidden — replayed to the model, dropped by every transcript
  surface, exactly like compaction-reference rows.
- Honor display_kind=hidden in the gateway's _history_to_messages projection
  (it only sniffed the [System: convention), so the checkpoint can't leak
  through the live/resume path to the TUI/CLI either.

The model still receives the full interrupted context on the wire; the
transcript shows the partial reply and the user's correction.
2026-07-28 01:05:02 -05:00
Brooklyn Nicholson
1eb5ee1eaa fix(mcp): make Figma remote OAuth work via DCR allowlist defaults
Figma's mcp.figma.com register endpoint is a client_name allowlist
(Claude Code / Codex succeed; Hermes Agent 403s) and returns a client
secret while advertising auth_method=none, then requires the secret on
token exchange. Auto-set client_name + client_secret_post for Figma
hosts, pass oauth cfg through login/add paths, force interactive OAuth
for hermes mcp login from non-TTY desktop shells, and ship a catalog
entry. Proven: hermes mcp login figma → 26 tools.
2026-07-28 00:53:16 -05:00
brooklyn!
3c388db06b
Merge pull request #73128 from NousResearch/bb/steer-mid-stream
fix(agent): mid-stream steering survives retry/backoff; interrupt sentinel stays out of the transcript
2026-07-27 23:59:52 -05:00
Brooklyn Nicholson
d76d08360b docs(skills): add inspecting-hermes-desktop-dom
The port is only half of it. Ships the skill that tells the agent the
capability exists, when reaching for it beats reading .tsx, and how not
to hurt the user's running app while using it.

Lands in skills/software-development/ next to node-inspect-debugger,
which covers the same protocol for Node/perf work — this one is the
DOM/CSS half.

Load-bearing parts: don't relaunch or kill the user's app to get a port
(a mid-serve kill nukes Chromium's socket pool and the fallout gets
blamed on the last CSS edit); never dump the whole DOM into context;
prefer the maintained SELECTORS map to invented querySelectors; and
CDP answers factual questions only — whether it *looks* right is still
the user's call.
2026-07-27 23:58:54 -05:00
brooklyn!
5b22bd9556
Merge pull request #73105 from NousResearch/bb/tighter-turn-block-gap
Tighten the transcript's scaffolding rhythm, give diffs room
2026-07-27 23:52:49 -05:00
Brooklyn Nicholson
070093a318 feat(desktop): on by default for dev-server runs
Gating this behind an opt-in was the wrong call. A dev server already
executes arbitrary local JS — vite's module graph, every postinstall in
node_modules — so a loopback debugging port does not meaningfully widen
what a `npm run dev` session can already do, and `perf:serve` has opened
one unconditionally all along.

Requiring the variable also defeated the point: the tooling exists to be
reached for mid-task, and a capability you must remember to enable before
launching is one you don't have when you need it.

So the port opens on 9222 — the same port scripts/eval.mjs and
scripts/perf/lib/cdp.mjs already default to — for any dev-server run.
HERMES_DESKTOP_CDP_PORT stops being an on-switch and becomes an
override: a different port, or `off` to disable.

The hard gate is unchanged and still checked first: a packaged build
never opens the port, and no env value talks it into it. Neither does an
unpackaged `electron .` against dist/, which is how the packaged app
gets smoke tested.

Refusals only log when they contradict something the developer asked for
(a typo'd port, an explicit `off`). Packaged and dist runs are closed by
design and stay quiet.
2026-07-27 23:52:33 -05:00
brooklyn!
4cee9aab61
Merge pull request #73075 from NousResearch/bb/drop-leva-backdrop-controls
refactor(desktop): drop leva from the chat backdrop
2026-07-27 23:48:10 -05:00
Brooklyn Nicholson
12cd4ab423 fix(agent): steering corrections survive retry/backoff, and the interrupt sentinel stays out of the transcript
A mid-stream steer/redirect cancels only the live model request and queues
the correction for a rebuild. But the retry-wait, error-handling, and
backoff-wait paths all treated the cancellation bit as a hard stop:
clear_interrupt() destroyed the pending correction and the turn died with
"Operation interrupted…" — the user's message silently lost. All three
sites now preserve the redirect and rebuild the iteration from it, exactly
like the InterruptedError handler.

tui_gateway also gets the two suppressions its sibling surfaces already
had: the "Operation interrupted: waiting for model response (…)" sentinel
is cancellation metadata and no longer ships as assistant prose in
message.complete (gateway/run.py and ACP already suppress it), and a
leftover pending_steer returned by the turn is requeued as the next prompt
instead of dropped (cli.py and gateway/run.py already do this).

session.steer now records the correction on the inflight turn like
session.redirect does, so a resume/reconnect mid-turn rebuilds the steered
user bubble instead of losing it.
2026-07-27 23:47:57 -05:00
brooklyn!
0ae2997345
Merge pull request #73110 from NousResearch/bb/composer-at-chip
Chip @ file/folder refs instead of dropping them as plain text
2026-07-27 23:46:14 -05:00
Brooklyn Nicholson
16ef964fdb feat(desktop): rest the thinking caret at a faint hint instead of invisible
A run of thinking headers is the one place the disclosure affordance isn't
otherwise discoverable — every other one sits in a row you're already
reaching for. The resting opacity becomes a token so only that surface opts
in; DisclosureRow is shared with tool rows and run headers, which keep the
invisible-until-hover default.
2026-07-27 23:27:35 -05:00
Brooklyn Nicholson
2881243df3 fix(desktop): tighten the gap between adjacent transcript scaffolding
The turn block gap is the space between the reply and the work around it, so
a back-to-back run of thinking headers and tool rows spent the full gap
between every line and read as a stack of cards rather than one column.
Adjacent scaffolding now ticks at a third of it.

The hook is the block list the rhythm rule already enumerates, minus prose,
rather than `data-conversation-scaffold`: that marker is absent on a
multi-call tool group — most of a real run — and present on rows nested
inside one, so it both missed the lines that needed tightening and moved
ones that didn't.

An open file edit is the exception. A diff is the deliverable, read like a
PR, so it keeps the full block gap on both sides while the scaffolding
around it stays tight. A streaming turn seals blocks into separate bubbles
where the flex gap would restore the full gap, so that seal is corrected to
match — and skipped when either side carries a diff.
2026-07-27 23:27:35 -05:00
Teknium
fab4c888ae feat(voice): say 'stop' to end a voice chat hands-free
Saying EXACTLY a configured stop phrase (default: 'stop') and nothing
else now ends the voice conversation instead of being sent to the agent
as a prompt. Match is deliberately strict — whole utterance,
case-insensitive, surrounding punctuation stripped — so 'stop doing
that and try X' still reaches the agent.

- tools/voice_mode.py: is_voice_stop_phrase() + voice.stop_phrases
  config loader (default ['stop'], [] disables, malformed config falls
  back safely).
- hermes_cli/voice.py: shared continuous loop (TUI + desktop) halts on
  a stop phrase exactly like the silent-cycle limit (fires
  on_silent_limit so every UI turns voice off); stop_continuous
  force-transcribe path swallows the phrase without counting a silent
  cycle.
- cli.py: classic CLI push-to-talk/continuous path and barge-in
  utterance path disable voice mode on a stop phrase.
- Config default voice.stop_phrases: ['stop']; docs updated.

Tests: tests/tools/test_voice_stop_phrase.py (27 tests,
sabotage-verified: loop test fails when detection is disabled);
voice suites green (110 + 44 passed).
2026-07-27 21:26:23 -07:00