Commit graph

142 commits

Author SHA1 Message Date
Brooklyn Nicholson
ea5fa505d9 fix(desktop): clamp floating composer to the thread area, not the whole window
Now that the popped-out composer is fixed to the viewport, clamping against the
window let it slide under a pinned sidebar. Confine it to the thread region
(data-slot="composer-bounds") instead — its rect already excludes a pinned
sidebar and the header — falling back to the full window before it's measured.
This subsumes the old titlebar top-margin (the thread rect starts below the
header).
2026-06-22 13:57:53 -05:00
Brooklyn Nicholson
aff5ae692f fix(desktop): move composer out of contain wrapper instead of portaling
Replaces the body-portal approach: render ChatBar as a sibling of the
contain:[layout paint] chat wrapper (inside the same runtime boundary) rather
than portaling the floating instance to <body>. The wrapper is a containing
block for — and clips — position:fixed descendants, which is what stranded the
popped-out composer off-screen. As a sibling it anchors to the outer relative
container: docked stays absolute (identical placement), floating resolves
against the viewport. Both states stay mounted, so dock<->float no longer
remounts the editor (the portal toggle did).
2026-06-22 13:41:53 -05:00
Brooklyn Nicholson
79f270f549 fix(desktop): portal floating composer to body so it can't be clipped off-screen
The popped-out composer is position:fixed, but the chat content wrapper sets
`contain: layout paint`, which makes it a containing block for — and clips —
fixed descendants. Inline, the floating composer was positioned/clipped relative
to the chat column (which shifts with the sidebars), not the viewport, so the
viewport-based bounds clamp from #50466 couldn't keep it reachable: users still
lost it off-screen. Portal it to <body> when popped out so fixed positioning and
the clamp finally share the viewport as their reference. Docked stays inline
(it's absolute within the chat column by design).
2026-06-22 13:37:31 -05:00
kshitij
ab22317d09
Merge pull request #50214 from kshitijk4poor/salvage/desktop-rename-branched-50143
fix(desktop): rename a branched session via session.title RPC (fixes "Session not found")
2026-06-22 15:15:30 +05:30
brooklyn!
1ec4fcf614
Merge pull request #50466 from NousResearch/bb/composer-popout-bounds
fix(desktop): keep the floating composer in-bounds (can't be lost off-screen)
2026-06-21 18:58:14 -05:00
liuhao1024
bef1d3e4ff
fix(desktop): filter undefined entries in AttachmentList to prevent refText crash on session switch (#49624)
* fix(desktop): filter undefined entries in AttachmentList to prevent refText crash on session switch

When switching sessions, the attachments array can contain stale/undefined
entries from the previous session's state. Accessing attachment.refText on
an undefined entry throws TypeError, breaking session switching entirely.

Fix: add .filter(Boolean) before .map() to skip undefined/null entries.

Fixes #49614

* fix(desktop): update I18nConfigClient usage in attachment test

The i18n config API changed from getLocale/saveLocale to
getConfig/saveConfig. Update the test fixture to match.
2026-06-21 18:54:09 -05:00
Brooklyn Nicholson
16aeba1707 fix(desktop): clamp composer peel-off under cursor
Keep the floating composer bounded from the first peel-off frame and leave titlebar clearance when recovering bad persisted positions.
2026-06-21 18:52:01 -05:00
Brooklyn Nicholson
7785655b4e fix(desktop): keep the floating composer in-bounds so it can't be lost off-screen
The pop-out position is a bottom-right corner inset; the old clamp only floored
it and capped each inset by a flat constant, so dragging left/up (or restoring a
position saved on a larger/other monitor) could push the box's width/height past
the left/top edges and strand it off-screen — unrecoverable since the bad spot
persisted to localStorage.

Now the clamp bounds the WHOLE box (accounting for its measured width/height plus
an edge margin) on all four sides. Applied on drag (measured size), on load
(clamped in readPosition), and via a mount + window-resize reclamp so a shrunk
window or stale persisted value always pulls the box back into view.
2026-06-21 18:35:33 -05:00
kshitijk4poor
ed81f0b633 fix(desktop): log session.title RPC failure before REST fallback
The RPC-rename fallback swallowed all errors silently. Narrow it to log
the swallowed error via console.warn so a genuine session.title RPC
failure (which then surfaces a REST 404 for the runtime id) is
diagnosable instead of invisible. Behavior is unchanged: REST fallback
still runs for any session with a persisted row.
2026-06-21 20:41:31 +05:30
xxxigm
7f43378931 test(desktop): cover renameSessionPreferringRpc routing
Verifies the active branched session renames via the session.title RPC
(not REST), and that REST is used for non-active rows, title clears, RPC
failures (socket mid-reconnect), and when no gateway is connected.
2026-06-21 20:41:11 +05:30
xxxigm
0e47f68a47 fix(desktop): rename branched session via session.title RPC
A freshly branched session (and any brand-new chat) lives only in the
gateway's in-memory _sessions map keyed by its runtime id — no row is
persisted to state.db until the first turn. The rename dialog hit REST
PATCH /api/sessions/{id}, which resolves against the stored sessions
table, so it 404'd with "Session not found" on these runtime-only rows.

Route the rename of the ACTIVE/selected session through the gateway's
session.title RPC (which resolves the live runtime session and persists
the row on demand), mirroring the /title slash command. Fall back to REST
for non-active rows, title clears, and when no gateway is connected.
2026-06-21 20:41:11 +05:30
Brooklyn Nicholson
eed78d6ebb fix(desktop): composer popout polish — peel-off placement, panels, chip editing
- Peel-off undock drops the floating composer under the cursor (centered
  horizontally, preserving the vertical grab offset) instead of snapping to
  the docked corner.
- Unify the / · @ · ? completion drawer and the attach (+) menu onto one
  shared glassy panel primitive (composerPanelCard): smallest theme font,
  hairline border, nous shadow; floats off the composer, inset from the left.
- Directive chips: Backspace removes the chip + its auto-inserted trailing
  space atomically (no orphaned space), and a phantom trailing block left by
  contenteditable no longer falsely expands the composer to two rows.
- Model picker: scroll area capped at max(150px, 30dvh); footer rows aligned
  (matching icons, dropped a redundant margin).
- Composer focus shifts the border ~15% toward foreground (no fill change);
  input is cursor-text; trimmed control icon/button sizes.
2026-06-20 02:10:38 -05:00
Brooklyn Nicholson
f697c97e02 fix(desktop): keep floating composer radius consistent with docked 2026-06-20 01:36:29 -05:00
Brooklyn Nicholson
236f0597e5 feat(desktop): pop the composer out into a draggable floating window
Gesture-driven: drag the docked composer up to peel it out, drag it back to
the bottom-center dock zone (radial glow ramps with proximity) to redock, and
double-click the grab area to toggle. Floating composer is compact, grows
upward as it wraps, and can be moved by its 5px transparent grab platform
(diagonal hatch on hover). Position + popped state persist; secondary windows
always start docked. rAF-coalesced drag, persisted only on release.
2026-06-20 01:35:30 -05:00
Austin Pickett
016bce1a09
fix(desktop): recover stranded session windows when resume fails (#47655)
* fix(desktop): recover stranded session windows when resume fails

Opening a session in a new window (or any routed resume) could latch the
thread loader on "session" forever — the reported "stays stuck loading,
even after a nap" bug. Two compounding causes:

1. use-session-actions.resumeSession's catch ran the REST transcript
   fallback OUTSIDE its own try. When session.resume rejected AND the
   fallback also threw (the common case on a wedged/unreachable backend),
   the throw skipped setMessages and left activeSessionId null with an
   empty transcript — exactly the state the loader gates on
   (messagesEmpty && !activeSessionId), with no terminal/error state.

2. use-route-resume's self-heal could never re-fire: resumeSession sets
   selectedStoredSessionIdRef synchronously at entry (before failing), so
   stuckOnRoutedSession stays false, and on an already-open idle window
   neither pathnameChanged nor gatewayBecameOpen fire again. The window
   never retried — naps, focus, nothing recovered it.

Fix:
- Wrap the REST fallback in its own try so a fallback failure can't strand
  the loader.
- Add $resumeFailedSessionId: armed on terminal resume failure, cleared at
  the next resume's entry (and left clear on success).
- use-route-resume gains a bounded backoff auto-retry (4 attempts, 1s→8s)
  that re-resumes while the routed session matches the failure flag, with a
  fire-time liveness recheck so a recovered session isn't double-resumed.

Regression tests cover: fallback-wrap arming the flag without throwing,
flag cleared on success, retry fires on backoff, no retry for a
non-routed/recovered session, and the retry cap.

* feat(desktop): show error + manual Retry when resume retries exhaust

When a stranded session window's bounded auto-retry gives up (gateway
resume RPC + REST fallback fail through all MAX_RESUME_RETRIES attempts),
the loader latched forever. Add a $resumeExhaustedSessionId atom armed at
the give-up point so the chat view swaps the perpetual spinner for an
explicit error state + manual Retry button. Retry / reconnect / reselect
clears the latch and resets the auto-retry counter for a fresh cycle; a
route-change away from the stranded session also clears it.

Distinct from $resumeFailedSessionId (armed during the backoff window) so
the error UI only appears once auto-recovery has actually given up, not
mid-retry. Adds i18n strings across en/ja/zh/zh-hant and 3 tests covering
latch-arms-on-exhaustion, stays-clear-while-retries-remain, and
clears-on-route-change.

* fix(desktop): address review on stranded-resume recovery layer

Follow-up to review on #47655 (PR head 253bfc0e3). Four issues on the
recovery layer:

1. (blocking) Arm $resumeFailedSessionId only when the transcript is still
   empty after the REST fallback ($messages.get().length === 0), matching the
   atom's documented contract and the loader's messagesEmpty gate. Previously
   armed on any resume-RPC reject regardless of fallback outcome, so a window
   that recovered its history via REST still auto-retried and, on exhaustion,
   blanked the visible transcript behind the error overlay.

2. Reset the bounded-retry attempt counter on the $resumeExhaustedSessionId
   armed->cleared edge so a manual Retry / reconnect / reselect on the SAME
   stranded session gets a fresh backoff cycle, not a single one-shot attempt
   that immediately re-arms the error. (Keyed on the exhausted latch rather
   than the resumeFailedSessionId null->value transition the review suggested:
   the auto-retry loop itself toggles resumeFailedSessionId every cycle, so
   keying the reset there would defeat the MAX_RESUME_RETRIES cap. Only
   resumeSession clears the exhausted latch, making its clear edge the
   unambiguous manual-retry signal.)

3. Advance retryAttemptRef only when the timer actually dispatches a resume,
   not at schedule time. Prevents unrelated dep changes during the 1s-8s
   backoff window (transient gatewayState flip, non-stable resumeSession) from
   burning attempts and hitting MAX with fewer than 4 real resume attempts.

4. Drop unrelated blank-line-only insertions in store/session.ts and
   use-session-actions.ts to keep the diff tight.

Tests: +3 (RPC-fails-REST-succeeds-no-arm; manual-retry-fresh-cycle;
no-attempts-burned-on-dep-churn). All 19 resume tests + full session-hook
suite (65) pass; tsc --noEmit clean.

---------

Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
2026-06-17 17:33:53 -04:00
Austin Pickett
ee41aa0c1a
feat(desktop): add dismiss control to chat error banners (#47985)
A failed turn leaves a red error banner inline in the transcript. These
errors are renderer-local state (never persisted) and stay pinned to the
message until the session is reloaded, so a stale, no-longer-relevant
error (e.g. a transient provider/inference error) lingers with no way to
clear it.

Add an 'x' dismiss button inside the existing MessagePrimitive.Error
block. Clicking it clears the error from BOTH the live $messages view
and the per-runtime session cache — the view first, because
preserveLocalAssistantErrors re-grafts any still-errored message it finds
in the view onto the next session.info flush, so clearing only the cache
would let the heartbeat resurrect the banner. A bare error placeholder
(no streamed content) is dropped entirely; a turn that streamed partial
output before failing keeps its text and just sheds the error.

The control only renders when an onDismissError handler is wired, so
secondary/embedded Thread usages are unaffected. Adds the dismissError
string to all four locales (en/ja/zh/zh-hant) and two behavior tests.

Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
2026-06-17 16:46:43 -04:00
brooklyn!
44e5848e74
feat(desktop): stream subagent activity into watch windows (#47060)
* feat(desktop): stream subagent replies into watch windows

A desktop watch window resumes a child session lazily (no full agent) and
mirrors the parent-relayed `subagent.*` events into native child-session
stream events. The child's streamed reply text was never relayed, so the
window sat blank while the subagent "talked".

- delegate_tool: forward the child's `run_conversation` stream tokens up the
  progress relay as `subagent.text` (inert under CLI/TUI — their progress
  handlers ignore non-tool event types; only a gateway watch window mirrors it).
- server: mirror `subagent.text` -> `message.delta` on the child sid only, and
  skip the parent emit (per-token frames are meaningless on the parent session,
  which shows the child via the spawn tree). Demote `subagent.start` to a
  one-time goal header and drop the noisy `subagent.progress` mirror — tools
  already mirror natively.
- server: guard `_start_agent_build` so a lazy watch session spectating an
  in-flight child stays lazy; incidental RPCs were upgrading it to a full
  agent mid-stream and silently killing the mirror.

* fix(desktop): keep watch-window chat clear of titlebar chrome

Secondary windows (new-session scratch, subagent watch, cmd-click pop-out)
hide the titlebar tool cluster + session header, so the transcript ran to the
window's top edge and streamed text slid up under the OS traffic lights.

- Gate the hidden chrome on `isSecondaryWindow()` everywhere (app-shell,
  chat header, thread list) instead of the narrower new-session flag.
- Add a fixed opaque drag-strip at the top of the secondary-window transcript:
  content padding alone scrolls away with the text, so the strip masks
  anything behind it and keeps the window draggable like the main header.

* fix: WSL subagent window

* fix: subagent window top padding

---------

Co-authored-by: Austin Pickett <pickett.austin@gmail.com>
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
2026-06-16 14:30:11 -04:00
Brooklyn Nicholson
80e4b8985e feat(desktop): tighten composer model picker interactions
Clicking a model row in the composer dropdown now commits and closes the menu
(via a close context); the hover-revealed reasoning/fast submenu stays open to
tweak. The pill shows a quiet braille loader instead of literal "No model"
until one resolves, and steer takes over the mic slot while typing into a
running agent.
2026-06-16 09:50:27 -05:00
Brooklyn Nicholson
c92a95a130 feat(desktop): move model selector from statusbar to composer
Relocate the model pill to the composer, left of the mic. A new
ModelPill reuses the live ModelMenuPanel dropdown verbatim (single
click target) and the formatModelStatusLabel "Model · Fast Med" label,
anchored to its right edge so the menu doesn't drift with model-name
length. modelMenuContent now flows to ChatView instead of
useStatusbarItems, and the status-bar model-summary item is removed;
the pill subscribes to the model atoms directly and falls back to the
full picker when the gateway is closed.
2026-06-15 23:53:41 -05:00
Brooklyn Nicholson
0f75e9904a feat(desktop): trim scratch window chrome
Hide nonessential Hermes chrome in the new-session pop-out while preserving native window controls and stable first-message positioning.
2026-06-15 20:59:57 -05:00
brooklyn!
49dd91d682
fix(desktop): show copied checkmark on session Copy ID (#46030)
Route sidebar Copy ID through CopyButton so dropdown and context menus
get the same checkmark feedback as every other copy action.
2026-06-14 07:38:55 +00:00
brooklyn!
6b76284c77
fix(desktop): surface off-screen approvals via the jump-to-bottom control (#45853)
* fix(desktop): jump-to-approval pill for off-screen approvals

A blocked approval's only response surface is the inline Run/Reject bar on
the pending tool row. When that row is scrolled out of view the session looks
stalled with no visible action. Surface a composer-anchored "Approval needed"
pill only when an approval is pending AND its inline bar is scrolled away;
clicking scrolls the bar back into view. Preserves the deliberate inline (not
modal) approval design — the pill never duplicates the approve/reject controls.

The inline bar mirrors its own viewport visibility via IntersectionObserver
(tracks scroll/resize/layout) and registers a scroll-into-view handler the pill
fires, mirroring the existing thread-scroll jump-button bridge.

Supersedes #45828.

* fix(desktop): morph jump-to-bottom into approval prompt; drop scroll bridge

Collapse the separate "jump to approval" pill into the existing
scroll-to-bottom control: when scrolled away from the bottom while an approval
is pending, it relabels to "Approval needed". A parked approval's inline
Run/Reject bar is always the bottom-most content, so the existing
scroll-to-bottom action lands the user right on it — one control, no collision.

This also fixes the layout corruption from the first cut: the pill called
native el.scrollIntoView(), which scrolls every scrollable ancestor including
the overflow:hidden chat shell containers. Those have no scrollbar to scroll
back and don't remount on session switch, so the composer stayed shoved and
the breakage persisted across sessions. Reusing requestScrollToBottom() (the
use-stick-to-bottom path) only touches the one designated scroll container.

Removes the now-unused approval-scroll store + IntersectionObserver wiring.
2026-06-13 23:07:22 +00:00
brooklyn!
0a865e5948
fix(desktop): bypass Chromium editing pipeline for large paste & select-delete (#45812)
Large paste and Ctrl+A → Delete froze the composer for seconds — both routed
through Chromium's contenteditable editing pipeline (~O(n²) on multiline DOM).

- insertPlainTextAtCaret: Range + text/<br> fragment (paste path)
- deleteSelectionInEditor: range.deleteContents for non-collapsed Backspace/Delete
- Shared composerSelectionRange helper; both flush via flushEditorToDraft

Profiled live (47 KB / 122 paragraphs): paste 4474 ms → 13 ms; select-delete
1304 ms → 4 ms. Collapsed-caret deletes still native.
2026-06-13 20:49:58 +00:00
brooklyn!
425e777f54
fix(desktop): polish slash command completion (space/tab/click + typed args) (#45760)
* fix(desktop): accept slash command on space at command stage

Pressing space on a no-arg slash command (e.g. /hermes-agent) fell
through to the arg-completion stage and dead-ended on "No matches"
instead of inserting the directive. Space now mirrors Tab/Enter while
the command name is still being typed: no-arg commands commit the chip,
arg-taking commands expand to their options step.

* fix(desktop): suppress arg popover for no-arg slash commands

Committing a no-arg command (`/hermes-agent `) re-detected the chip+space
as an arg query and re-opened the popover on "No matches". The arg-stage
menu now only opens when the command actually takes args.

* fix(desktop): polish slash arg completion (space/tab/click + typed args)

Unify Enter/Tab/Space accept of the highlighted item at both the command
and arg stages: no-arg commands commit a chip, arg commands expand to
options, and an arg option commits the full `/cmd arg` chip. A fully-typed
arg (which the backend completer drops from suggestions) now commits on
Space/Tab via the verbatim text instead of dead-ending, and the "No
matches" empty state is suppressed past a command's name. Space stays
slash-only so @ mentions keep a literal space.
2026-06-13 18:43:52 +00:00
WompaJango
28bf8fb47d feat(dashboard): clone profiles from any source 2026-06-13 07:33:58 -07:00
Teknium
8cf9d8689d
fix(desktop): keep composer usable during reconnect (#45488)
* feat(cli): add --safe-mode troubleshooting flag

Inspired by Claude Code v2.1.169 (June 2026): run Hermes with all
customizations disabled to isolate setup problems from product bugs.

--safe-mode implies --ignore-user-config and --ignore-rules, and
additionally skips plugin discovery (hermes_cli/plugins.py) and MCP
server loading (tools/mcp_tool.py) via the internal HERMES_SAFE_MODE
env bridge.

* fix(desktop): keep composer usable during reconnect
2026-06-13 02:36:09 -07:00
brooklyn!
b62e57b2f4
Merge pull request #45445 from NousResearch/bb/desktop-stick-to-bottom
fix(desktop): stabilize thread scrolling and session switching
2026-06-13 04:14:49 -05:00
brooklyn!
bdd3868b57
fix(desktop): keep profile color picker open from the context menu (#45489)
Some checks are pending
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Docker Build and Publish / build-amd64 (push) Waiting to run
Docker Build and Publish / build-arm64 (push) Waiting to run
Docker Build and Publish / merge (push) Blocked by required conditions
Lint (ruff + ty) / ruff + ty diff (push) Waiting to run
Lint (ruff + ty) / ruff enforcement (blocking) (push) Waiting to run
Lint (ruff + ty) / Windows footguns (blocking) (push) Waiting to run
Nix Lockfile Fix / auto-fix-main (push) Waiting to run
Nix Lockfile Fix / fix (push) Waiting to run
Nix / nix (macos-latest) (push) Waiting to run
Nix / nix (ubuntu-latest) (push) Waiting to run
OSV-Scanner / Scan lockfiles (push) Waiting to run
Tests / test (1) (push) Waiting to run
Tests / test (2) (push) Waiting to run
Tests / test (3) (push) Waiting to run
Tests / test (4) (push) Waiting to run
Tests / test (5) (push) Waiting to run
Tests / test (6) (push) Waiting to run
Tests / save-durations (push) Blocked by required conditions
Tests / e2e (push) Waiting to run
Typecheck / typecheck (apps/bootstrap-installer) (push) Waiting to run
Typecheck / typecheck (apps/desktop) (push) Waiting to run
Typecheck / typecheck (apps/shared) (push) Waiting to run
Typecheck / typecheck (ui-tui) (push) Waiting to run
Typecheck / typecheck (web) (push) Waiting to run
uv.lock check / uv lock --check (push) Waiting to run
Right-click → Color flashed open then closed: on dismiss the context menu
refocuses its trigger, which doubles as the popover anchor, so the picker
read it as a focus-outside event and closed itself. Suppress the menu's
close auto-focus so the picker survives. Long-press already worked since
it bypasses the menu lifecycle.
2026-06-13 04:00:09 -05:00
Brooklyn Nicholson
76b93869d8 fix(desktop): rebuild thread autoscroll on use-stick-to-bottom 2026-06-13 01:57:30 -05:00
brooklyn!
a856276124
Merge pull request #45414 from NousResearch/bb/fix-desktop-queue-drain-strand
fix(desktop): stop stranding queued prompts across backend bounces
2026-06-13 00:39:13 -05:00
Gille
1e755ff556
fix(desktop): keep recents sorted unless manually reordered (#45404) 2026-06-13 00:38:10 -05:00
Brooklyn Nicholson
7f302c91b2 chore: uptick 2026-06-13 00:33:44 -05:00
Brooklyn Nicholson
bf090deed3 fix(desktop): stop stranding queued prompts across backend bounces
A prompt typed mid-turn ("ghost bubble") could stick forever and never
send when the backend restarted/reconnected during the turn. Two fragile
assumptions in the composer queue drain caused it:

1. Drain fired ONLY on an observed busy true→false edge. A remount/
   reconnect resets `previousBusyRef` to the current busy value, so the
   settle edge is swallowed and the queue never drains. Replace
   `shouldAutoDrainOnSettle` with the edge-independent `shouldAutoDrain`
   (idle + non-empty), driven on the settle edge, on mount/reconnect, and
   after a re-key. The drain lock still serializes sends.

2. The queue is keyed by `queueSessionKey || sessionId`. When a backend
   resume mints a new runtime session id for the same conversation, the
   entry strands under the dead key. Pass the *stable* stored id as
   `queueSessionKey` so the composer can tell runtime churn from a real
   session switch, and `migrateQueuedPrompts` re-keys pending entries on a
   runtime-id change only (never on a deliberate switch).

Also make the drain resilient to a thrown/rejected onSubmit (e.g. a stale-
session 404): the entry stays queued and is retried on the next idle, with
a per-entry attempt cap (MAX_AUTO_DRAIN_ATTEMPTS) to avoid spin-loops and a
quiet toast once it gives up. A manual send clears the backoff.

Tests: composer-queue covers edge-free drain + re-key migration;
use-prompt-actions covers rejected-drain-keeps-entry + idle retry sends.
2026-06-13 00:20:51 -05:00
Brooklyn Nicholson
7c226cc57f perf(desktop): isolate streaming re-renders & cut layout thrash
During a token stream $messages is replaced ~30x/s. Subscribing the whole
chat view to it re-rendered the composer, runtime boundary, and every
message on every delta.

- Derive coarse facts (empty thread? tail is user?) via nanostores
  `computed` atoms so per-token flushes don't re-render their consumers.
- Move the $messages subscription + runtime wiring into a dedicated
  ChatRuntimeBoundary; the composer reads $messages imperatively.
- Drive message rows off stable useAuiState selectors and a lazy
  getMessageText getter instead of eagerly materialized text.
- Feed ResizeObserver entry sizes into measureClamp / FadeText and dedupe
  the style writes, killing the read-write-read reflow cascade.
2026-06-12 21:07:33 -05:00
Brooklyn Nicholson
a1c6349c1f Merge branch 'main' of github.com:NousResearch/hermes-agent into bb/sidebar-workspace-dedup 2026-06-12 19:40:24 -05:00
Brooklyn Nicholson
1a3cd3d436 refactor(desktop): collapse sidebar drag-reorder into one generic ReorderableList
Every reorderable surface (repos, worktrees, sessions, pins) now drops in a
single ReorderableList that owns its own DndContext, so a drag only ever
collides with that list's own items — nesting "just works" without leaking
into the lists around or inside it. This replaces the shared DndContext +
id-prefix dispatch (parent:/group:) whose closestCenter collisions resolved
to a different-typed droppable and silently no-op'd worktree/repo drags.

- Delete groupDndId/parentDndId/parse* helpers and the monolithic
  handleAgentDragEnd/handlePinnedDragEnd; each list persists its new id order
  via a direct typed write (reorderParents/reorderWorktree/reorderSessions/
  reorderPinned).
- Sessions inside repos/worktrees are date-ordered and static (no drag),
  matching the "never reorder on new messages" rule.
- Add setPinnedSessionOrder; drop now-unused reorderPinnedSession.
2026-06-12 18:59:54 -05:00
Brooklyn Nicholson
dd12a5403d refactor(desktop): extract shared WorkspaceHeader for repo + worktree rows
The repo and worktree header rows were ~identical after the handle move.
Fold them into one WorkspaceHeader (emphasis flag for the repo level) plus
a small WorkspaceAddButton, so the toggle/handle/count/+ wiring lives in
one place.
2026-06-12 18:30:49 -05:00
Brooklyn Nicholson
0595af0ad1 feat(desktop): move workspace/worktree drag handle into the leading icon
Mirror the session row: the repo/worktree header's leading glyph (repo
mark, or a new git-branch mark for worktrees) swaps to a grabber on
hover/drag instead of carrying a separate handle on the right — freeing
header width for the label and + button.
2026-06-12 18:26:38 -05:00
Brooklyn Nicholson
e90672696e feat(desktop): worktree-aware sidebar grouping + composer/sidebar UX fixes
Group recents as parent-repo → worktree → sessions using local git
metadata (probed over IPC, with a path-name heuristic fallback for
remote backends). Single-worktree repos collapse to one level. Sessions
order by creation time and never reshuffle on new messages.

Also: fuse the status stack to the composer border, restore icon actions
in the queue panel, fix sidebar label truncation and drag styling, hide
sticky-message attachments while pinned, and bump the terminal font.
2026-06-12 18:18:39 -05:00
brooklyn!
bbf020e709
feat(desktop): follow streaming output at bottom + jump-to-bottom button (#45263)
Strict sticky-bottom autoscroll for the chat thread: while the viewport is
parked at the bottom, the tail follows content growth (streaming tokens, late
measurement, Shiki re-highlight) via a useLayoutEffect keyed on the
virtualizer's own size signal, pinned in the same pre-paint pass as its
scrollToFn so the two never rubber-band. The gate is a single boolean — one
upward pixel (scroll/wheel/touch) disarms follow until the user returns to the
bottom.

Adds a floating jump-to-bottom control that appears once scrolled ~10px away
(above the dim threshold so a sub-pixel settle never flashes it), positioned
above the composer with respect to the status stack, with a subtle
scale + slide in/out animation that honours prefers-reduced-motion. The button
bridges to the virtualizer's re-arm + pin path through a small nanostore
emitter.

Supersedes #43624.
2026-06-12 23:00:11 +00:00
brooklyn!
d62979a6f3
feat(desktop): composer status stack, live subagent windows, editable prompts (#44630)
* feat(desktop): session-scoped status stack + kill new-window theme flash

Stack subagents, background tasks, and the queue into one collapsible
"sink" above the composer, reusing the queue's chrome so every status
reads as one piece. Extracts shared StatusSection / StatusRow /
TerminalOutput primitives and a unified $statusItemsBySession store
(subagents mirrored, background owned here, merged + grouped for render).
Renames BrailleSpinner → GlyphSpinner now that it drives more than braille.

Separately, fix the white flash on every new/cmd-clicked window: macOS
`vibrancy` paints an NSVisualEffectView that follows the OS appearance and
ignores `backgroundColor`, so a dark app on a light-mode Mac flashed white
until the renderer painted over it. Pin `nativeTheme.themeSource` to the
app theme (persisted to userData so cold launches paint right before the
renderer loads), hold windows with `show:false` until `ready-to-show`, and
pre-paint the themed background via an inline script before the bundle runs.

* feat(desktop): dock the slash popover to the composer via one shared fill var

The slash·@ popover (and ? help) now docks onto the composer's edge with the
same chrome as the queue/status stack — rounded outer corners, fused borderless
edge, no shadow — but keeps its own narrow width.

Surface + drawer paint a single --composer-fill var; the state ladder
(rest / scrolled / focused / drawer-open) lives once in styles.css on
[data-slot='composer-root']. The :has() drawer-open rule is last and forces an
opaque fill, since translucent glass sampling different backdrops (thread vs
fade gradient) can never match. This replaces the focus-within !important
override that repainted the surface behind every previous matching attempt.

Also drop the chevron column from the project file tree — the folder open/closed
icon already carries the expand state.

* feat(desktop): base inset for file tree rows (post-chevron alignment)

* feat(desktop): wire the status stack's background tasks to the real process registry

The background group was UI-only (dev-mock seeded). Now it's live e2e:

- tui_gateway: new session-scoped `process.list` (registry snapshot filtered
  by the session's session_key, plus a 4KB output tail for the inline
  terminal viewer) and `process.kill` (single process, ownership-checked —
  unlike process.stop's kill_all).
- Renderer: `reconcileBackgroundProcesses` syncs snapshots into the store
  layout-stably — rows keep their position when state flips (never re-sort),
  new processes append, unchanged rows keep object identity so memoised rows
  skip re-rendering, and a dismissed-set stops the registry's retained
  finished procs from resurrecting X-ed rows.
- Refresh triggers: session open, terminal/process tool.complete,
  status.update(kind=process) from the gateway's notification poller, and a
  5s poll armed only while a running row is visible (catches silent exits).
- Stop = real `process.kill` + optimistic dismiss; Dismiss = client-side
  with resurrection guard.
- Re-keyed the stack to the RUNTIME session id: it was keyed by the stored
  session id, where neither subagent events nor process.list would ever land.
- Deleted dev-status-mocks.ts (__hermesStatusMocks) — no more seed shit.

Reconcile invariants covered in store/composer-status.test.ts.

* feat(desktop): todos + openable subagents in the status stack, self-healing file tree

- todo lists move out of the inline chat panel into the composer status stack
  (checklist icon, dashed ring = pending, spinner = in progress, check = done),
  fed live from todo tool events and seeded from history on session open
- subagent rows carry the child's real session id end-to-end
  (delegate_tool → gateway → renderer) so clicking one opens ITS session window
- status stack publishes its measured height so the thread's bottom clearance
  grows with it; card paints the shared --composer-fill so focused/scrolled
  states match the composer exactly
- file tree self-heals: ENOENT roots retry on a 3s cadence + Try again button,
  and the main process expands ~ in IPC paths (gateway cwds arrive as ~/...)
- composer drag-drop of tree entries inserts inline refs instead of attachments

* fix(desktop): file tree falls back to the workspace dir when a session's cwd is gone

Sessions record their launch cwd; deleted worktrees leave that path dead,
so opening such a session swapped the tree from the default workspace to a
directory that ENOENTs forever — the 3s retry just spun on it. On a root
read error the tree now asks main to sanitize the cwd (prefers the
configured default project dir), displays that fallback, and quietly
re-probes the original path so it switches back if the dir reappears.

* feat(desktop): working restore-checkpoint button on past user prompts

The discard icon on hover of a past user bubble was decorative — clicking
did nothing. It's now a real control: a confirmation dialog explains that
everything after the prompt is removed, then the session rewinds to that
turn and reruns the same prompt (prompt.submit with
truncate_before_user_ordinal, the same mechanism the edit composer uses).
Failures rethrow into the dialog's inline error instead of toasting.

* fix(desktop): show the restore-checkpoint button on the latest user prompt too

Restoring the most recent prompt is just 'retry this turn' — no reason to
exclude it. Stop still takes the slot while the turn is running.

* fix(desktop): finished todo lists clear themselves out of the status stack

A list whose every item is completed/cancelled lingers ~4s so the final
checkmark is visible, then the todo group drops out of the stack. A fresh
active list arriving within the linger cancels the scheduled clear.

* chore(desktop): drop dead editableCheckpoint copy, terser restore confirm

* fix(desktop): rewind clears the abandoned timeline's todos + background

Restoring to (or editing) an earlier prompt rewinds the conversation, but
the todos and background processes spawned by the now-discarded turns kept
showing in the status stack — and the real background processes kept
running. Both rewind paths now clear the session's todo rows and kill +
drop its background processes before the fresh run repopulates them. Also
drops the click-to-edit clamp transition, which flashed a half-expanded
bubble on the way into the edit composer.

* feat(desktop): user messages are always editable; edit/restore revert mid-stream

The bubble is now always click-to-edit — even while a turn streams — instead
of going inert during a run. Sending an edit acts like restore: it rewinds to
that prompt and re-runs with the new text. Both edit and restore can fire
mid-stream now; the gateway refuses prompt.submit while a turn runs (4009
"session busy"), so they interrupt the live turn first and retry the submit
until the cooperative interrupt winds it down. Restore (re-run as-is) shows on
every prompt except the latest running one, which keeps the Stop button.

* fix(desktop): label preview-pane ⌘L selections with the filename, not "zsh"

The terminal owns a global ⌘/Ctrl+L "send selection to composer" shortcut, so
selecting text in the file preview pane and hitting it fell through to the
terminal handler — which imported the right text but labelled the composer ref
"zsh:N lines" off the shell name. When the selection isn't an xterm selection,
label it with the previewed file instead.

* fix(desktop): ⌘L on a preview line selection inserts the @line ref, like dragging

The source preview lets you select lines in the gutter and drag them into the
composer as an @line:path:start-end ref. ⌘/Ctrl+L now does the same when a line
selection is active — it drops the identical ref instead of falling through to
the terminal's global handler (which grabbed the native text selection and sent
a bogus terminal block). Capture-phase + stopPropagation so it wins; with a line
selection there's no native selection, so the terminal handler stays out of it.

* chore: gitignore apps/desktop/demo/ scratch output

The desktop demo prompt writes demo/*.txt during recorded walkthroughs; it's
throwaway, never part of the app. Ignore it so it stops cluttering git status.

* feat(desktop): subagent watch windows, hard stop, sidebar hygiene

Child-session mirror for live subagent windows, delegate sessions tagged
and excluded from the sidebar, composer focus/stop polish, and WS stall
resilience on the gateway transport.

* refactor: DRY delegate SQL + trim status-stack noise

Extract shared listable-child and delegate-delete helpers in hermes_state,
collapse cancelRun busy release, and cut comment bloat in resume/status paths.

* fix(desktop): hide orphaned subagent sessions in sidebar

Cascade-delete all ephemeral children on parent delete (not just tagged rows),
run v16 backfill to tag legacy orphans, and record new delegates as source=subagent.

* fix: restore orphan contract for untagged children + lazy session eviction

Cascade-delete only _delegate_from-tagged rows (v16 backfill covers legacy),
walk marker chains recursively with FK-safe orphaning, gate lazy watch
sessions out of the still-starting eviction exemption via an explicit flag,
pass session_id to _make_agent only when resuming, and hide source=subagent
from session search.

* fix(gateway): gate child mirror off upgraded sessions + age out stale run entries

Review findings: the mirror could interleave synthetic events with a real
native stream once a watch window upgrades (prompt.submit builds an agent),
and a lost subagent.complete left _active_child_runs pinning running=true
forever. Mirror now stops when the live session owns an agent; liveness
reads ignore entries older than an hour.

* fix(gateway): reject prompt.submit into a watch session while its child runs

A lazy watch session's running flag is False (the run lives in the parent
turn), so typing mid-run sailed past the busy guard and built a second agent
racing the in-flight child on the same stored session. Busy error until the
run completes; afterwards the submit upgrades into a normal conversation.

* refactor(gateway): DRY watch-resume payload + compose listable-child SQL

Fold the duplicated child-run busy overlay into one _reuse_live_payload
helper across both resume reuse paths, collapse the twin mirror early-returns,
and build _LISTABLE_CHILD_SQL from _BRANCH_CHILD_SQL instead of restating it.

* fix(desktop): clip horizontal overflow on sidebar scroll areas

Add overflow-x-hidden alongside overflow-y-auto on session list scrollers
and the shared SidebarContent primitive — vertical scroll unchanged.
2026-06-12 08:30:06 -05:00
brooklyn!
24f74eb888
fix(desktop): make file-preview source + markdown selectable (#44648)
body sets user-select:none for native feel and opts text back in only via
[data-selectable-text='true']; the preview's source and rendered-markdown
panes never set it, so code couldn't be selected or copied. Tag the Shiki
code column and the markdown root. The attribute stays off the SourceView
grid root so the gutter keeps its select-none and line numbers don't bleed
into copied text.
2026-06-12 04:15:06 +00:00
Evis
7a2d498b9d fix(desktop): route profile session reads
(cherry picked from commit 64aaf58f5e)
2026-06-11 18:09:24 -05:00
brooklyn!
484f484c25
fix(desktop): carve sidebar nav rows out of the titlebar drag region (#44453)
A WSL2 user reported the top two left-sidebar items being unclickable
while the rest of the UI works. That symptom shape matches an
-webkit-app-region:drag hit-test band eating clicks, not GPU/compositing:
the shell's titlebar drag strips (app-shell.tsx) span the top 34px and
the nav group clears them by only 6px, and drag regions win hit-testing
over DOM regardless of pointer-events. Linux WCO (Electron >=32) is the
newest implementation and has known region quirks (electron#43030).

Apply the same no-drag carve-out the codebase already uses for sticky
user bubbles (USER_BUBBLE_BASE_CLASS in thread.tsx) to the sidebar nav
buttons. Harmless on every platform: the rows were never meant to be
draggable surface.
2026-06-11 15:10:09 -05:00
brooklyn!
8505e9d669
fix(desktop): disable spellcheck on composer inputs (#44415)
Turn off browser spellcheck, autocorrect, and autocomplete on the main chat composer and message-edit composer so code, paths, and slash commands are not flagged or altered.
2026-06-11 18:03:23 +00:00
yoniebans
56a0f48ba6 fix(desktop): tighten remote filesystem wiring 2026-06-11 09:41:35 -07:00
yoniebans
8878484f85 feat(desktop): wire remote filesystem browsing 2026-06-11 09:41:35 -07:00
Brooklyn Nicholson
d7d281fa37 feat(desktop): strict per-thread drafts on decoupled composer
Keyed draft stash (Map + localStorage mirror) behind the live composer:
switching threads stashes the departing draft and restores the entering
one; empty threads show an empty box. Session lifecycle never clears
composer state — the scope swap is the only coupling.

Co-authored-by: mollusk <roger@roger.local>
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
2026-06-11 00:01:06 -05:00
Brooklyn Nicholson
292192f7d7 refactor(desktop): tidy composer draft persistence
- DRY the duplicated submit-restore blocks into dispatchSubmit()
- inline localStorage access (drop browserStorage indirection);
  clearPersistedComposerDraft delegates to write('')
- drop stale per-scope-stash comment in use-session-actions
2026-06-10 23:47:32 -05:00
Brooklyn Nicholson
c710868fbc refactor(desktop): decouple composer from session lifecycle entirely
The composer is a single global surface that sits ABOVE the thread: its
contents follow the user across session switches and are never touched
by session lifecycle. Switching threads doesn't change the render.

Replaces the per-scope draft choreography (scoped storage keys, attachment
stash map, skip-sentinel, restore-on-scope-change effect) with:
- one global localStorage key so an unsent draft survives app reloads
- a one-shot restore on mount
- nothing else — session switches simply don't touch the composer

Verified E2E via CDP with real sidebar clicks + real keystrokes:
typed draft survives A->B->A switching and a full page reload.
2026-06-10 23:39:35 -05:00