Commit graph

1351 commits

Author SHA1 Message Date
ethernet
406d7a67f0
fix(desktop): split steer and queue keyboard shortcuts (#69797)
Keep Enter as the Cursor-style live-turn steering gesture and assign
Ctrl/Cmd+Enter to queue the current draft. Align keybind settings,
tooltips, translations, and the Electron queue-boundary coverage.
2026-07-23 00:13:58 -04:00
hermes-seaeye[bot]
2ebeede006
fmt(js): npm run fix on merge (#69823)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-23 04:09:14 +00:00
brooklyn!
88a9557385
Merge pull request #69812 from NousResearch/bb/desktop-tab-close-shift
fix(desktop): session-tab UX — ⌘W tab-shift, draft new-tab, unified status dot, stable lanes & optimistic delete
2026-07-22 23:01:17 -05:00
Brooklyn Nicholson
56b2b5a331 fix(desktop): mark session delete/archive in-flight so the row can't flash back
removeSession/archiveSession now pin their tombstone via beginSessionMutation
until the RPC settles (finally → endSessionMutation), keyed per id so
concurrent deletes across worktrees stay independent. This lets the
projects.tree prune hold the optimistic removal through the whole in-flight
window — no shared lock, no serialization.

Drops the now-redundant post-success re-filter band-aid in archiveSession:
the refresh honors tombstones generically, so there's nothing left to win.
2026-07-22 22:54:28 -05:00
Brooklyn Nicholson
4db2f78ec7 fix(desktop): honor optimistic session tombstones on list & tree refresh
A refresh racing an in-flight delete/archive resurrected the just-removed
row: refreshSessions repopulated $sessions straight from the backend page
ignoring tombstones, and the projects.tree prune dropped a tombstone the
moment its id left scoped_session_ids — so the grouped lane un-filtered it
too. The row only vanished on a later refresh once the RPC finally landed.

refreshSessions now filters tombstoned rows (and their lineage tip) before
merging. The prune keeps a tombstone while its mutation is still in flight
locally via $sessionMutationsInFlight, then hands it back to the normal
scoped-based prune once settled.
2026-07-22 22:54:25 -05:00
Brooklyn Nicholson
66747f154c fix(desktop): auto-expand a lane when starting a session in it, and stabilize sidebar collapse state
Clicking '+' on a collapsed worktree/branch lane (or repo) created a session in a folder the user couldn't see. It now force-expands the target node.

The root cause was that collapse state was stored as an XOR override of defaultOpen. defaultOpen flips for a worktree lane (collapsed while empty, open once it holds a session), so an explicit expand of an empty lane silently re-read as a 'collapse' the moment the lane gained its first row - collapsing the very lane you'd just opened to work in. Store the resolved open/collapsed boolean per node instead ($sidebarWorkspaceNodeOpen), which survives the default flip; one-time migration off the old set. The review file-tree shares this store and is updated to match.
2026-07-22 22:43:55 -05:00
Brooklyn Nicholson
87aaf87748 fix(desktop): render session status dot from one primitive (sidebar, tiles, main tab)
The sidebar row and the pane tabs each painted their own dot from different data: the sidebar read color from $sessionColorById[id] (map-only, no resolver fallback) layered with live state, while a tab painted a flat 'accent' color via sessionColorFor (map + fallback). A session older than the recents page missed the map, so the same session showed grey in the sidebar and its project color in the tab.

Add a single SessionStatusDot primitive keyed by the stored session id (the key every live-state atom already uses) that resolves color (override -> project, with fallback) and live state (working/needs-input/stalled/unread/background) itself. The sidebar row, session tiles, and the main workspace tab all render it, so a session's status/color can never disagree across surfaces. Tabs gain the full live status (pulse etc.), not just a static color. Collapses the now-orphaned generic 'accent' tab-dot path into the one primitive.
2026-07-22 22:43:51 -05:00
hermes-seaeye[bot]
7d96e602a9
fmt(js): npm run fix on merge (#69805)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-23 03:32:32 +00:00
ethernet
9f5e568812
fix(desktop): prevent stale worktree status (#69781)
Clear the coding rail while a session's workspace changes and discard late
Git status results for the previous cwd. This prevents Ctrl+Shift+B from
opening a worktree dialog against an old branch.
2026-07-22 23:24:17 -04:00
ethernet
dbf4f69b72
fix(desktop): queue prompts during context compaction (#69783) 2026-07-22 23:19:22 -04:00
Brooklyn Nicholson
067cb9e033 feat(desktop): shift next tab into main on ⌘W + always-shown new-session tab
- ⌘W on the main tab promotes the next stacked session tab into main
- '+' / ⌘T open a new 'New session' tab, unlisted until first message
  (no sidebar pollution); multiple new-session tabs allowed
- tile resolves its own row via by-id lookup once it has a message
2026-07-22 22:18:33 -05:00
brooklyn!
5c4d358a7e
Merge pull request #69750 from NousResearch/bb/desktop-fork-new-tab
fix(desktop): open branched chat in a new tab and switch to it
2026-07-22 22:17:17 -05:00
brooklyn!
6d76f5ca51
Merge pull request #69769 from NousResearch/bb/salvage-69720-clarify-late
feat(desktop): skipped clarify keeps its choices visible and answerable
2026-07-22 22:12:53 -05:00
Brooklyn Nicholson
f91d5b2d13 chore(desktop): dedupe session-color precedence, tighten tileStoredRow
- Extract resolveSessionColor(): the sessionColorFor fallback no longer
  re-implements the override -> project-color precedence that the
  $sessionColorById computed already owns.
- tileStoredRow: collapse the nested project-tree walk into a flatMap +
  find, matching the local style.
2026-07-22 22:11:49 -05:00
Brooklyn Nicholson
e80e036086 fix(desktop): use <Tip> instead of native title= on clarify skip buttons
The no-native-title guard (added on main after this PR's first CI run)
bans native title= on <button>. Wrap the shared ChoiceButton in the
themed <Tip>, which renders the child untouched when the label is falsy
so the live card is unaffected.
2026-07-22 21:56:01 -05:00
Brooklyn Nicholson
30f6fc81e2 fix(desktop): keep ever-active tab panes mounted so revisiting a tab doesn't layout-shift
A tab group rendered only the active pane's content, so every tab switch
unmounted and remounted the whole surface — revisiting a session tab
re-measured and re-scrolled the thread from scratch, visibly shifting
layout each time.

Panes that have been active in a zone now stay mounted in absolutely
positioned layers; the inactive ones hide via visibility (keeping their
layout box, so scroll positions and measurements survive) with
pointer-events disabled and aria-hidden. Mounting stays lazy — a pane
first mounts when first activated — so a boot-restored tab stack still
doesn't resume every session up front, and panes that leave the zone
(closed / moved) unmount as before.
2026-07-22 21:49:12 -05:00
Brooklyn Nicholson
bf21ba08e3 fix(desktop): resolve tab title and project color for sessions outside the recents page
Opening a session in a new tab left the tab titled 'Session' with no
project-color dot until new activity landed the row in the paginated
recents list. Two gaps:

1. tileTitle/tileAccent/tileDragPayload only looked the stored row up in
   $sessions (the recents page). Sessions opened from a project group are
   often older than that page, so the lookup missed entirely. Resolve
   through the project tree as a fallback (tileStoredRow) and re-sync pane
   titles/accents when $projectTree loads.

2. Even with the row present, liveSessionProjectId returned null for a
   session whose cwd sits outside its recorded git_repo_root (mid-session
   relocation / sibling worktree), because the cwd-under-root guard ran
   before the explicit-project folder match. The backend tree groups such
   rows under the project; the client now agrees — an explicit folder
   match is authoritative, only the auto-project (repo root) fallback
   still needs cwd-under-root confidence.

sessionColorFor also computes directly (overrides -> project color) when
the row isn't in the $sessionColorById map, which is keyed over $sessions
only.
2026-07-22 21:46:15 -05:00
ethernet
d63a1c4ccb
fix(desktop): separate workspace defaults from live cwd (#69765) 2026-07-23 02:44:26 +00:00
Brooklyn Nicholson
efa002dd5d refactor(desktop): share the clarify choice row and fix skip copy
Builds on the skipped-clarify card so it holds up beyond the timeout case:

- Extract a shared `ChoiceButton` (letter badge + label + row chrome) used
  by both the live pending card and the settled skip card. The two blocks
  had drifted into duplicated markup; now they can't diverge.
- Fix the hint copy. An empty `user_response` is emitted for BOTH a
  server-side timeout AND a manual Skip (tools/clarify_tool.py) — there is
  no field on the result that tells them apart — so asserting "This question
  timed out" was wrong half the time. Neutral wording ("This prompt is no
  longer waiting…") is correct for either, and the recover-your-answer path
  now also helps someone who mis-clicked Skip. Updated en/ja/zh/zh-hant.

No behavior change to the live prompt or the follow-up-message flow.

Co-authored-by: SHL0MS <SHL0MS@users.noreply.github.com>
2026-07-22 21:39:27 -05:00
SHL0MS
8b96fc57ed feat(desktop): skipped clarify keeps its choices visible and answerable
When a clarify prompt times out, the settled card collapsed to just
'Skipped' — the options were unrecoverable (the args carry them, the
renderer dropped them) and there was no way to answer late.

The skipped card now:
- renders the original choices, letter-badged like the live card
- clicking one drafts a quoted follow-up ('Re: "<question>" — my
  answer: <choice>') into the composer via the insert bus. Enter sends
  it; if the agent is mid-turn it queues like any other prompt.
- a hint line explains the question timed out and what picking does

No retroactive resolution of the expired request: the tool already
returned empty and the turn moved on — injecting into past context
would break prompt-cache and role-alternation invariants, and
clarify.respond on an expired id hard-errors (#56558). The follow-up
message path needs no backend change and works against old backends.

Answered clarifies and free-text (no-choice) skips are unchanged.

Interim UX for #44845 (durable ID-addressable clarify decisions).
2026-07-22 21:38:32 -05:00
Brooklyn Nicholson
35cdc63ca0 fix(desktop): use repo-forked codicon (git-fork has no glyph)
The bundled @vscode/codicons font has no `git-fork` glyph (only
`git-fork-private`), so the Branch menu item rendered blank. Use
`repo-forked`, the actual fork icon, to match the inline GitFork action.
2026-07-22 21:32:23 -05:00
Brooklyn Nicholson
a0a24ba215 fix(desktop): use fork icon for branch in session context menu
The row/tab context-menu 'Branch from here' item used the git-branch
codicon while the inline message action uses a GitFork icon. Switch the
menu item to the git-fork codicon so branching looks consistent across
surfaces.
2026-07-22 21:27:56 -05:00
Brooklyn Nicholson
6326b30c93 fix(desktop): open branched chat in a new tab and switch to it
Branching/forking a chat replaced the primary chat (setActiveSessionId +
setSelectedStoredSessionId + navigate). Instead, open the branch as its own
session tile tab in the center zone and reveal it, leaving the parent chat
exactly where it is — mirroring openNewSessionTile. All branch entry points
(message GitFork button, /branch and /fork, sidebar Branch, tile Branch) go
through forkBranch, so this covers every surface.
2026-07-22 21:13:06 -05:00
ethernet
8a21df18ac
fix(desktop): place steer messages before redirected replies (#69739)
* test(desktop): reproduce steer transcript placement

* fix(desktop): place steer messages before redirected replies
2026-07-22 22:04:57 -04:00
brooklyn!
2c1d585002
Merge pull request #69655 from NousResearch/bb/out-of-credits-ux
feat(billing): consistent out-of-credits UX across CLI, TUI, and desktop
2026-07-22 21:03:38 -05:00
brooklyn!
da3c506db5
Merge pull request #69691 from NousResearch/bb/desktop-billing-polish
Desktop billing polish + shared Progress primitive + settings skeletons
2026-07-22 21:03:34 -05:00
ethernet
6d17b2a593
fix(desktop): preserve active correction on warm resume (#69725)
Update the gateway's live turn projection after an accepted correction so a warm session resume does not add the stale original prompt beside the persisted correction.

Cover the inference-time correction path end to end and assert both redirect entry points refresh the live user text.
2026-07-22 21:53:56 -04:00
ethernet
2a2474512b
test(desktop): cover queued prompt turn boundary (#69729) 2026-07-23 01:36:31 +00:00
ethernet
a23e39fe6d
test(desktop): cover correction resume without duplicate prompts (#69708)
* test(desktop): cover correction resume without duplicate prompts

Exercise a live composer correction, switch away and back before the
response settles, and assert both user turns retain their order and occur
exactly once.

* test(desktop): cover correction warm resume during tool run

Exercise a correction accepted at a foreground-tool boundary, a switch through a persisted session, and the warm resume back. Assert the original prompt and correction remain singular and ordered.
2026-07-23 01:06:30 +00:00
ethernet
deadb43cc2
fix(desktop): show composer action shortcuts (#69707)
Expose the existing platform-aware keybind hints on Send, Steer, and Queue
composer control tooltips. Add focused tooltip coverage for each action.
2026-07-22 21:00:50 -04:00
Brooklyn Nicholson
43787dab14 feat(desktop/settings): DOM-shaped skeleton loaders on every settings page
Model settings was the only page that kept its shape while loading; the rest
flashed a centered spinner (LoadingState) or empty placeholders. Standardize on
skeletons that mirror the settings rhythm.

- Add shared SectionHeadingSkeleton / ListRowSkeleton / SettingsSkeleton to
  settings/primitives.tsx (mirror SectionHeading + ListRow).
- Convert keys, providers, sessions, gateway, custom-endpoints, and config
  (non-model) from LoadingState to SettingsSkeleton; remove now-dead LoadingState.
- billing: BillingSkeleton (summary cards + sections) on first load instead of
  "—" placeholder cards.
- pet: skeleton grid on first load instead of a premature "unreachable" message.
2026-07-22 19:22:37 -05:00
ethernet
3651627d88
fix(desktop): skip bootstrap for explicit Nix backend (#69688)
Treat HERMES_DESKTOP_HERMES as an authoritative deployment override.
This keeps the Nix desktop package on its matching immutable Hermes CLI
instead of falling through to install.sh when a best-effort version probe
fails or times out.
2026-07-22 20:00:48 -04:00
Yingliang Zhang
8c745314b9 fix(desktop): synchronize context usage and compaction status 2026-07-22 16:58:58 -07:00
Brooklyn Nicholson
e4b2b77852 refactor(desktop): extract shared Progress primitive; billing uses it
Add components/ui/progress.tsx — one rounded track + animated fill that owns
role="progressbar" + aria. Migrate the hand-rolled bars (updates overlay,
onboarding, install overlay, billing usage, pet hatch) onto it.

- Pet's bar was never a color variant (--primary and --ui-accent are the same
  brand color); its only real difference is the sliding indeterminate, now an
  `animated` prop. Color stays a normal `fillClassName` override.
- Billing usage now uses the plain primitive — dropped the bespoke dither
  track, inset shadow, and danger nub; tone rides `destructive`/`fillClassName`.
- Drop the redundant emoji "no saved card" row description; the page-level
  warn banner is the single explainer.
- Rename pet CSS .pet-progress* -> generic .progress-slide.
2026-07-22 18:53:26 -05:00
ethernet
93c97073d8
fix(desktop): prevent stale optimistic tails after compression (#69682) 2026-07-22 23:32:20 +00:00
Brooklyn Nicholson
066b53b282 fix(desktop): narrow billing custom top-up input (w-20 -> w-16) 2026-07-22 18:31:21 -05:00
ethernet
6283a33a50
fix(desktop): show steer glyph in busy composer (#69612)
* fix(desktop): show steer glyph in busy composer

Restore the steering-wheel glyph when a typed draft redirects a live turn.
Add a real Electron E2E regression test for stop, steer, attachment queueing,
and a paused queue after Stop.

* feat(desktop): add queue action beside steer

Restore an explicit queue action in the former steering slot while a typed
correction is ready. Keep the primary steering-wheel action for redirecting
the active turn.

* fix(desktop): retain dictation beside queued drafts

Keep the dictation microphone visible when a typed busy draft exposes the
separate queue action. Cover the combined controls in the real Electron flow.

* fix(desktop): place queue beside the busy action

Keep the Queue message control after the read-aloud toggle and directly before
the shared Stop/Steer action. Cover the rendered control order in Electron E2E.
2026-07-22 23:29:38 +00:00
Brooklyn Nicholson
c4acc4d2c5 feat(desktop): add prefix/suffix adornments to Input primitive; use $ prefix in billing top-up 2026-07-22 18:17:33 -05:00
ethernet
55759cb273
fix(desktop): refresh composer branch after worktree creation (#69657)
* fix(desktop): refresh composer branch after worktree creation

Route new worktree sessions through the shared workspace target handoff so
composer git status follows the created worktree instead of remaining on the
main checkout. Add an Electron E2E regression test for Ctrl+Shift+B.

* fix(desktop): fixme flaky test
2026-07-22 23:13:23 +00:00
brooklyn!
9024835bf2
Merge pull request #69602 from NousResearch/bb/voice-interrupt-note
feat(voice): tell the model when the user interrupts its spoken reply
2026-07-22 18:13:19 -05:00
brooklyn!
0ac07fdafd
Merge pull request #69511 from NousResearch/bb/voice-streaming
feat(voice): streaming, conversational TTS with barge-in across all surfaces
2026-07-22 18:13:08 -05:00
Brooklyn Nicholson
d0c4a82da9 feat(desktop): billing toast + in-chat status-row banner with smart CTA
On a billing wall, raise a sticky, billing-specific toast (never the generic
error toast) and a persistent in-composer banner for the active session — both
with one recovery action: Nous → in-app Settings → Billing, other providers →
their billing page (deep-linked). The banner reuses the shared StatusRow chrome
(no bordered alert, Codicon glyph, shared buttons), and the composer stays usable
so slash commands keep working.
2026-07-22 18:09:08 -05:00
Brooklyn Nicholson
960d339f86 feat(billing): shared cross-surface out-of-credits signal
Detect a billing wall once (agent/error_classifier → FailoverReason.billing) and
map it to a recovery link + label in one place, then carry that structured
BillingBlock to every surface instead of re-parsing free-form error text per
surface.

- agent/billing_links.py: provider-agnostic slug/host → (label, billing URL)
  table (single source of truth), Nous-aware (is_nous routes to the in-app flow);
  unknown providers degrade to a readable label with no invented URL.
- conversation_loop: both billing exit paths return a billing_block through one
  helper; the guidance message carries the derived URL for every provider.
- gateway forwards billing_block on message.complete (it was dropped).
- @hermes/shared: BillingBlock type shared by desktop + TUI.
2026-07-22 18:08:59 -05:00
hermes-seaeye[bot]
54aaff142e
fmt(js): npm run fix on merge (#69669)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-22 23:05:44 +00:00
ethernet
68abf0b33d
test(desktop): add E2E coverage for session lifecycle (#69580)
* test(desktop): e2e test for interim assistant message preservation (#65919)

Adds a Playwright E2E test that reproduces the fix from PR #65919 across
all three layers (agent core → tui_gateway → desktop renderer). The mock
inference server is upgraded with a multi-turn scripted response that
exercises several interleaved patterns:

  1. text + tool_call  → should produce an interim message
  2. text + tool_call  → another interim message
  3. no text + tool_call → NO interim (no visible text alongside tools)
  4. text + tool_call  → another interim message
  5. final answer (stop) → message.complete, different from all interims

Two describe blocks exercise display.interim_assistant_messages both on
(default) and off:
  - ON:  all interim texts + the final answer visible in the transcript
  - OFF: only the final answer visible, all interim texts wiped

Also fixes a footgun: test:e2e now runs `npm run build` as a pretest
hook so the renderer dist/ is always fresh. Previously, running
`npx playwright test` locally would silently load a stale dist/ that
predated renderer fixes — the python backend ran from source (had the
fix) but the renderer was frozen in an old bundle. CI already built
fresh, so the explicit build step there is removed to avoid duplication.

* test(desktop): e2e sidebar states — background dot, subagent, cross-session

Add sidebar-states.spec.ts with three E2E tests exercising the desktop
sidebar's session dot states driven by real gateway events:

1. Background process dot appears during a terminal(background=true)
   call and disappears after auto-dismiss; subagent (delegate_task)
   runs concurrently; final answer is visible in the transcript.

2. Background dot remains visible while a subagent runs concurrently
   (longer sleep 5 background process so the dot is catchable).

3. Cross-session dot transition: start a turn with a background process,
   wait for the turn to complete, open a new session, then verify the
   original session's dot transitions from 'background running' to
   'finished — unread' when the background process exits.

The mock server gains SIDEBAR_SCRIPT and SIDEBAR_CROSS_SCRIPT trigger
keywords that return tool_calls for terminal(background=true) and
delegate_task — the agent executes these for real (real background
process, real subagent), so the tests assert against genuine gateway
events rather than mocked UI state.

Verified: 3 passed (1.2m) under cage headless wlroots.

* test(desktop): e2e tests for tile-unread bug (tab passes, split fails)

Two scenarios for the tile-unread bug where a session that finishes
while visible on-screen gets the green 'finished unread' dot even
though the user is looking right at it.

The unread check in handleTransition (session-states.ts:174) only
compares against $selectedStoredSessionId and ignores $sessionTiles,
so a session visible in a tile gets marked unread even though it's
on screen.

1. TAB (hidden, PASSES): ⌃-click opens the session as a stacked tab
   that is NOT visible on screen. The unread dot IS correct here —
   the user isn't looking at it.

2. SPLIT (visible, FAILS): drag the session row to the workspace's
   right edge to create a side-by-side split tile. Both sessions are
   visible on screen. The unread dot is WRONG — the session is visible
   in the split tile, so it should not be marked 'unread'. This test
   is RED until the fix lands.

Also adds explicit page.screenshot() calls at key assertion points in
sidebar-states.spec.ts so the trace viewer has full-res captures of the
sidebar dot states during the test.

* test(desktop): cover compression and queued stop lifecycle

Add real desktop E2E coverage for session compression continuation and
queue parking after an explicit Stop. Extend the mock server with a
blocking scripted turn and submitted-prompt assertions.

* test(desktop): cover busy composer submit routing

Replace the invalid queued-stop E2E scenario: plain text redirects a busy
turn rather than entering the queue. Add focused submit-routing coverage for
plain text, slash commands, attachments, explicit Stop, and idle submission.
2026-07-22 22:56:13 +00:00
Brooklyn Nicholson
177a57f70c feat(voice): desktop flags interrupted submits
markVoicePlaybackInterrupted() / takeVoicePlaybackInterrupted() mirror
the backend latch in the renderer (the barge happens client-side, where
the audio plays). VAD barges and typing over playback mark it; the next
prompt.submit carries interrupted:true, which the TUI gateway latches
into the model note.
2026-07-22 17:53:06 -05:00
Brooklyn Nicholson
93e9061f15 feat(voice): desktop speech-stream sessions with barge-in capture
/api/audio/speak-stream WebSocket: one socket + one Web Audio clock per
reply. The renderer feeds raw LLM deltas as they arrive; the server
cuts sentences with the shared chunker and streams int16 PCM back while
generation continues — speech overlaps generation with no per-sentence
connection or synthesis gaps. Falls back to the POST data-URL path for
old backends / non-chunked providers.

Barge-in runs a MediaRecorder on the monitor's stream the whole time
playback is live (rotated while quiet to bound pre-roll); talking over
the agent cuts playback and the complete utterance goes straight to
transcription and submit. /api/audio/transcribe returns 200/"" for
no-speech results so quiet turns re-listen instead of toasting a 400.
2026-07-22 17:47:25 -05:00
hermes-seaeye[bot]
15dc65eeda
fmt(js): npm run fix on merge (#69651)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-22 22:14:06 +00:00
ethernet
73c7f68456
fix(desktop): avoid duplicate inflight user rows on resume (#69649)
Keep a live session projection from adding its user turn when the latest
persisted row already represents that same inflight prompt. Add real Electron
coverage for fast and cold resume with idle and background-inference sessions.
2026-07-22 18:07:12 -04:00
hermes-seaeye[bot]
1b8e34e945
fmt(js): npm run fix on merge (#69644)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-22 21:56:16 +00:00