Commit graph

18355 commits

Author SHA1 Message Date
Brooklyn Nicholson
6ba2f3e644 fix(desktop): say a sub-second reasoning block was brief
The elapsed timer counts whole seconds, so reasoning that finished
inside one rendered as "Thought for 0s" — accurate and useless, and on a
turn with several short blocks it repeats down the transcript. Drop the
number below a second and say it was brief instead.
2026-07-27 18:10:27 -05:00
Brooklyn Nicholson
f96997c36f fix(desktop): settle a tool run whose calls never resolved
A run inferred "still working" from a missing result alone, so a call
left unresolved — by an interrupted turn, or an agent that moved on —
pinned its run as live forever. That stranded the summary in the present
tense ("Exploring 2 files" on a finished turn) and, because a live run
withholds its toggle so approvals can't hide, left the run permanently
expanded with no way to collapse it.

Qualify liveness the way ToolEntry already qualifies a row's: a missing
result only means pending while the run is the tail of a running
message. Liveness is now passed into summarizeToolRun rather than read
off the calls, since it isn't a property of the calls.
2026-07-27 18:10:27 -05:00
Brooklyn Nicholson
f7d6c1be8f docs(desktop): explain what UNBOUNDABLE_TOOLS now guards
The list gates two behaviours since the settled-run summary landed, but
the comment still justified it only in terms of the scroll window's
height cap. Record the pending-row rule that keeps approvals visible,
and why file edits stay off the list.
2026-07-27 18:10:26 -05:00
Brooklyn Nicholson
a77c75f3e0 feat(desktop): report how long the model thought
A settled reasoning block reads "Thought for 5s" instead of staying
"Thinking" forever. Nothing in the persisted turn records the duration,
so the number is frozen when the block finishes on screen and simply
omitted on a rehydrated turn, rather than reporting whatever a timer that
never ran would say.
2026-07-27 18:10:26 -05:00
Brooklyn Nicholson
7151ea4c77 feat(desktop): collapse a settled tool run to its summary line
A run of two or more tool calls now renders behind its summary once it
has finished, so a long transcript reads as what the agent did rather
than as a wall of rows. The run is keyed by its first tool call instead
of its part index: live and rehydrated turns agree on which calls belong
together but not on the indices they land at, and keying by index is what
made the previous attempt reshuffle on settle.

A run holding anything still pending always renders its rows, which is
what keeps a clarify question or an approval bar out from behind a
chevron. The approval-group tests move to tool-group and grow coverage
for both halves of that rule.
2026-07-27 18:10:26 -05:00
Brooklyn Nicholson
97d790d8b1 feat(desktop): summarize a run of tool calls as one line
Adds the grammar behind "Edited wiring.tsx, explored 3 files, ran 5
commands": one clause per category of work, a name when the category
holds a single thing and a count otherwise, and the present tense for
whichever category is still running.

The continuity test is the load-bearing part. Tool grouping was reverted
once because it reshuffled the moment a turn settled, so this replays the
same turn twice — as the gateway event stream the live view builds
bubbles from, and as the rows toChatMessages rehydrates on resume — and
asserts both produce the same runs.
2026-07-27 18:10:26 -05:00
brooklyn!
9f02bb207d
Merge pull request #72965 from NousResearch/bb/resume-freetext-search
fix(desktop): keep /resume's free-text search typeable
2026-07-27 18:09:50 -05:00
brooklyn!
fbd8d1a93d
Merge pull request #72963 from NousResearch/bb/artifacts-real-preview
Unify the preview rail onto one tab list
2026-07-27 18:04:18 -05:00
Brooklyn Nicholson
3c74f463d0 fix(desktop): keep /resume's free-text search typeable
/resume was classified as an options command, but its argument is a
free-text query the picker fuzzy-matches against session titles and
previews. Its completion list also always ends in a "Browse all
sessions…" action row, so Space-to-accept never fell through to an
empty list: the first space in a multi-word query emptied the composer
and threw the user into the overlay, query and all.

