Commit graph

18582 commits

Author SHA1 Message Date
Brooklyn Nicholson
dc50ddbbd6 style(desktop): hold the delegation card to three-quarter width
A list of short rows — goal, model, timer — reads better narrow than stretched across the full reading column. Scoped in styles.css rather than as a utility: the rule above it sets width on every tool block and wins over one.
2026-07-28 03:15:20 -05:00
Brooklyn Nicholson
2818c9e514 feat(desktop): render delegate_task as its live subagent list
A fan-out showed up in the transcript as one grey row and a JSON blob, so the several agents it started were invisible until they finished. Give the call its own card: one line per child with the goal, the model running it, and a live timer, over a single ticking line of that child's relayed activity.
2026-07-28 03:05:14 -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!
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!
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!
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
Teknium
bc997a36a8 feat(stt): default global stt.language to 'en'
Whisper auto-detection frequently misidentifies short/accented clips,
which users experience as voice notes transcribed in the wrong language
(Teknium + CTO both hit this). The unified resolver from #73067 made a
global hint possible; this makes it the DEFAULT so stock installs stop
guessing. Non-English users set stt.language once; '' restores
auto-detect for multilingual use.

Deep-merge gives existing configs the new default automatically (no
_config_version bump needed); any explicit per-provider or global
language setting still wins.
2026-07-27 21:26:20 -07:00
Brooklyn Nicholson
90797eb224 fix(desktop): chip a bare @path into the ref it means
Tab-descending into a folder from the @ popover re-types the token as a
bare `@apps/desktop/` so the next complete.path lists its children. That
is right while typing, but a bare token is not a reference —
REFERENCE_PATTERN only matches @kind:value — so a draft sent with one
rendered as plain text and attached nothing at all.

Promote it to @file:/@folder: on the way out, the same shape url-refs.ts
uses for bare links: on the committing space, on paste, and on submit for
a path that never got its space. A '/' is required so a handle like
@teknium1 is never mistaken for a path.
2026-07-27 23:13:32 -05:00
hermes-seaeye[bot]
3af7b867fd
fmt(js): npm run fix on merge (#73107)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 03:59:15 +00:00
Brooklyn Nicholson
73f8ddbb8b fix(desktop): stop hoisting an @ ref that is already inline in the prose
displayContentForMessage re-derived every ref from the attached context
block and re-emitted it as a detached list above the text. Now that the
token survives expansion, that list duplicated the inline chip. Hoist
only the refs the prose is missing, so turns persisted by an older
backend still render their chips.
2026-07-27 22:55:24 -05:00
Brooklyn Nicholson
045811f5be fix(context): keep @ref tokens inline instead of stripping them from the prompt
Expanding an @file:/@folder: reference deleted the token from the message
it was typed in, leaving a hole in the sentence and no anchor for clients
to chip. The attached context block still names each ref, so nothing is
lost by leaving the token where the user put it.
2026-07-27 22:55:18 -05:00
brooklyn!
56aacbdb01
Merge pull request #73101 from NousResearch/bb/queue-double-send
feat(desktop): double-Enter sends the queued turn
2026-07-27 22:50:51 -05:00
Brooklyn Nicholson
ac8310bcc2 test(desktop): cover the busy empty-Enter double-send 2026-07-27 22:43:42 -05:00
hermes-seaeye[bot]
9f4a6fdf4a
fmt(js): npm run fix on merge (#73096)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 03:43:07 +00:00
Brooklyn Nicholson
84d77b608c feat(desktop): double-Enter sends the queued turn, and the row says Enter
Empty Enter while busy was a hard no-op, so a queued turn could only be
sent early via the panel's send arrow or Cmd+Shift+K. With prompts
queued, a second Enter now promotes the head and interrupts, mirroring
the idle empty-Enter drain. Nothing queued keeps the old no-op.

The panel's send-now row action switches from a bare up-arrow to the
return glyph, so the row states the keybind the double-send uses.
2026-07-27 22:42:21 -05:00
brooklyn!
ef26701134
Merge pull request #73089 from NousResearch/bb/desktop-sidebar-counts
Drop the remaining session counts from the sidebar
2026-07-27 22:33:09 -05:00
Teknium
fae29c8410 fix(tts): class-level .ogg container repair + multi-platform opus voice detection
Root-cause fix for the 'TTS voice bubble broken' issue family (#57048,
#54589, #57213, #58845, #14841, #45557, #57049). Two class-level defects:

1. Several backends silently write MP3/WAV bytes into a .ogg output path
   (Edge only emits MP3, Piper writes WAV, xAI writes MP3, some
   OpenAI-compatible servers ignore response_format=opus). Platforms that
   need real Ogg/Opus render 0-second/broken voice bubbles. Instead of
   per-provider patches, text_to_speech_tool now sniffs magic bytes once
   after synthesis (_sniff_audio_container) and repairs the container
   centrally (_repair_ogg_container): ffmpeg transcode in place, or rename
   to the honest extension when ffmpeg is unavailable. Covers every
   current and future provider, including command providers and plugins.

2. want_opus only recognized Telegram, so Matrix/Feishu/WhatsApp/Signal
   auto-TTS voice replies were synthesized as MP3 and delivered as broken
   attachments. New OPUS_VOICE_PLATFORMS set covers all voice-bubble
   platforms.

_convert_to_opus refactored onto a shared _ffmpeg_transcode_to_opus that
supports safe in-place transcodes (-f ogg forced muxer, temp file +
os.replace).

Tests: tests/tools/test_tts_container_repair.py (13 tests incl. a live
ffmpeg round-trip); full TTS suite green (153 + 187 passed).
2026-07-27 20:28:35 -07:00
Teknium
a10bd49ddd feat(stt): unify language resolution across all STT providers
Class-level fix for the 'STT transcribes the wrong language' issue family
(#55551, #50181 and siblings). Previously language handling was per-provider
chaos: local honoured stt.local.language, Groq/OpenAI/Mistral/DeepInfra sent
no language hint at all, xAI silently forced 'en', ElevenLabs used its own
language_code key, and there was no global setting.

- New _resolve_stt_language() helper: stt.<provider>.language >
  stt.language (new global key) > HERMES_LOCAL_STT_LANGUAGE > auto-detect.
- Threaded through ALL providers: local, local_command, groq, openai,
  mistral, xai, elevenlabs, deepinfra (shared OpenAI handler), command
  providers, and plugin dispatch.
- xAI no longer forces English when nothing is configured (auto-detect).
- Mistral Voxtral now receives a language hint when configured.
- stt.groq.model is now honoured from config (previously env-only).
- DEFAULT_CONFIG gains stt.language, stt.groq, stt.xai, stt.mistral.language.
- Tests: tests/tools/test_stt_language_resolution.py (11 tests, sabotage-
  verified) + full transcription suite green (236 passed).

Builds on cherry-picked contributor work from #19786 (@zombopanda),
#23161 (@materemias), #50684 (@BlackishGreen33).
2026-07-27 20:28:31 -07:00
墨綠BG
7e75752516 test(stt): isolate null config from language env 2026-07-27 20:28:31 -07:00