Classify it as mixed so spaces type through, matches keep narrowing as
you refine, and Tab or arrow-then-Enter still accept a highlighted
session.
2026-07-27 18:03:07 -05:00
brooklyn!
7de4fbd493
Merge pull request #72956 from NousResearch/bb/slash-freetext-enter
fix(desktop): don't let Enter swap a free-text slash argument for a completion
2026-07-27 17:56:45 -05:00
Brooklyn Nicholson
cd25a9f405 fix(desktop): open a preview without dragging the file tree open
The preview pane shares a collapsible column with the file tree, and
`revealTreePane` un-collapses a column through that column's bound store —
which on the right is `$fileBrowserOpen`, the tree's own ⌘J toggle. So
every preview open literally called `setFileBrowserOpen(true)` and the tree
came with it.

`revealPreview` now un-collapses the column directly and leaves the toggle
alone. The tree pane's visibility binding gains `$fileBrowserOpen` to match,
since its presence was tracking only the column's collapse — without that it
would still render the moment anything opened the column.
2026-07-27 17:55:46 -05:00
Brooklyn Nicholson
003ff53fb4 fix(desktop): scope composer and transcript state to their own session
`$activeSessionId` only ever holds the primary chat's session, but surfaces
that render once per transcript were reading it as if it meant "the session
on screen." A preview produced inside a session tile was recorded under the
main chat's key and surfaced in the main chat's composer, which is what
prompted this.

The tool row now records under its own `SessionView`, and the same fix
applies to the other readers of that atom that render per surface:
attachment pills and inline preview links resolve relative paths against
their session's cwd, composer voice and auto-speak read and subscribe to
their own transcript, and the thread's compaction label, prompt-wait gate
and turn timer follow the session that mounted them. `ComposerScope` now
carries a `$messages` atom rather than a read closure so both the
imperative read and the subscription come from one place.
2026-07-27 17:55:46 -05:00
Brooklyn Nicholson
96999b116b refactor(desktop): put every preview on one rail tab list
The right rail held two things at once: a list of file tabs, and a
privileged "live preview" slot with a hardcoded `preview` tab id backed by
a separate session-keyed registry. The two were written under different
session-id rules and reconciled against each other, so an `open_preview`
from a session whose stored id hadn't landed yet was set and then
immediately cleared — the pane flashed and vanished. Artifacts arrived as
a third list with their own pane and renderers.

Now everything the rail can show is a `PreviewTarget` in `$previewTabs`,
and `openPreview` is the only way in. `$previewTarget` is a computed read
of the active tab, the session registry and its reconciler are gone, and
artifacts render in the real preview pane through the shared mode switcher
and source view instead of a parallel one. Artifact tabs stay memory-only
since the registry rebuilds from the transcript.
2026-07-27 17:55:27 -05:00
Brooklyn Nicholson
43571601aa fix(desktop): don't let Enter swap a free-text slash argument for a completion
`/goal` keeps its completion popover open across arbitrary prose so its
subcommands stay reachable. The popover highlights its first row on open, and
Enter accepted that highlight unconditionally — so pressing Enter to send
`/goal ship the redesign` would replace the sentence with a row the user never
chose. Space was already guarded; Enter and Tab were not.

Enter now accepts only after the user has arrowed to a row deliberately, so
the highlight never lies about what Enter will do. Tab stays an unconditional
accept, since it means nothing else in the composer.

This is latent rather than reproducible today: `/goal` is absent from
`SUBCOMMANDS` (its `args_hint` pipes are spaced, so the extraction regex
misses them), so the backend returns no arg completions and the branch never
runs. Giving `/goal` the subcommands it already advertises would resurrect
the #71963 symptom in a worse form — losing the prose instead of chipping it.
2026-07-27 17:50:18 -05:00
Brooklyn Nicholson
e3acdfb21d refactor(desktop): lift the completion-accept decision out of the keydown ladder
Which keys accept the highlighted completion was an inline condition in the
composer's keydown god-function, untestable without a DOM harness. Move it to
a pure helper beside the other slash-query utilities.
2026-07-27 17:50:12 -05:00
Gille
b429194478
refactor(desktop): simplify free-text slash mode check (#72815) 2026-07-27 17:45:03 -05:00
brooklyn!
7c532e1006
Merge pull request #72889 from NousResearch/bb/composer-at-paths
Fix `@` path navigation, folder completion, and chip baseline in the composer
2026-07-27 17:13:00 -05:00
brooklyn!
42c308ecdc
Merge pull request #72897 from NousResearch/bb/desktop-drift-fixes
Desktop: fix diff color drift, replayed notifications, stall timing, and quit-on-active-work
2026-07-27 17:12:49 -05:00
brooklyn!
14cb0507a7
Merge pull request #72912 from NousResearch/bb/desktop-project-count
Drop the leftover session counts inside an entered project
2026-07-27 17:12:39 -05:00
Gille
dbc18c6d62
fix(desktop): preserve live model after settings save (#72903) 2026-07-27 17:52:37 -04:00
Teknium
731aa0ccc9 fix(browser): stop stale cdp_url from stalling every startup by 10+ seconds
Tool-schema assembly at CLI/Desktop startup runs the browser-family
check_fns (browser, browser_cdp, browser_dialog, browser_vision). Each
of those gates called _get_cdp_override(), which resolves the configured
endpoint over HTTP (GET /json/version, timeout=10) — so a *stale*
browser.cdp_url pointing at a dead debug browser cost ~7 serial blocking
socket connects before the banner rendered. Measured on a real Windows
install with a dead http://[::1]:9222 config: 15.1s of an 18s launch,
with no warning or error — just mystery slowness. The value is easy to
leave behind: /browser connect writes a session-scoped env override, but
'hermes config set browser.cdp_url' persists forever while the debug
Chrome it pointed at dies on the next browser restart.

Split the helper:

- _get_cdp_override_raw() — returns the configured value (env var or
  config.yaml) with zero network I/O. Used by every is-it-configured
  gate: check_browser_requirements, _browser_cdp_check, _is_local_mode,
  _is_local_backend, _navigation_session_key, _should_inject_engine
  (via _is_local_mode), and the hermes doctor chromium-skip check.
- _get_cdp_override() — unchanged contract (raw + /json/version
  resolution), now only called on paths that are about to connect:
  session creation and the dialog-supervisor attach.

This follows the existing rule in check_browser_requirements ('do not
execute agent-browser --version here') and the browser.manage status
path, which already banned _get_cdp_override for exactly this reason
(test_browser_manage_status_does_not_call_get_cdp_override): schema
assembly must not perform blocking I/O.

A/B on the same machine, same dead endpoint: get_tool_definitions()
15.08s unpatched -> 1.89s patched, with browser_cdp/browser_dialog
still advertised (gate now keys off configuration, not reachability —
matching the documented lazy-supervisor contract in
_browser_dialog_check).

Adds a regression test asserting the browser_cdp check_fn never touches
the network.
2026-07-27 14:32:05 -07:00
brooklyn!
c63be0daf7
Merge pull request #72900 from NousResearch/bb/desktop-home-project
Add a Home project at the top of the desktop sidebar
2026-07-27 16:29:57 -05:00
Brooklyn Nicholson
e04ed64637 style(desktop): drop the session counts inside an entered project
The sidebar's flat session list lost its `x/<total>` chip in #72336, but
the project drill-in kept counting: WorkspaceHeader still rendered a
SidebarCount for every repo and every branch/worktree lane. Entering a
project put a number next to each label again.

Remove the header's count slot and both call sites, along with the now
dead repo total.
2026-07-27 16:25:51 -05:00
Brooklyn Nicholson
e00901259c feat(desktop): Tab into a folder from the @ popover
Tab and Enter shared one branch, so picking a folder always committed a
chip and closed the menu — the list could show `apps/` but never open it.
Reaching a nested path meant typing every segment by hand.

Split the two intents. Tab re-types the token as a bare path so the next
completion lists that folder's children; Enter still commits the folder
itself as a chip. Files ignore the distinction — there's nowhere deeper
to go. Backspace mirrors the descent, dropping one path segment per press
instead of one character, so climbing out costs the same as going in.
2026-07-27 16:20:58 -05:00
Brooklyn Nicholson
579b66336f fix(desktop): let automated teardown quit past the active-work prompt
Playwright closes the app with a turn still in flight, so the new quit
confirmation waited on a click nobody was there to make and the E2E
worker died on a 90s teardown timeout.
2026-07-27 16:19:27 -05:00
hermes-seaeye[bot]
a88e27e9e5
fmt(js): npm run fix on merge (#72902)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-27 21:15:26 +00:00
brooklyn!
1d76a15188
Merge pull request #72899 from NousResearch/bb/desktop-recede-chrome-seams
Let the desktop chrome recede, and mark the active tab instead
2026-07-27 16:07:08 -05:00
Brooklyn Nicholson
5a5d7b9386 feat(desktop): pin Home to the top of the project sidebar
Home leads the overview above the active project and outside any drag
order, drills in to a flat chat list (it has no repo or worktree
structure), and overlays live sessions so a brand-new detached chat
appears instantly. Starting a chat from inside Home stays detached
instead of picking up the configured default project dir. Rename and
delete are hidden — there's no record behind the row.
2026-07-27 16:05:19 -05:00
Brooklyn Nicholson
ef0f4763e3 i18n(desktop): name the project-less bucket "Home"
The sidebar row is a place you enter, not a status line, so it reads as a
destination rather than "No project".
2026-07-27 16:05:10 -05:00
Brooklyn Nicholson
60b6ea237f feat(gateway): group unplaced sessions into a Home bucket in the project tree
Sessions with no cwd — or whose folder can't be promoted to a project (the
bare home dir, a deleted workspace, HERMES state) — were dropped from the
project tree entirely, so the grouped sidebar silently showed fewer chats
than flat Recents. Collect them into a synthetic `__no_project__` node at
the head of the list. It carries one lane purely to hold the rows, and is
omitted when empty so a project-less install stays blank.
2026-07-27 16:04:57 -05:00
Brooklyn Nicholson
2a7da2b549 style(desktop): fade split sashes until hover
The seam hairline sat at full strength on every split, so an empty
workspace read as a wireframe. Hold it at 0.1 and bring it up with the
grab band already on hover.
2026-07-27 15:58:34 -05:00
Brooklyn Nicholson
35a002a441 feat(desktop): mark the active pane tab with a primary underline
The active tab was defined by absence: the strip painted a rule and the
tab covered it, so inactive tabs stopped a pixel short to let it show.
Draw the state instead. The tab carries its own 2px --theme-primary
underline and the strip's rule goes away, which lets tabs run full height
and removes PANE_TAB_STRIP_LINE along with it.
2026-07-27 15:58:22 -05:00
Brooklyn Nicholson
d8b5bbf607 style(desktop): drop the titlebar and statusbar edge rules
The window chrome bracketed the workspace with a 1px rule top and bottom.
Both bars already paint the sidebar surface, so the rules divided one
continuous color rather than separating two.
2026-07-27 15:58:12 -05:00
Brooklyn Nicholson
c7ef4c192d refactor(desktop): name the overlay z-index ladder
DESIGN.md already said app-wide surfaces must not compete through ad-hoc
z-index literals, and the code disagreed: three overlapping numbering
schemes, and comments narrating the fight ("defaults to z-130, renders
UNDER the onboarding overlay (z-1300) ... bump it above with z-[1310]").
Picking a number meant reading someone else's near miss.

Name the rungs — modal, over-modal, switcher, and the boot chain — and
point the call sites at them. Every rung keeps the exact value it had, so
nothing moves; what changes is that the next overlay has a name to reach
for instead of a number to guess. Local stacking within a component stays
on plain z-10/z-20.
2026-07-27 15:48:09 -05:00
Brooklyn Nicholson
9ae3bd73c9 feat(desktop): confirm before quitting with a turn in flight
Cmd-Q went straight through to teardown, killing the backend mid-tool-call
— the turn is gone and whatever the agent was part-way through writing
stays part-way written, with nothing on screen to warn about it.

Renderers now report which chats are mid-turn; before-quit merges the
reports and asks, naming them, defaulting to Keep Running. Update, swap,
and uninstall relaunches skip the prompt: those are the app replacing
itself, and a modal there would strand the detached script waiting on a
PID that never exits.
2026-07-27 15:47:57 -05:00
Brooklyn Nicholson
2f5926ed05 fix(desktop): time a stream stall from the last activity
The tail "Hermes is thinking" indicator resets on every flush, but its
timer never did: with no timer key, useElapsedSeconds anchors to mount,
and the indicator mounts with the assistant message. A stall two minutes
into a turn therefore claimed two minutes of silence instead of the two
seconds that had actually passed.

Give the hook an explicit epoch and hand it the timestamp of the activity
the quiet spell followed. Compaction still counts from the turn's start,
which is the span it owns.
2026-07-27 15:47:53 -05:00
Brooklyn Nicholson
e9bb4c3951 fix(desktop): don't alert for prompts a reconnect replayed
A socket opening replays state that already existed — a session parked on
an approval re-emits its request so the UI can draw the prompt. Those
arrive as ordinary events, so launching Hermes, switching profiles, or
riding out a reconnect fired an OS notification for a prompt the user had
known about for an hour.

Hold native notifications for a beat after any gateway opens. The sidebar
row and the inline approval bar still appear immediately; only the OS
notification waits for something that actually just happened.
2026-07-27 15:47:45 -05:00
Brooklyn Nicholson
93477b2a0c fix(desktop): paint diffs from the theme palette
Diff add/remove lines were hardcoded to Tailwind's emerald/rose while the
overview ruler beside them — and the rest of the app — used --ui-green /
--ui-red, so every diff sat slightly off-brand and stayed put when the
semantic palette moved. Derive the tint, gutter, and text from those two
colors instead. One renderer feeds the tool card, the file preview, and
the review pane, so all three follow.
2026-07-27 15:47:42 -05:00
brooklyn!
3be565fbde
Merge pull request #72886 from NousResearch/bb/desktop-titlebar-sidebar-bg
fix(desktop): paint the titlebar with the sidebar's surface color
2026-07-27 15:28:56 -05:00
Brooklyn Nicholson
ecd5c79636 fix(desktop): sit composer chips on the text baseline
`align-middle` centers a pill on the x-height midpoint, which sits above
the center of the surrounding text box, so chips rode visibly low against
the words they're nestled between. Measured against the rendered surface,
`-0.12em` lands the chip's own baseline within 0.08px of the line's
(vs 0.79px off before) without growing the line box.

Applies to both the directive and slash chip classes — they share a line,
so fixing one and not the other just moves the mismatch.
2026-07-27 15:24:56 -05:00
Brooklyn Nicholson
de0b376cc9 fix(desktop): keep the @ popover open while typing a path
`AT_TRIGGER_RE` excluded `/` from the query, so the trigger died on the
first separator: `@/desk`, `@./www`, `@~/Desktop` and even `@file:src/foo`
all stopped matching the moment a path appeared. The gateway already
answered those queries correctly — the composer just never asked.

A `/` inside an `@` token is navigation, not a delimiter. The token stays
whitespace-bounded, which is what actually ends it.
2026-07-27 15:24:44 -05:00
Brooklyn Nicholson
b378cc0a72 fix(gateway): let @ completion find folders by name
The fuzzy branch of `complete.path` ranked basenames from
`_list_repo_files`, which lists files only, so a directory was only ever
reachable by typing a `/` — `@Desktop` returned nothing at all. Rank each
ancestor directory alongside the files, and break same-tier ties toward
the folder so `@docs` leads with `docs/` rather than `docs.md`.

Outside a git repo the fallback `os.walk` compounded this: it can spend
the whole `_FUZZY_CACHE_MAX_FILES` budget inside one deep subtree before
reaching a sibling, hiding top-level folders entirely. Seed the scan with
a `listdir` of the root so immediate children are always candidates.
2026-07-27 15:24:35 -05:00
Brooklyn Nicholson
bdd75630a7 fix(desktop): paint the titlebar with the sidebar's surface color
The shell titlebar declared no background of its own, so it showed through\nto the wrapper's --ui-bg-chrome and read as a lighter band above the\nsession list. Use --ui-sidebar-surface-background, the token the sidebar\nalready paints with, so the two chrome surfaces meet on the hairline\ninstead of a color change.
2026-07-27 15:22:13 -05:00
kshitijk4poor
551e1c6d64 refactor(agent): direct flag access in redecoration, matching call-block site
The call-block decoration reads agent._use_prompt_caching / _cache_ttl /
_use_native_cache_layout directly; the redecoration helper wrapped each in
getattr with divergent defaults (e.g. or-'5m' vs verbatim _cache_ttl).
The flags are unconditionally initialized on AIAgent, so the defaults
served only test fixtures and would mask a real init bug as silent
cache-off. Align with the house style.
2026-07-28 01:10:05 +05:30
kshitijk4poor
708390f47d refactor(moa): co-locate guidance peel with attach, add round-trip contract
_peel_moa_guidance hand-implemented the inverse of moa_loop's
_attach_reference_guidance from a different module — a drifting separator
or shape would make the peel silently no-op and put the last cache
breakpoint on the turn-varying guidance block (the #72626 bug class).
Move the inverse into moa_loop.peel_reference_guidance directly adjacent
to the attach, keep a thin wrapper in conversation_loop, and pin the
contract with a round-trip test over all three attach shapes.

Also fix the empty-list residue: peeling a guidance-only content part now
drops the whole message (mirroring the appended-user-message shape)
instead of leaving an empty-content user turn behind.
2026-07-28 01:10:05 +05:30
kshitijk4poor
f9be15d0f9 fix(agent): rebase MoA prepared request even when guidance is empty
guidance=None is a real prepared shape (all references failed / silent
degraded policy builds prepared_request without attaching guidance), and
the MoA facade sends prepared['messages'] — not api_kwargs['messages'].
Gating the rebase on 'and guidance' left the stale decoration in the
prepared object for the no-guidance MoA sub-path, so #72626 persisted
there. rebase_prepared_request already handles falsy guidance (copies
messages, skips the attach).
2026-07-28 01:10:05 +05:30
kshitijk4poor
bfd82660b5 refactor(agent): share static-prefix reconstruction, memoize failed rebuilds
The static-prefix reconstruction pattern (build_system_prompt_parts ->
['stable'] -> startswith gate -> fail-open) existed in three copies:
session restore (conversation_loop), compression keep-prompt path
(conversation_compression), and the new failover redecoration helper.
Hoist it into agent/system_prompt.reconstruct_static_prefix and call it
from all three sites.

Also memoize failed rebuilds per stored prompt (_static_rebuild_failed_for):
the redecoration chokepoint runs at the top of every retry attempt, and a
persistent stable-tier mismatch (restored session whose SOUL.md/skills
changed since save) would otherwise re-run the full prompt build — SOUL.md,
context files, memory I/O — on every attempt of every API call for the
life of the session. A legitimately changed stored prompt retries once.
2026-07-28 01:10:05 +05:30
kshitijk4poor
2322f0dcca fix(agent): restrict strip flatten to decoration-produced shapes
strip_anthropic_cache_control flattened ANY pure-text multi-part content
list with a separator-less join. Decoration only ever produces a single
text part or the 2-part [static, volatile] system split; organic
multi-part text (merged user turns, imported transcripts) got word-jammed
and parts carrying extra keys (citations) were silently dropped — on the
common no-failover path, since redecoration runs on every attempt.

Restrict the flatten to the exact decoration-produced shapes and make
marker removal copy-on-write on part dicts (the per-call message copy is
shallow, so parts alias the persistent history).
2026-07-28 01:10:05 +05:30
HexLab98
ece0107fc2 test(agent): cover prompt-cache redecoration across failover policy changes
Add strip_anthropic_cache_control coverage and policy-change cases
(cache-off→on, on→off, native→envelope, MoA guidance outside marker)
that TestSyncFailoverPreservesCacheDecoration did not exercise.
2026-07-28 01:10:05 +05:30
HexLab98
3e86df2753 fix(agent): redecorate prompt-cache breakpoints after provider failover
try_activate_fallback refreshes the cache policy flags for the new
provider, but the retry loop reused the primary's decorated api_messages.
Cache-off→cache-on shipped zero breakpoints; cache-on→cache-off left
stale markers. Strip and re-render at each retry attempt (same chokepoint
as reasoning-echo reapply), peel/rebase MoA guidance so the last marker
stays off the turn-varying block, and rebuild the static system prefix
when caching becomes active mid-turn (#72626).
2026-07-28 01:10:05 +05:30