mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-13 09:01:54 +00:00
50 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9dbd3c57d7 |
feat(desktop): drag sessions into chat as @session links + spawn loader
Drag a sidebar session into the composer to drop an @session:<profile>/<id> chip the agent resolves via session_search. New READ shape dumps a whole session by id (head+tail when large); a `profile` param reads another profile's DB read-only, and a cross-profile locate scan resolves bare ids when the model drops the owning profile from the link. Also: ASCII "waking up <profile>" overlay during lazy gateway swaps, global haptic rate-limit to kill the reconnect-storm "clickity" buzz, and reauth toasts surfaced once per disconnect instead of every backoff tick. |
||
|
|
a40e20e136 |
feat(desktop): profile rail rename/delete + context-switch polish
- right-click a profile square to rename or delete it, via shared self-contained dialogs (also reused by the profiles page) - switching or creating a profile now resets to a fresh new-session draft so the prior session doesn't stay sticky across contexts - deleting the profile you're currently in falls back to default instead of stranding the gateway on a dead profile - shared ConfirmDialog: Enter/Space confirm from anywhere in the dialog; profile-delete and cron-delete both route through it |
||
|
|
5df732a355 |
feat(desktop): quick-create profile from rail + pin rail on empty sidebar
- Add a "+" in the profile rail that opens a self-contained CreateProfileDialog (name + clone toggle + optional SOUL.md); extract it and ActionStatus from the profiles view so both surfaces share one flow. - Keep the profile rail pinned to the bottom when a profile has no sessions by rendering a flex-1 spacer (previously the rail floated up to the nav). |
||
|
|
b94b3622b5 |
feat(desktop): per-session profile switching + cross-profile sessions
Add first-class profile support to the desktop app without app reloads. - Swap the single live gateway onto a session's profile lazily (spawned on demand by the Electron backend pool), so one backend serves the active profile and others stay cold — no OOM with many profiles. - Aggregate sessions across profiles by reading each profile's state.db read-only; unified "All profiles" view groups sessions per profile with per-profile pagination, while the default view stays scoped to one profile. - Add an Arc-style profile rail at the sidebar foot: a default<->all toggle pinned left, colored named-profile squares scrolling between, Manage pinned right. Profile identity is a deterministic per-name color. - Route profile-scoped REST (config/env/skills/tools/model) to the active gateway profile and invalidate React Query caches on swap. Single-profile users never trigger a swap, so their path is unchanged. Backend: - web_server: profile-aware active/list endpoints + per-profile session totals; hermes_state: session_count(exclude_children); main.py: honor --profile over HERMES_HOME env for pooled backends. UI primitives: - Add a position-aware Tip tooltip (instant, themed) as a drop-in for native title=, and strip redundant tooltips from self-descriptive chrome. |
||
|
|
e003c53b06
|
chore(desktop): zero eslint/typecheck debt + prettier pass (#39100)
- eslint --fix across src/ and electron/ (unused imports, import/prop sort, padding) - flatten empty catch blocks in electron CJS; drop unused applyUpdatesPosixInApp arg - add setMutableRef helper for imperative ref writes (react-compiler clean) - move sidebar cookie persistence into an effect; extract scrollElementToBottom helper |
||
|
|
b1b0f4b668
|
fix(desktop): surface command approval even when its tool is in a collapsed group (#38829)
The desktop command-approval ApprovalBar renders inline inside ToolEntry, which lives inside ToolGroupSlot. When 2+ tools group, the group body is hidden until expanded, so an approval raised by a pending terminal/ execute_code call was buried behind "Tool actions · N steps" and required manual expansion to act on (sudo/secret were unaffected — they use modal overlays). ToolGroupSlot now subscribes to $approvalRequest and force-opens its body while an approval targeting one of its pending approval-eligible tools is in flight, so the inline controls surface with nothing expanded. The group reverts to the user's stored collapse state once the approval resolves. |
||
|
|
9cbc37e25b
|
feat(desktop): dedicated Providers settings + polished Accounts/API-keys UX (#38551)
* feat(desktop): dedicated Providers settings with Accounts/API-keys subnav Rework provider configuration in the desktop app into its own Providers page that mirrors the first-run onboarding picker, instead of burying provider keys in the generic Tools & Keys list. - Add a Providers settings page (providers-settings.tsx) reusing the onboarding picker cards/ApiKeyForm so the two surfaces stay identical - Add a sidebar subnav (Accounts vs API keys) backed by a deep-linkable `pview` URL param; nested OverlayNavItem variant for a lighter active state so children don't compete with the parent item - Scope provider search to the active sub-view in its native card format (no more accordion fallback); collapse the API-key grid to the top providers behind a "Show all" toggle to cut scrolling - Launch real in-app OAuth from settings via startManualProviderOAuth; fix the misleading red "reason" banner that showed during an active connect (neutral style, hidden during a flow, omitted for direct per-provider launches) - Expand PROVIDER_GROUPS and add longest-prefix matching so providers like xAI/Ollama group correctly instead of landing under "Other" - Drop redundant messaging API keys from Tools & Keys (channel_managed) Co-authored-by: Cursor <cursoragent@cursor.com> * feat(desktop): Cursor-style provider key list with inline inputs Replace the card-grid API-key form on the Providers page with a per-provider list (mirrors Cursor's API keys section): - One row per vendor with its primary key input inline; rows with extra vars (base URL, region, alt tokens) expand to reveal those on focus - Set keys show their redacted value as the placeholder; Save appears on edit, Remove on a set key - Hide redundant alias key fields (e.g. ANTHROPIC_TOKEN vs ANTHROPIC_API_KEY) unless already set, and label set aliases by env var name so they're unambiguous - Smaller mono input text + compact height Co-authored-by: Cursor <cursoragent@cursor.com> * style(desktop): flatten providers settings UI chrome Tighten the providers settings surface to match the newer desktop style: remove extra card rails/borders in API-key rows, reduce visual noise in the providers subnav, replace bespoke link-like controls with shared text-button variants, and improve key input readability. * feat(desktop): rework providers settings UI - Flatten the shared OAuth picker rows (accounts + onboarding): drop the rounded-2xl/border cards for flat hover-bg rows; Nous hero keeps a subtle tint plus an animated blue→purple arc border. - Key fields collapse to a single input: a set key reads read-only (redacted) and edits in place on focus/click — no Replace/Cancel chrome. Save on type, Esc cancels (without closing the overlay), "Remove or esc to cancel" hint. - Non-key overrides render boxless, content-sized (field-sizing) and right-anchored; advanced fields align under the primary key column. - Add `xs` control size; size fields via padding (no fixed heights). - Cards expand on key-input focus; chevron shows on hover/expanded; expanded state uses a ring + softer bg tier so hover ≠ focus. - Relocate "Get a key" to the bottom-right of the expanded panel; drop the redundant provider description. - Cmd+K: add Providers (accounts) and Provider API keys deep-links. * fix(desktop): flatten provider fields, drop input shadows, fix Cmd+K provider rank - KeyField: collapse to one stacked label-above-input form field (drop the bespoke `naked`/inline/column branches); empty advanced overrides fade until hover/focus/set - styles: kill the resting + focus drop shadow on shared input chrome so form inputs sit flat (composer keeps its own shadow) - Cmd+K: drop stray `providers` keyword from Skills & Tools so the Providers settings entry ranks first for "provider" * fix(desktop): nous portal arc blue → orange * fix(desktop): rank appearance above settings in Cmd+K --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com> |
||
|
|
3c163cb035
|
feat(desktop): background needs-input indicator, clarify redesign, Cmd+K palette & UI consistency pass (#38631)
* fix(desktop): surface background-session clarify prompts instead of hanging
clarify.request is a one-shot blocking event: the gateway turn blocks on
clarify.respond. The desktop handler dropped it for any non-focused session
(`if (!isActiveEvent) return`) and stored at most one request in a single
global atom, so a background session that asked a clarifying question hung
forever and re-focusing it could never recover (the event was already gone).
- store/clarify.ts: key pending requests by runtime session id; expose the
active session's request via a focus-scoped computed view (ClarifyTool is
unchanged). clearClarifyRequest takes an optional session id for targeted
clears, with a request-id fallback.
- use-message-stream.ts: park every session's clarify (drop the isActiveEvent
early return); toast when one lands for a background session since the row
otherwise just keeps spinning like normal work.
- clarify-tool.tsx: clear by session id so answering one chat can't wipe
another's pending request.
- store/clarify.test.ts: concurrent independence, focus-scoped view,
targeted/stale/fallback clears.
* feat(desktop): persistent needs-input indicator + icon button consolidation
Replace the background-clarify toast (expired on alt-tab, easy to miss) with a
persistent, glowing amber "needs input" dot on the session's sidebar row,
driven off a new ClientSessionState.needsInput flag mirrored into a
$attentionSessionIds store. The flag is set on clarify.request and cleared the
moment the turn resumes (tool.complete) or ends.
Also: redesign the clarify tool UI (borderless choices, pseudo-radio dots,
right-aligned checkmark, arc border, tighter padding), make Button the single
source of icon-button styling (4px radius, new icon-titlebar variant, titlebar
buttons rendered polymorphically via asChild, Codicons throughout), put the
file-tree refresh action first, and .trim() pasted composer text.
* style(desktop): padding-driven, square non-icon buttons
Default button sizing was vanilla-shadcn chunky (fixed h-9, 16px padding) and
inconsistent with the icon-button radius pass. Size text variants by
padding + line-height instead of fixed heights so they stay snug and scale
with content, and drop the radius on non-icon buttons (icon buttons keep the
shared 4px). Move the update-overlay CTAs off a hardcoded h-10 onto the
padding-based lg variant. Composer and the inline approval strip are untouched.
* style(desktop): shrink button scale, flush overlay sidebar, variant-ize stray buttons
- Buttons: smaller default font (14px -> 13px) and tighter padding-driven sizes
across every variant; the chunky shadcn scale read as oversized in a dense
desktop UI.
- Overlay split layout (settings / command center): the shared OverlayView top
padding left the card surface showing as a gap above the sidebar. Move the
titlebar clearance into each column so the sidebar background runs flush to
the card's top edge.
- Consolidate buttons that hardcoded size/radius/font onto the proper size
variants (tooltip-icon-button, overlay close, cron IconAction, SidebarTrigger,
gateway system button, session-row actions radius, title chip radius, release
notes link) so styling flows from variant props, not per-call overrides.
Composer and the inline approval strip are intentionally left as-is.
* style(desktop): 12px button text, drop sparkle decoration + redundant settings titles
- Button base font down to 12px (text-xs) for the dense desktop scale.
- Remove the decorative Sparkles glyph from the model "Apply" button (keep the
spinner while applying).
- Drop the page-level section titles that just restate the left nav ("Main
model", "Appearance", "MCP servers") — the sidebar already labels the pane.
Sub-section headings (Auxiliary models, LLM providers, etc.) stay.
* feat(desktop): add boxless `text` button variant; use for aux-model actions
New reusable `text` variant renders a button as inline label text (no
bg/border, muted -> foreground, underline-on-hover affordance). Emphasize the
actionable word by adding `font-semibold`/`underline` at the call site. Applied
to the auxiliary-model "Set to main" (plain), "Change" and "Reset all to main"
(bold + underlined) actions, replacing the boxed ghost/outline buttons.
* style(desktop): nudge button scale up + 2.5px radius on non-icon buttons
Bump default/sm vertical padding a step (the 12px pass read too small) and give
non-icon buttons a subtle 2.5px radius instead of square corners. Icon buttons
keep their 4px.
* style(desktop): unify Input/Textarea/SelectTrigger on shared controlVariants
Mirror the buttonVariants exercise for non-composer form controls: add a
single controlVariants source of truth (2.5px radius, 12px text,
padding-driven sizing, chrome via desktop-input-chrome) and consume it from
Input, Textarea, and SelectTrigger. Drop per-call radius/height/font
overrides that fought the shared look.
* style(desktop): flatten appearance settings — drop card-in-card sections
Remove the outer card chrome (border/bg/shadow/rounded) wrapping each
appearance section so they're flat headings + option grids instead of
boxes nested inside boxes, matching the other settings pages.
* style(desktop): de-box appearance options into flat rows + bare theme swatches
Color Mode and Tool Call Display become flat radio-style rows (no tile
border/fill, no inner icon box, no filled check badge — just a subtle active
bg and a check). Theme drops its outer card wrapper so only the preview
swatch shows, with a primary ring marking the active palette.
* style(desktop): primitive-level pointer cursor + borderless settings lists
Add a base-layer rule giving every interactive control (button, select,
menu item, switch, tab, summary) cursor:pointer, and strip the now-redundant
hardcoded cursor-pointer from those elements (plain clickable divs/labels
keep theirs). Remove the divide-y separators from settings list sections so
they breathe.
* style(desktop): Color Mode + Tool Call Display as one-row segmented controls
Replace the vertical option-row lists with a compact SegmentedControl
(grouped pill buttons on a single track), dropping the per-option
descriptions since the section subtitle already covers the context.
* style(desktop): drop redundant On/Off label next to boolean config switches
The switch already communicates state, so the text label was noise.
* style(desktop): add Switch xs size; move appearance controls inline-right
Add an xs size variant to the Switch primitive and use it for the provider
edit submenu toggles. In appearance settings, drop the redundant selection
Pills (the UI already shows the active choice), move the Color Mode and Tool
Call Display segmented controls into the section header's right side
(responsive: stacks under the heading on narrow widths), and shrink the
segmented control.
* feat(desktop): titlebar toggle to flip sidebar sides
Adds a top-left swap button (replacing the search icon) that mirrors the
layout: sessions sidebar ↔ file browser + preview rail. Persisted via
$panesFlipped. The left/right sidebar toggles, content inset, and pane
borders all follow the active side so the buttons stay accurate after a flip.
* feat(desktop): global Cmd+K palette + UI consistency overhaul
Builds on the clarify/needs-input work with a cross-cutting pass to make
the desktop surfaces feel like one app.
- Global Cmd+K command palette (cmdk): nav, settings deep-links, async
API-key / MCP-server / archived-session groups, reusable theme sub-page
(light/dark groups, stays open on pick), loop nav, fuzzy match. Replaces
per-page settings search.
- Shared SearchField: borderless, underline-on-focus, `field-sizing`
auto-width. Unifies sessions sidebar, pages, overlays, command center,
cron; drops bespoke OverlaySearchInput.
- Cron & Profiles converted to OverlayView; flat token-driven panels
(no card-in-card / divider borders) matching command center.
- `r` refresh hotkey via useRefreshHotkey; drop the visible refresh buttons.
- Button text/textStrong link variants applied across settings & views;
shared PAGE_INSET_X content gutters.
- Math/ascii loaders replace "Loading…" text placeholders; x-icon close
over text "Close"; cursor-pointer at the dropdown/select primitive level.
* style(desktop): tidy root error-boundary actions
Reload window → text link, Open logs pushed right (ml-auto), and the
error message box drops the oversized rounded-2xl for rounded-md.
* style(desktop): fix profiles sidebar — header + add-icon, drop text-link
The full-width `text` New-profile button drew an underline under the +
glyph on hover (text-decoration spans the icon). Replace with a proper
"PROFILES" section header + ghost add-icon button, matching the chat
sidebar's header/new-item pattern.
* style(desktop): kill focus rings globally
Tab/focus showed Tailwind's `focus-visible:ring-*` (a box-shadow) plus the
native outline. Drop both via an unlayered reset that nulls --tw-ring-*;
the composer / input soft-glow is untouched (those use direct box-shadows).
* style(desktop): shared Badge component; tidy profile metadata
Add a proper shadcn-style Badge (CVA tones, app radius — not a full pill)
and use it for the Default/.env tags instead of bespoke rounded-full spans.
Drop the oversized text-sm metadata values to text-xs.
* style(desktop): migrate bespoke pills to shared Badge; tidy cron/titlebar
- Sidebar toggles in the titlebar no longer carry an active highlight —
they're plain show/hide affordances now.
- Replace every bespoke rounded-full status pill (cron, messaging,
settings, skills) with the shared Badge (adds a `warn` tone). App radius,
one component.
- Cron row actions use Codicons (play/debug-pause/zap/edit/trash) to match
the rest of the chrome instead of stray lucide glyphs.
* style(desktop): drop active background on titlebar actions
Mute/haptics state reads from the icon glyph (and aria-pressed) — no
background highlight on any titlebar action.
* style(desktop): tighten error-boundary action gap
gap-4 → gap-2.5 between Try again / Reload window.
* style(desktop): hide search when there's nothing to search
Empty datasets no longer render a search field. Adds a `searchHidden` prop
to PageSearchShell (artifacts/skills/messaging) and gates cron + command
center sessions search on a non-empty list. The chat sidebar already did
this via showSessionSections.
* fix(desktop): composer wraps long text & expands at the real wrap point
Long unbroken input ran off horizontally and the stacked layout flipped
on a char-count guess (too early). Add wrap rules to the contentEditable
and drive expansion off the editor's actual rendered height via the
resize observer, so it stacks exactly when the text wraps to a 2nd line.
* feat(desktop): composer/intro polish + shared ErrorState
- Composer single-line row centers (was bottom-aligned); placeholder
randomizes per session (starter vs follow-up) without mid-stream flip.
- Drop chat header on brand-new sessions (dead label + border).
- ⌘N flashes its sidebar hint; ⌘. toggles the command center.
- Intro wordmark fills width (drop 8rem fit cap).
- Unify error states on a shared ErrorState component (boundary + updates).
* style(desktop): satisfy lint across PR-touched files
* refactor(desktop): DRY/elegance pass over PR-touched files
- Shared useDeepLinkHighlight hook collapses 3 near-identical settings
deep-link effects (keys/mcp); config kept inline (distinct bail-clear).
- command-center: table-driven SECTION_ICONS + single errorText helper.
- clarify-tool: OPTION_ROW_CLASS + RadioDot extracted from option rows.
- desktop-controller: merge Cmd+K / Cmd+. into one keydown handler.
- statusbar-controls: hoist shared action class.
- Misc: drop redundant cn()/cursor-pointer/dead fields; tidy switch.
* feat(desktop): Cmd+K jumps to sessions; drop API-key entries
Add active sessions to the palette (fuzzy jump-to-chat), remove the
low-value per-API-key entries, and move the lazy palette sources
(config/sessions/archived) to react-query instead of hand-rolled
useState + effect fetching. Hoist the shared nav helper.
|
||
|
|
bc9e33d66b |
refactor(desktop): DRY/elegance pass over PR-touched files
- Shared useDeepLinkHighlight hook collapses 3 near-identical settings deep-link effects (keys/mcp); config kept inline (distinct bail-clear). - command-center: table-driven SECTION_ICONS + single errorText helper. - clarify-tool: OPTION_ROW_CLASS + RadioDot extracted from option rows. - desktop-controller: merge Cmd+K / Cmd+. into one keydown handler. - statusbar-controls: hoist shared action class. - Misc: drop redundant cn()/cursor-pointer/dead fields; tidy switch. |
||
|
|
38acced687 | style(desktop): satisfy lint across PR-touched files | ||
|
|
5bb7156949 |
feat(desktop): composer/intro polish + shared ErrorState
- Composer single-line row centers (was bottom-aligned); placeholder randomizes per session (starter vs follow-up) without mid-stream flip. - Drop chat header on brand-new sessions (dead label + border). - ⌘N flashes its sidebar hint; ⌘. toggles the command center. - Intro wordmark fills width (drop 8rem fit cap). - Unify error states on a shared ErrorState component (boundary + updates). |
||
|
|
9e02b18828 |
style(desktop): tighten error-boundary action gap
gap-4 → gap-2.5 between Try again / Reload window. |
||
|
|
e026fd88cd |
style(desktop): migrate bespoke pills to shared Badge; tidy cron/titlebar
- Sidebar toggles in the titlebar no longer carry an active highlight — they're plain show/hide affordances now. - Replace every bespoke rounded-full status pill (cron, messaging, settings, skills) with the shared Badge (adds a `warn` tone). App radius, one component. - Cron row actions use Codicons (play/debug-pause/zap/edit/trash) to match the rest of the chrome instead of stray lucide glyphs. |
||
|
|
fd88d527af |
style(desktop): shared Badge component; tidy profile metadata
Add a proper shadcn-style Badge (CVA tones, app radius — not a full pill) and use it for the Default/.env tags instead of bespoke rounded-full spans. Drop the oversized text-sm metadata values to text-xs. |
||
|
|
311e80809f |
style(desktop): tidy root error-boundary actions
Reload window → text link, Open logs pushed right (ml-auto), and the error message box drops the oversized rounded-2xl for rounded-md. |
||
|
|
ac9de2e80c |
feat(desktop): global Cmd+K palette + UI consistency overhaul
Builds on the clarify/needs-input work with a cross-cutting pass to make the desktop surfaces feel like one app. - Global Cmd+K command palette (cmdk): nav, settings deep-links, async API-key / MCP-server / archived-session groups, reusable theme sub-page (light/dark groups, stays open on pick), loop nav, fuzzy match. Replaces per-page settings search. - Shared SearchField: borderless, underline-on-focus, `field-sizing` auto-width. Unifies sessions sidebar, pages, overlays, command center, cron; drops bespoke OverlaySearchInput. - Cron & Profiles converted to OverlayView; flat token-driven panels (no card-in-card / divider borders) matching command center. - `r` refresh hotkey via useRefreshHotkey; drop the visible refresh buttons. - Button text/textStrong link variants applied across settings & views; shared PAGE_INSET_X content gutters. - Math/ascii loaders replace "Loading…" text placeholders; x-icon close over text "Close"; cursor-pointer at the dropdown/select primitive level. |
||
|
|
40420a619b
|
fix(desktop): attachments on Enter, IME composition, scroll, fetchJson resets (salvage #38502) (#38677)
* fix(desktop): critical fixes — attachments, IME composition, scroll, fetchJson
DC2: Pass attachments to onSubmit() on direct Enter submit and call
clearComposerAttachments(). Previously attachments were silently
dropped — only text was sent while attachment pills remained visible.
DH1: Add 'open' to ThinkingDisclosure ResizeObserver effect deps.
When the disclosure toggles, refs point to new DOM but the observer
wasn't reattached, breaking live-scroll preview after expand/collapse
and leaking detached DOM nodes.
DH3+DH4: Add composition tracking via composingRef (set by
compositionstart/compositionend). Guards handleEditorInput (skip
preedit state writes), handleEditorKeyDown (prefer composingRef over
unreliable isComposing), and form onSubmit (prevent IME Enter from
triggering submission). Fixes IME Enter message splitting and preedit
text leaking into app state on CJK input.
DH6: Add res.on('error', reject) to fetchJson response stream.
Without this, a TCP reset mid-transfer left the promise hanging forever,
freezing the desktop UI.
All TypeScript compiles cleanly.
* chore: add copii.list@gmail.com to AUTHOR_MAP (stremtec)
* fix(desktop): prevent scroll snap-back during streaming, atomic config writes
DH2: Defer pinToBottom() in useLayoutEffect to rAF so that browser
scroll/wheel events from the current frame are processed first.
Previously an immediate pinToBottom() could snap the viewport back
to bottom against the user's trackpad scroll-up intent during
streaming — the wheel event hadn't fired yet so stickyBottomRef was
still true.
DH7: Add writeFileAtomic() helper (write to .tmp then rename) and
use it in writeDesktopConnectionConfig, writeDesktopUpdateConfig,
and writeBootstrapMarker. Prevents partial writes on crash/power
loss that would corrupt JSON config files, requiring manual repair.
* fix(desktop): guard nativeTheme listener from duplicates, invalidate connection config cache
DM9: Guard nativeTheme.on('updated') with a one-shot flag so that
multiple createWindow() calls (e.g. macOS activate after all windows
closed) don't accumulate duplicate listeners on the process-wide
singleton.
DM3: Add mtime-based cache invalidation to readDesktopConnectionConfig.
Previously the cache was populated once and never invalidated — if an
external tool modified connection.json, the desktop ignored the change
until restart. Now re-reads when the file's mtime differs.
* fix(desktop): widen fetchJson res.on('error') to sibling fetch + sort JSX props
Follow-up to salvaged #38502:
- resourceBufferFromUrl had the same mid-stream-reset hang class as
fetchJson (req.on('error') present, res.on('error') missing). Add the
response-stream error handler so a TCP reset during body read rejects
instead of leaving the promise unsettled.
- Sort the new onComposition* JSX props to satisfy perfectionist/sort-jsx-props
(was an introduced eslint error in the composer).
---------
Co-authored-by: asill-livestream <copii.list@gmail.com>
|
||
|
|
75e29f97ee |
style(desktop): add Switch xs size; move appearance controls inline-right
Add an xs size variant to the Switch primitive and use it for the provider edit submenu toggles. In appearance settings, drop the redundant selection Pills (the UI already shows the active choice), move the Color Mode and Tool Call Display segmented controls into the section header's right side (responsive: stacks under the heading on narrow widths), and shrink the segmented control. |
||
|
|
f15d2cb5e4 |
style(desktop): primitive-level pointer cursor + borderless settings lists
Add a base-layer rule giving every interactive control (button, select, menu item, switch, tab, summary) cursor:pointer, and strip the now-redundant hardcoded cursor-pointer from those elements (plain clickable divs/labels keep theirs). Remove the divide-y separators from settings list sections so they breathe. |
||
|
|
0776d1b19c |
style(desktop): unify Input/Textarea/SelectTrigger on shared controlVariants
Mirror the buttonVariants exercise for non-composer form controls: add a single controlVariants source of truth (2.5px radius, 12px text, padding-driven sizing, chrome via desktop-input-chrome) and consume it from Input, Textarea, and SelectTrigger. Drop per-call radius/height/font overrides that fought the shared look. |
||
|
|
d6e2c940e9 |
style(desktop): nudge button scale up + 2.5px radius on non-icon buttons
Bump default/sm vertical padding a step (the 12px pass read too small) and give non-icon buttons a subtle 2.5px radius instead of square corners. Icon buttons keep their 4px. |
||
|
|
fb0250ef63 |
feat(desktop): add boxless text button variant; use for aux-model actions
New reusable `text` variant renders a button as inline label text (no bg/border, muted -> foreground, underline-on-hover affordance). Emphasize the actionable word by adding `font-semibold`/`underline` at the call site. Applied to the auxiliary-model "Set to main" (plain), "Change" and "Reset all to main" (bold + underlined) actions, replacing the boxed ghost/outline buttons. |
||
|
|
1e1ab31ad6 |
style(desktop): 12px button text, drop sparkle decoration + redundant settings titles
- Button base font down to 12px (text-xs) for the dense desktop scale.
- Remove the decorative Sparkles glyph from the model "Apply" button (keep the
spinner while applying).
- Drop the page-level section titles that just restate the left nav ("Main
model", "Appearance", "MCP servers") — the sidebar already labels the pane.
Sub-section headings (Auxiliary models, LLM providers, etc.) stay.
|
||
|
|
8c0f15478d |
style(desktop): shrink button scale, flush overlay sidebar, variant-ize stray buttons
- Buttons: smaller default font (14px -> 13px) and tighter padding-driven sizes across every variant; the chunky shadcn scale read as oversized in a dense desktop UI. - Overlay split layout (settings / command center): the shared OverlayView top padding left the card surface showing as a gap above the sidebar. Move the titlebar clearance into each column so the sidebar background runs flush to the card's top edge. - Consolidate buttons that hardcoded size/radius/font onto the proper size variants (tooltip-icon-button, overlay close, cron IconAction, SidebarTrigger, gateway system button, session-row actions radius, title chip radius, release notes link) so styling flows from variant props, not per-call overrides. Composer and the inline approval strip are intentionally left as-is. |
||
|
|
712bf4d8e4 |
style(desktop): padding-driven, square non-icon buttons
Default button sizing was vanilla-shadcn chunky (fixed h-9, 16px padding) and inconsistent with the icon-button radius pass. Size text variants by padding + line-height instead of fixed heights so they stay snug and scale with content, and drop the radius on non-icon buttons (icon buttons keep the shared 4px). Move the update-overlay CTAs off a hardcoded h-10 onto the padding-based lg variant. Composer and the inline approval strip are untouched. |
||
|
|
35a750eedd |
feat(desktop): persistent needs-input indicator + icon button consolidation
Replace the background-clarify toast (expired on alt-tab, easy to miss) with a persistent, glowing amber "needs input" dot on the session's sidebar row, driven off a new ClientSessionState.needsInput flag mirrored into a $attentionSessionIds store. The flag is set on clarify.request and cleared the moment the turn resumes (tool.complete) or ends. Also: redesign the clarify tool UI (borderless choices, pseudo-radio dots, right-aligned checkmark, arc border, tighter padding), make Button the single source of icon-button styling (4px radius, new icon-titlebar variant, titlebar buttons rendered polymorphically via asChild, Codicons throughout), put the file-tree refresh action first, and .trim() pasted composer text. |
||
|
|
72f556dfc4 | Merge remote-tracking branch 'origin/main' into bb/desktop-background-clarify | ||
|
|
58eb473baa |
fix(desktop): surface background-session clarify prompts instead of hanging
clarify.request is a one-shot blocking event: the gateway turn blocks on clarify.respond. The desktop handler dropped it for any non-focused session (`if (!isActiveEvent) return`) and stored at most one request in a single global atom, so a background session that asked a clarifying question hung forever and re-focusing it could never recover (the event was already gone). - store/clarify.ts: key pending requests by runtime session id; expose the active session's request via a focus-scoped computed view (ClarifyTool is unchanged). clearClarifyRequest takes an optional session id for targeted clears, with a request-id fallback. - use-message-stream.ts: park every session's clarify (drop the isActiveEvent early return); toast when one lands for a background session since the row otherwise just keeps spinning like normal work. - clarify-tool.tsx: clear by session id so answering one chat can't wipe another's pending request. - store/clarify.test.ts: concurrent independence, focus-scoped view, targeted/stale/fallback clears. |
||
|
|
f66a929a6b
|
fix(desktop): render approval/sudo/secret prompts so tools stop silently timing out (#38578)
* fix(desktop): render approval/sudo/secret prompts so tools stop silently timing out The desktop app's gateway event handler (use-message-stream.ts) handled clarify.request but had no case for approval.request, sudo.request, or secret.request. When a tool needed approval, the gateway emitted approval.request and blocked the agent thread in _await_gateway_decision() for up to 5 min (approvals.gateway_timeout); the desktop dropped the unknown event, never showed a dialog, then the agent returned BLOCKED. No prompt, just a stall then a block. The Ink TUI already handles all three (createGatewayEventHandler.ts); this brings the Electron app to parity. - store/prompts.ts: approval/sudo/secret atoms (+ request-id-guarded clears) - components/prompt-overlays.tsx: Radix dialogs; close/Esc maps to refusal so silence is never mistaken for consent (parity with TUI Esc->deny) - use-message-stream.ts: wire the three *.request cases; clearAllPrompts on message.complete so an overlay can't outlive its turn - chat-messages.ts: GatewayEventPayload gains command/description/env_var/prompt - mount PromptOverlays in the chat shell * feat(desktop): inline tool-call approval bar (Cursor-style "Run") Render dangerous-command / execute_code approval inline on the pending tool row instead of as a modal. Binding is positional: the desktop tool.start payload carries no structured args, but approval.request only fires from the terminal/execute_code guards and the agent blocks on one approval at a time, so the single pending row of those tools is the one that raised it. Command/description text comes from $approvalRequest. Drops ApprovalDialog from PromptOverlays (sudo/secret stay modal). * style(desktop): make inline approval bar match Cursor's command card Drop the amber alert styling for a neutral elevated card: command on a terminal-prefixed row up top, a divided footer with the muted description on the left and right-aligned controls — a ghost "Reject" (Esc) plus a split primary "Run" (⌘⏎) whose chevron opens "Allow this session" / "Always allow" / "Reject". Wire ⌘/Ctrl+Enter → Run and Esc → Reject to match Cursor's accept/skip bindings, guarded against double-send via the $approvalRequest atom. * style(desktop): shrink inline approval to a tiny Cursor-style button strip The running tool row already shows the command, so drop the whole card + command echo + description band. What's left is a compact strip under the row: a small split "Run ⌘⏎" button (chevron → Allow this session / Always allow / Reject) and a ghost "Reject Esc", indented to sit under the row's title text. * style(desktop): drop the loud blue Run button for a quiet outlined control Swap the primary (blue) Run for a subtle outlined split control — neutral border, transparent fill, hover-accent — so the approval strip reads as quiet inline affordance rather than a big CTA. Reject stays ghost. * style(desktop): make Run a soft primary badge Tint the Run split control with the primary color as a badge (bg-primary/10, primary text, primary/25 border, rounded-md, hover primary/15) instead of a solid CTA or a neutral outline. * style(desktop): slim the approval chevron and space out Reject The chevron button had ballooned because dropping the size prop fell back to the big default size (h-9 + has-svg px-3). Pin size=xs everywhere and give the chevron a tight w-5/px-0. Bump the gap between the Run badge and Reject (gap-2.5) and loosen Reject's internal spacing. * feat(desktop): confirm before "Always allow" persists an approval "Always allow" writes the matched pattern to ~/.hermes/config.yaml and suppresses the prompt in every future session — too consequential to fire straight from a menu click. Route it through a confirm dialog that names the pattern + command and the file it touches. The dialog owns the keyboard while open so Esc closes it instead of denying the approval. * fix(gateway): make sudo + secret prompts actually fire in the desktop Tek's PR added the sudo/secret overlays and callback wiring, but neither reached the live path: - Sudo: the sudo password callback is thread-local (terminal_tool _callback_tls), and _wire_callbacks runs on the agent-build thread, not the turn thread that executes tools. At command time the callback was missing, so terminal sudo fell through to /dev/tty and hung the headless gateway. Re-wire callbacks at the top of the prompt-submit turn thread. - Secret: skills_tool short-circuited to the "secret entry unsupported" hint for any gateway surface, before invoking the callback. Interactive surfaces (desktop/TUI) register a secret-capture callback that routes to the secret.request overlay; only short-circuit when no callback exists, so messaging still gets the hint but the desktop prompts. * docs(desktop): drop Cursor references from approval comments * docs(desktop): drop Cursor reference from prompt-overlays comment * fix(skills): gate in-band secret capture on HERMES_INTERACTIVE, not callback presence The desktop/sudo PR switched the gateway secret-capture short-circuit from "any gateway surface" to "gateway surface with no callback registered". That made a messaging gateway (telegram/discord/...) attempt interactive in-band secret capture whenever any callback happened to be registered, instead of returning the safe "setup unsupported" hint — and broke test_gateway_still_loads_skill_but_returns_setup_guidance. Discriminate on HERMES_INTERACTIVE instead: the desktop app / TUI set it in _enable_gateway_prompts (alongside registering the secret.request callback), while messaging platforms never do. This is the same flag tools/approval.py uses to tell an interactive surface from a messaging one, so messaging keeps the hint and desktop/TUI still prompt. --------- Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com> |
||
|
|
d50741af90
|
fix(onboarding): clarify Anthropic API vs OAuth provider entries and reorder (#38577)
The setup-flow provider list showed two Anthropic/Claude entries with
ambiguous labels ('Anthropic (Claude API)' and 'Claude Code (subscription)')
in no deliberate order. Relabel and reorder so the distinction and the
subscription caveat are explicit:
- 'Anthropic API Key' (PKCE, API path)
- 'Anthropic OAuth: Required Extra Usage Credits to Use Subscription' (external)
- Both Anthropic entries moved to the bottom of the list.
- 'OpenAI Codex (ChatGPT)' -> 'OpenAI OAuth (ChatGPT)', now first after Nous.
Applied consistently to the backend OAuth catalog (web_server.py) and the
desktop onboarding overlay's PROVIDER_DISPLAY title/order map; test
assertions updated to the new titles.
|
||
|
|
7ea37cd082 |
fix(desktop): stop validating provider keys in launch setup
The launch provider setup screen rejected too many legitimate users:
a live credential probe ("key rejected"), a post-save runtime check
("still cannot reach X"), and an 8-char minimum all gated progression.
Corporate proxies, regional blocks, rate-limited/flaky probes, and
self-hosted endpoints all tripped these. Now we just require a
non-empty value and save it; a genuinely bad key surfaces later at
chat time instead of blocking onboarding.
|
||
|
|
0caa23788f
|
fix(desktop): prevent IME Enter from splitting messages and viewport resize from disarming scroll anchor (#38333)
* fix(desktop): prevent IME Enter from splitting messages and viewport resize from disarming scroll anchor Two fixes for the Hermes Desktop composer: 1. IME composition Enter was treated as message submission. When a Korean/ Japanese/Chinese IME is composing text and the user presses Enter to finalise the preedit, handleEditorKeyDown fired submitDraft() because it did not check event.nativeEvent.isComposing. The assistant-ui hidden textarea already guards this correctly; the custom contentEditable handler was missing it. Added an early return when isComposing is true. 2. Viewport resize (composer expand/collapse, window resize) was disarming the scroll sticky-bottom anchor. When the composer grows, the thread viewport shrinks, the browser adjusts scrollTop down to keep content visible, and the onScroll handler misread this as a user scroll-up. Added lastClientHeightRef tracking so the disarm condition now requires BOTH stable scrollHeight AND stable clientHeight before treating a scrollTop decrease as user intent. Fixes: random mid-message sends during IME typing; scroll jumps when the composer resizes or the window changes size. * fix(desktop): prevent virtualizer measurement adjustments from fighting scroll anchoring The virtualizer's measureElement callbacks trigger scroll adjustments when item sizes differ from estimates. These fight our ResizeObserver + pinToBottom loop, creating visible rubber-banding (view snaps to composer then jumps back up), even during idle. Three changes: 1. React.memo on VirtualizedThread to stop parent re-renders cascading 2. Shared stickyBottomRef so scrollToFn can check bottom state 3. scrollToFn override: skip adjustments when user is at bottom * fix(desktop): use stable useCallback ref instead of inline arrow for onBranchInNewChat The inline arrow `messageId => void branchInNewChat(messageId)` created a new function reference on every render. This cascaded through: desktop-controller → ChatView → Thread → useMemo([...onBranchInNewChat]) → new messageComponents object → VirtualizedThread receives new prop → React.memo overridden → virtualizer recalculates → measurement adjustments trigger scroll jumps at the 15-second useStatusSnapshot interval. Pass the already-useCallback'd branchInNewChat directly. * fix(desktop): use ctrlEnter submitMode on hidden textarea + gate ResizeObserver on isRunning Two root-cause fixes: 1. IME message splitting: The hidden ComposerPrimitive.Input textarea had submitMode='enter' (default), so any Enter keydown it received — even during IME composition — triggered form.requestSubmit(). Changed to submitMode='ctrlEnter' so only the contentEditable div (which correctly checks isComposing) handles plain-Enter submission. 2. Scroll jumps during idle: The ResizeObserver auto-follow loop was active even when the thread wasn't running, causing spurious pinToBottom calls whenever any layout shift occurred (browser reflow, font load, GPU cache eviction). Gated the ResizeObserver on thread.isRunning so auto-scroll only follows during active streaming. User messages still pin via useLayoutEffect, and thread.runStart still calls jumpToBottom. * fix(desktop): keep chat bottom anchor stable through idle layout shifts * fix(desktop): prevent code block shrink scroll bounce * fix(desktop): release bottom height lock on run completion * fix(desktop): keep streaming code blocks rendered * fix(desktop): keep bottom anchored through final render * fix(desktop): render streaming reasoning code blocks * feat(desktop): add subtle streaming block animations |
||
|
|
810e5864db |
fix(installer): stop mislabeling stdout-style progress as stderr
Both installers (Electron bootstrap-runner + Tauri) hardcoded a literal `stderr: ` prefix onto every line that arrived on fd 2. Tools like uv/pip/git/npm write normal progress to stderr by design, so routine install output showed up tagged as "stderr" (and rendered red in the Tauri progress UI), making a healthy install look like it was erroring. Carry the stream as structured metadata (`stream: 'stdout' | 'stderr'`) on the log event instead of mangling the line text. The UI now styles stderr subtly (dimmed) rather than alarmingly, and the persistent forensic logs keep their stdout/stderr distinction. |
||
|
|
c930a49ce9
|
fix(desktop): honor upward wheel scroll in long threads | ||
|
|
3aa24e2619
|
fix(desktop): stop chat scroll backward-jump from content-growth interim scrolls (#37997)
The thread scroll-anchor hook in apps/desktop/src/components/assistant-ui/ thread-virtualizer.tsx was disarming sticky-bottom whenever scrollTop decreased by >1px between scroll events. That check was too eager: when content height grows mid-frame (virtualizer measurement of a newly visible turn, streaming token, Streamdown/Shiki re-tokenization, composer chip toggle), the browser emits an interim 'scroll' event whose scrollTop is smaller than the previous frame's because scrollHeight just jumped. The rAF-scheduled pinToBottom hasn't run yet, so programmaticScrollPendingRef is 0 and the disarm fired. With sticky-bottom disarmed the scroller stuck ~50px above bottom — the visible at-rest backward jump that #37997 describes (and the same root cause as the wheel-up variant in #37527). Fix: - Track scrollHeight per frame (lastHeightRef). Disarm on scrollTop decrease ONLY when scrollHeight did not grow this frame. Real upward user intent (scrollbar drag, keyboard PgUp, programmatic scrollIntoView) still disarms because it moves scrollTop without growing the content. Wheel-up and touchmove continue to disarm via their own listeners. - Stop observing the scroller element itself in the ResizeObserver; only observe its content child. Viewport-only resizes (window resize, devtools panel toggle) no longer trigger spurious pins, matching the intent of the auto-stick-to-bottom behavior. Verified: - apps/desktop `tsc -b` clean. - apps/desktop `vitest run src/components/assistant-ui/streaming.test.tsx` passes (9/9), including the existing wheel-up disarm regression test that asserts scrollTop stays at 420 after a wheel-up + content growth. |
||
|
|
49f1b9e4b4 |
fix(desktop): stop Esc reopening the slash/@ menu; harden keyup guard
Follow-up to #37937. That fix guarded the composer's keyup with `shouldSkipTriggerRefreshOnKeyUp(key, trigger !== null)`. The `trigger !== null` check is timing-fragile for Escape: Escape's *keydown* sets `trigger = null` and closes the menu, but in a real browser the *keyup* fires after a re-render, so the handler closure sees `trigger === null`, the guard returns false, `refreshTrigger` runs, re-detects the still-present `/` in the input, and instantly reopens the menu. (jsdom batches state synchronously so a unit test could not observe this -- only the running app does.) Replace the value-based guard with a `triggerKeyConsumedRef` set synchronously in keydown whenever the open popover consumes a nav/control key (Arrow/Enter/Tab/Escape). keyup consults and clears that ref, so it is immune to the keydown->re-render->keyup timing. Applied to both the main composer (chat/composer/index.tsx) and the message-edit composer (assistant-ui/thread.tsx). Removes the now-unused `shouldSkipTriggerRefreshOnKeyUp` helper and its unit test. The real-DOM regression test now fires keydown+keyup pairs through the ref-based handlers and asserts Esc closes and stays closed. Verified by running a production renderer build (Vite v8) under Electron against a local backend: ArrowDown/ArrowUp cycle the full list and Esc dismisses the menu without reopening. |
||
|
|
188e52db91 |
fix(desktop): keep slash/@ completion menu navigable and Esc-dismissable
The desktop composer's `onKeyUp` handler unconditionally re-ran `refreshTrigger` on every keyup, including the Arrow/Enter/Tab/Escape keys the open-trigger `onKeyDown` branch had already fully handled. Because `refreshTrigger` re-detects the trigger and resets the active index to 0, this produced two bugs in the `/` (and `@`) completion popover: - ArrowDown/ArrowUp moved the highlight on keydown, then keyup snapped it straight back to the top — so the user could never cycle past the first couple of items. - Escape closed the menu on keydown, then keyup re-detected the still-present `/` and immediately reopened it — so Esc appeared to do nothing. Fix: skip the keyup-driven refresh for the navigation/control keys while a trigger menu is open (they never edit text, so refreshing is pointless), and only reset the highlight in `refreshTrigger` when the detected trigger query actually changed. Applied to both the main composer (chat/composer/index.tsx) and the message-edit composer (assistant-ui/thread.tsx), which shared the same bug. New `shouldSkipTriggerRefreshOnKeyUp` helper is unit-tested. |
||
|
|
06aa140fa1 |
fix(desktop): inset sticky human messages with --sticky-human-top
Pin user bubbles 0.75rem below the scroll top via a single token instead of flush top-0, so the sticky header doesn't sit hard against the thread edge. |
||
|
|
9bdf01852a |
feat(desktop): clamp sticky human messages to ~2 lines until hover/focus
Long user prompts stick to the top of the thread while the response streams beneath them, so a multi-line prompt could eat most of the viewport. Clamp the read-only human bubble's text to ~2 lines with a soft bottom fade; the clamp lifts on hover or keyboard focus, and clicking the bubble still opens the edit composer (which shows the full text). Short messages are untouched — no clamp, no fade. Overflow is measured on an unclamped inner wrapper so the ResizeObserver only fires on real content/width changes, not every frame while the outer max-height animates open; the measured height feeds --human-msg-full so expand/collapse animate to the true height instead of overshooting the cap. |
||
|
|
54343bcade
|
Merge pull request #37738 from NousResearch/bb/statusbar-model-menu
feat(desktop): inline model picker in the status bar |
||
|
|
ea4fe15631 |
feat(desktop): inline model picker in the status bar
Replace the status-bar model chip's modal with a Cursor-style dropdown: - providers grouped by name in a stable order (no recency reshuffle on select) - per-model hover-Edit submenu for reasoning effort + fast, gated by per-model capabilities now surfaced in the model.options payload - unified Fast toggle: flips the speed=fast param where supported, else swaps to the model's `-fast` variant (base and variant collapse into one row) - localStorage-backed "Edit Models" dialog to choose which models appear Adds reusable dropdown primitives (DropdownMenuSearch, shared row/label tokens, portaled + collision-aware submenus) and reads session state from nanostores rather than prop-drilling, so editing options doesn't rebuild and close the menu. |
||
|
|
dd5e97bd7f |
feat(desktop): make xAI Grok a first-class OAuth provider in the launcher
xAI Grok was only reachable via the "I have an API key" form. xAI's OAuth (SuperGrok / Premium+) flow already exists in the backend (`hermes auth add xai-oauth`) but was never surfaced in the desktop onboarding launcher. Add a loopback PKCE flow: the local backend binds the 127.0.0.1 callback listener, the client opens the browser, and the redirect lands back automatically — no code to copy/paste. Reuses the existing xAI OAuth helpers (discovery, callback server, token exchange, persist) rather than duplicating them. - web_server: catalog entry (flow: loopback) + status dispatch + _start_xai_loopback_flow + background worker + route branch - desktop: 'loopback' flow type, awaiting_browser status, xAI Grok card (PROVIDER_DISPLAY / FLOW_SUBTITLES / FlowPanel waiting render) - tests: catalog listing, start authorize-url, worker persist, state mismatch rejection |
||
|
|
ac76bbe21f
|
fix(desktop): triage batch of GUI quality-of-life fixes (#37536)
* fix(desktop): triage 24 GUI quality-of-life fixes across sidebar, composer, tool cards, messaging, and platform plumbing
A grab-bag of high-leverage UX fixes plus a few backend touches that the
GUI needs to behave correctly on Windows.
Sidebar / sessions
- Decrement $sessionsTotal on delete + archive so "Load N more" stops
claiming removed rows are still on the server.
- Hide the "Group by workspace" toggle when no unpinned sessions exist.
- Accept Cmd/Ctrl+N as a "new session" accelerator (in addition to bare
Shift+N), and render the kbd hint per-platform.
- Switch the statusbar to overflow-x-clip so untitled sessions don't
paint a horizontal scrollbar at the bottom of the window.
Messaging + Cron
- Add [-webkit-app-region: no-drag] to the page-search input so clicks
reach the field instead of routing to the OS window-drag handler.
- Replace single-letter PlatformAvatar with brand glyphs from
@icons-pack/react-simple-icons (telegram, discord, matrix, signal,
whatsapp, mattermost, wechat, qq, ...). Letter monogram fallback for
Slack / Dingtalk / Feishu / WeCom (removed from Simple Icons at brand
owner request).
- Drop the duplicate "Create first cron" button in the empty state.
Composer
- Dedupe pasted images by (name, size, lastModified, type) instead of
Blob identity; Chromium hands us the same screenshot via both
clipboard.items and clipboard.files with fresh File instances.
- Enable spellcheck on the contentEditable, configure Chromium's
spellchecker with the system locale on whenReady, and add
replaceMisspelling + "Add to dictionary" entries to the context menu.
- Render user messages through a minimal markdown pipeline (inline
backtick code + fenced ``` blocks) while keeping @file:/@image:
directive chips intact.
- max-h-[60vh] overflow-y-auto + collisionPadding on the prompt-snippet
submenu.
- Bake cursor-pointer into the <Button> primitive (with
disabled:cursor-default) and into titlebarButtonClass.
Dialogs + tabs + version
- Default DialogContent now has max-h-[85vh] overflow-y-auto so long
bodies scroll instead of falling off-screen.
- Right-rail preview tabs close on middle-click (button === 1), with an
onMouseDown swallow to suppress Chromium autoscroll.
- New refreshDesktopVersion() helper called from About mount, after
every update check, and on throttled window focus so About reflects
the just-installed binary.
Keys + Artifacts + Terminal
- Drop the global "Show advanced" toggle in KeysSettings. Provider
groups now default-expand when they have any key set.
- Extend openExternalUrl to handle file:// via shell.openPath, with
showItemInFolder fallback when the OS can't open the file.
- New lib/ansi.ts SGR parser + <AnsiText> component, applied to
terminal/execute_code tool output.
- ToolView gained stdout / stderr / rendersAnsi; tool-fallback renders
the two streams as separate labeled blocks with stderr in a neutral
tone (not destructive — many CLIs log info on stderr).
- Drop 'stderr' from ERROR_MSG_KEYS in tool-result-summary.
Paths + platform
- resolveHermesCwd skips process.cwd() when packaged and prefers a
user-configurable default project directory.
- New hermes:setting:defaultProjectDir:{get,set,pick} IPC handlers +
preload bridge + global.d.ts typing + a "Default project directory"
row in Sessions settings.
- FileOperations.delete_path(path, recursive=True) on the abstract
base; ShellFileOperations.delete_file rewritten to run a cross-
platform python3 -c snippet so deletes work on Windows shells (which
have no rm/rm -rf). Fallback to `python` when `python3` isn't on PATH.
- README troubleshooting block split into macOS/Linux + Windows
PowerShell recipes.
- Tightened renderer favicon links in index.html + added color-scheme
and theme-color meta.
Backend lifecycle (renderer-side mitigation)
- New noteSessionActivity() heartbeat + session.ts watchdog: an
8-minute silence on the stream auto-clears stuck $workingSessionIds
entries so "Session Busy" never gets permanently wedged. Wired into
useSessionStateCache so every state update refreshes the timer.
i18n spike
- docs/desktop-i18n-rfc.md scoping a future language-switcher PR
(recommends react-intl, audits IME/RTL/CJK in the composer +
chat bubbles, 4-PR rollout plan, ~3-4 eng-weeks for the first
non-English locale).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(desktop): replace native OS scrollbar in portaled dropdown menus
Radix's DropdownMenuPrimitive.Portal renders content under document.body,
outside the `.scrollbar-dt` scope on #root. Whenever a menu's max-height
clipped its content (even by a pixel — common for the composer "+" menu
that opens upward near the bottom of the window), the user saw the OS's
chunky native scrollbar painted across the whole menu.
Bake a thin, slot-styled scrollbar onto DropdownMenuContent and
DropdownMenuSubContent via [scrollbar-width:thin] + WebKit pseudo-element
arbitrary variants. The submenu also gets a max-h tied to
--radix-dropdown-menu-content-available-height so long snippet lists scroll
cleanly instead of running off the bottom of the viewport. Drop the now-
redundant max-h-[60vh] override on the prompt-snippet submenu.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(desktop): unbork dropdown menu — submenu opens, parent isn't a circle
Two regressions from the previous dropdown-scrollbar fix:
- The parent menu rendered as a rounded oval. Long Tailwind v4 arbitrary-
variant strings like [&::-webkit-scrollbar-thumb]:rounded-full inside a
cn() call were being mis-resolved so the `rounded-full` leaked onto the
menu container itself. Replaced the whole tower of arbitrary variants
with a real `.dt-portal-scrollbar` class in styles.css that mirrors what
`.scrollbar-dt` already does for #root descendants. Plain CSS, no Tailwind
parser ambiguity.
- The Prompt snippets submenu didn't open. Radix publishes
--radix-dropdown-menu-content-available-height on Content but NOT on
SubContent, so the `max-h` bound to that variable computed to 0 and the
submenu collapsed to zero height. Switched SubContent to a fixed
max-h-80 (≈20rem) which is plenty for a snippet list and never collapses.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(desktop): promote prompt snippets from Radix submenu to a real Dialog
The submenu refused to open when the parent dropdown was anchored at the
bottom of the window (composer "+" button) — Radix's collision detection +
SubContent positioning was fighting us. Rather than keep tuning side /
sideOffset / collisionPadding / max-h until something stuck, replace the
DropdownMenuSub with a clicked DropdownMenuItem that opens a proper
Dialog.
Side benefits over the submenu:
- Each snippet gets a description line, so a glance is enough to pick one.
- Focus management is handled by Dialog automatically.
- Easy to grow (search, custom user snippets, categories) without
another round of Radix positioning bugs.
Also extract types/interfaces to the bottom of the file per workspace
convention.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(desktop): move cron 'New cron' button off the top bar into the body
Reverses the previous direction on cron empty-state dedup. The body
button is more discoverable for first-time users (it's anchored next to
the "No scheduled jobs yet" copy that explains the feature) and frees
the top bar from a global CTA that wasn't pulling its weight.
- Empty (zero jobs): EmptyState renders the "Create first cron" button
again, like the original design.
- Empty (search filtered out all jobs): no button, just "Try a broader
search query" copy.
- Has jobs: small inline header above the list shows `N/M active` plus
a single "New cron" button (right-aligned). The rows themselves
already cover edit/pause/trigger/delete, so this is the only "create"
affordance.
Also drop the dead `<div className="hidden">…</div>` enabledCount line
the previous patch left behind; the count is now visible in the new
header instead of hidden.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(desktop): address Copilot review on PR 37536
- sessions-settings: guard the WHOLE bridge call rather than chaining
`?.settings.foo().then(...)` — the latter throws when
`window.hermesDesktop` is undefined (non-Electron / Vitest contexts)
because the chain short-circuits to `undefined.then(...)`.
- file_operations: drop `Path.unlink(missing_ok=True)` (Py>=3.8) so the
generated delete snippet still works on remote backends running
Python 3.7. The existing FileNotFoundError handler covers the same
case and works back to 3.4.
- ansi.test.ts: add focused Vitest coverage for the SGR parser
(basic/bright colors, bold toggles, default-fg reset, coalescing,
256-color / truecolor arg consumption, non-SGR CSI drop, empty SGR
full-reset) so future refactors can't silently regress terminal
rendering.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(desktop/updates): swallow refreshDesktopVersion bridge errors
`refreshDesktopVersion()` is called best-effort with `void` from
`checkUpdates()`, `startUpdatePoller()`, and the window focus handler.
If the IPC bridge rejects (main process shutting down during reload,
bridge not yet ready on first paint), the rejection surfaces as an
unhandled promise rejection in the renderer. Wrap the call in try/catch
and return null on failure so callers can keep the existing
fire-and-forget pattern safely.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(desktop): drop work duplicated by other in-flight PRs
- composer/text-utils.ts: revert paste-image dedupe — PR #37596
ships the same fix with a cleaner content-key approach and a
Vitest file (text-utils.test.ts). Letting that PR own the change.
- docs/desktop-i18n-rfc.md: delete the i18n scoping RFC — PR #37568
has already shipped a working i18n surface (homegrown nanostores
`t()` helper over en/zh dictionaries), so the RFC's framework
recommendation (`react-intl`) is now obsolete and would just
contradict the implementation that's actually landing.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
31c40c72c0
|
fix(desktop): stabilize project folder sessions (#37586)
* fix(desktop): stabilize project folder sessions Keep desktop folder selection aligned with new sessions and scope TUI gateway cwd through session context so prompts and tools resolve against the selected workspace. * fix(desktop): address review feedback on folder sessions Snapshot sessions before iterating to avoid concurrent-mutation crashes, optional-chain the revealLogs catch, and read console-message args from the correct Electron event/messageDetails positions. * fix(desktop): address second review pass on folder sessions Sync the remembered workspace key with the cwd atom (clear on empty), only load tree children for real directory nodes, and throttle renderer auto-reloads so a deterministic startup crash can't loop forever. * fix(desktop): inherit parent workspace for ephemeral agent tasks Background and preview tasks use ephemeral ids absent from the session map, so pass the parent session cwd into the session context explicitly instead of clearing it back to the gateway launch dir. Also correct the set_session_vars docstring about clear_session_vars semantics. * fix(desktop): validate preview cwd before pinning session context A non-empty but non-existent client cwd would pin an unusable override and silently fall back to the launch dir. Validate once, reuse for both the session context and the terminal override, and fall back to the parent session workspace when invalid. * fix(desktop): harden preview cwd normalization and adopt normalized cwd Guard preview cwd normalization against malformed client paths so a bad input can't fail the whole restart, and adopt the backend's normalized config.get cwd in the no-active-session path so the persisted workspace stays consistent with what the agent uses. |
||
|
|
0c29cfd1a6 | Clarify desktop install retry guidance | ||
|
|
4239230957 |
feat(desktop): cancellable first-launch install
The install overlay had no way to stop a running install — the runner already supported an abortSignal, but nothing drove it. Wire it end to end: - main.cjs holds an AbortController for the active runBootstrap and aborts it on a new hermes:bootstrap:cancel IPC and on app quit, so quitting/cancelling mid-install actually kills install.sh/ps1 instead of orphaning it. - runBootstrap bails before spawning anything if the signal is already aborted. - Install overlay gains a "Cancel install" button while a bootstrap is active; a cancel surfaces the recovery overlay (retry/repair). Test: electron/bootstrap-runner.test.cjs asserts the already-aborted early return (no spawn) via `node --test`. |
||
|
|
85b65e29f0
|
feat(desktop): session hygiene, archive, media streaming + connecting overlay (#37099)
* feat(desktop): session hygiene, archive, media streaming + connecting overlay
Address a batch of desktop feedback:
- Stop leaking empty "Untitled" sessions: the TUI gateway pre-created a DB
row on every session.create (i.e. every launch/draft). Persist the row
lazily on first prompt instead, and hide message-less rows in the sidebar.
- Archive/hide sessions: new `archived` column + set_session_archived, web
API (`?archived=` + PATCH archived), Ctrl/⌘-click and a context-menu item
in the sidebar, and an "Archived Chats" settings panel to restore/delete.
- Videos load via a streaming `hermes-media://` protocol instead of capped,
in-memory data URLs (16 MB limit) — bypasses the cap and supports seeking.
- Background-process completions route to the session that launched them:
the completion event now carries session_key and each poller only consumes
its own.
- Sidebar: "Group by workspace" toggle is always visible; each workspace
group gets a "+" to start a session in that directory; "New agent"/"Agents"
relabeled to "New session"/"Sessions".
- New gateway connecting overlay (ascii decode → fade out) replacing the bare
skeleton/"starting gateway" state.
* fix(desktop): bail connecting overlay on boot error
The shownRef latch kept the connecting overlay mounted behind
BootFailureOverlay after a hard boot failure. Return null on boot.error
so the failure recovery surface fully owns the screen.
* fix(desktop): address Copilot review
- /api/sessions: validate `archived` (400 on unknown) and return `archived`
as a JSON boolean instead of SQLite's 0/1.
- PATCH /api/sessions/{id}: 400 (not a misleading 404) when the body has no
updatable fields; stop conflating a no-op with "not found".
- hermes-media protocol: drop `bypassCSP` — streaming only needs
secure/standard/stream/supportFetchAPI.
- Sidebar workspace header: split the toggle and the "+" into sibling buttons
so we no longer nest interactive elements inside a <button>.
* fix(desktop): address Copilot re-review
- hermes-media protocol: restrict streaming to an audio/video extension
allowlist (415 otherwise) so it can't be used to read arbitrary local files.
- Connecting overlay: use z-[1200] instead of the non-standard z-1200 utility.
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
||
|
|
359f2be12e
|
feat(desktop): drop files anywhere in the chat area (#36262)
* feat(desktop): drop files anywhere in the chat area File drops were only wired to the composer input. Add a reusable useFileDropZone hook (enter/leave depth counting + capture-phase reset so the affordance clears even when the composer claims the drop) and a pointer-events-none ChatDropOverlay, wired onto the conversation viewport. Drops funnel through the existing onAttachDroppedItems; composer drops keep their own inline-ref behavior. * fix(desktop): chat-area drops insert inline @file refs, not attachment cards Match the composer-input drop behavior — funnel dropped paths through droppedFileInlineRef + the composer insert bus so they render as inline ref chips instead of attachment cards. * fix(desktop): don't render bare file paths as tool images (404) vision_analyze reports its input image as a local filesystem path, which toolImageUrl handed straight to <img src>. In the renderer that resolves against the dev-server origin and 404s. Restrict inline tool images to fetchable sources (data: URLs and remote http(s)); bare paths now fall back to the tool's codicon. |
||
|
|
0bc616ecf9
|
fix(desktop): darken light-mode code comment color for legibility (#36234)
Shiki's github-light-default colors comments #6e7781 (~4.2:1 on the code card background), which is borderline unreadable at the 11px code font size — and worst for shell snippets, where a single `#` turns the rest of the line into one long comment span. Remap light-mode comments to GitHub's darker muted gray (#57606a, ~6.4:1) via per-theme colorReplacements. Dark mode (~6.1:1) reads fine and is left untouched. |
||
|
|
51c68d4ab1
|
Add Hermes desktop app (#20059)
* feat: better composer etc * docs: add desktop and dashboard run instructions * fix(desktop): address security scan findings * fix(dashboard): resolve @nous-research/ui path under npm workspaces The sync-assets prebuild step shelled out to 'cp -r node_modules/@nous-research/ui/dist/fonts ...' with a path relative to apps/dashboard/. That works only when the dep is installed locally in the dashboard workspace, but 'npm install' at the repo root (the documented setup — see apps/desktop/README.md) hoists shared deps to the root node_modules under npm workspaces. The relative cp then fails with 'No such file or directory', sync-assets exits 1, the Vite build aborts, and 'hermes dashboard' surfaces a generic 'Web UI build failed' message. Replace the shell one-liner with scripts/sync-assets.cjs, which walks up from the dashboard directory looking for node_modules/ @nous-research/ui — working in both the hoisted (workspaces) and co-located (standalone) layouts. Also guards against a missing dist/fonts or dist/assets with a clearer error pointing at a rebuild of the UI package rather than silently copying nothing. * feat(desktop): support connecting to a remote Hermes backend Add HERMES_DESKTOP_REMOTE_URL and HERMES_DESKTOP_REMOTE_TOKEN env vars that, when set, short-circuit the local-child spawn in startHermes() and connect the Electron renderer to an already- running 'hermes dashboard' server reachable over the network. Motivating use case: WSL2 users who want to run the Hermes core (agent loop, tools, filesystem access) inside their WSL distribution while rendering the Electron GUI on native Windows. Before this change, the desktop app always spawned a local Python child on the same host as the renderer, which doesn't cross the WSL/Windows boundary. The remote path reuses waitForHermes() as a liveness probe (/api/status is in the backend's public endpoint allowlist), so the connection is only returned once the backend is actually ready. WebSocket URL derivation picks ws:// or wss:// based on the input scheme. URL validation rejects non-http(s) schemes and requires both env vars together to avoid a half-configured connection that would silently fall through to the spawn path. No behaviour change when the env vars are unset — the default local-spawn flow is untouched. Typical usage: # in WSL2 hermes dashboard --tui --no-open --host 0.0.0.0 --port 9119 --insecure # on Windows set HERMES_DESKTOP_REMOTE_URL=http://localhost:9119 set HERMES_DESKTOP_REMOTE_TOKEN=<session token> set HERMES_DESKTOP_IGNORE_EXISTING=1 (launch Hermes desktop) * ci(desktop): automate desktop releases Add GitHub Actions release channels for signed desktop installers and document the stable/nightly download paths. * feat: file tabs * refactor(desktop): tighten right-rail tab close API Promote closeRightRailTab/closeActiveRightRailTab as the single public entry point. Drops the activeTabRef + handleCloseDocument indirection in ChatPreviewRail, the unused $rightRailHasContent atom, and the legacy dismissFilePreviewTarget alias. -70 LOC. * feat(desktop): polish composer pill toward reference look Solid foreground-on-background send/voice-conversation circle (black-on-white in light, white-on-black in dark) anchors the right edge as the primary CTA instead of the orange theme primary. Bumps the primary control to 2.125rem so it visually outranks the ghost mic/plus controls. Opens up the surface padding (0.625rem x / 0.5rem y) so the input row breathes around its controls, and nudges the corner radius from 20 to 24px for a slightly pill-ier silhouette. LiquidGlass distortion is preserved. * feat(desktop): add startup and onboarding flow Add phase-based desktop boot progress, fresh-install sandbox testing, and first-run provider credential onboarding so packaged installs can start cleanly without manual settings detours. * fix(desktop): gate prompts on provider setup Show the desktop provider onboarding flow before prompt submission when no inference provider is configured, preventing fresh installs from falling through to backend credential errors. * fix(desktop): surface provider onboarding from session warnings Propagate credential warnings through session runtime info and open desktop onboarding whenever a session reports no usable provider, so unconfigured installs cannot fall through to prompt errors. * fix(desktop): route gateway provider errors to onboarding The "No inference provider configured" auth error reaches the renderer through gateway error events, not the prompt.submit promise; the previous patch only caught the latter, so the error toast still surfaced and onboarding never opened. Also strip credential-shaped env vars from the test:desktop:fresh sandbox so the packaged backend can't see provider keys leaking from the launching shell. * fix(desktop): use strict runtime check to drive onboarding setup.status returned True whenever any provider auth state was discoverable, including indirect fallbacks like a gh-CLI Copilot token. That made desktop think the user was set up while the agent's actual resolve_runtime_provider call still raised AuthError, leaving the user with a useless toast and no onboarding. Add a setup.runtime_check gateway method that runs the same resolver the agent uses on session creation, and switch the desktop onboarding overlay and prompt precheck to use it. * feat(desktop): OAuth-first onboarding using existing dashboard provider API Replace the engineer-flavored API key form with a Sign-in-first onboarding overlay that uses the dashboard's existing /api/providers/oauth catalog and PKCE/device-code endpoints (Anthropic, Nous, OpenAI Codex, etc.). API key entry is now a fallback tab with friendly provider names instead of env var prefixes, and the loud raw resolver error is gone in favor of a one-line welcome message. * fix(desktop): polish onboarding provider list Reorder OAuth providers so Nous Portal is first, give the segmented Sign in / API key control equal column widths, and replace the engineer-flavored backend names like "Anthropic (Claude API)" / "MiniMax (OAuth)" with friendlier in-app titles. External-CLI providers now show a softer subtitle and an external-link icon instead of a chevron. * refactor(desktop): split onboarding overlay into store + view Move the OAuth state machine, runtime check, copy-to-clipboard, and api-key save into store/onboarding.ts (matching the boot.ts pattern), leaving the overlay as a presentation layer that subscribes via useStore. Tabs are now table-driven, child panels read flow from the store instead of prop-drilling, and the polling/PKCE/error/success branches share a small Status atom. * fix(desktop): external CLI providers + center mode tabs External-CLI providers (Claude Code, Qwen Code) now open an in-overlay panel with the CLI command, copy button, and an "I've signed in" recheck instead of firing an invisible toast. Center the Sign in / API key tab control so it sits under the heading instead of hugging the left edge. * fix(desktop): drop onboarding tabs for an inline link, group device-code waiting state Replace the Sign in / API key tab pair with an "I have an API key" footer link under the OAuth provider list, with a "Back to sign in" affordance inside the API key form. Group the device-code "Waiting for you to authorize..." status next to the Cancel button so the alignment matches the action. * refactor(desktop): tighten onboarding store + overlay Drop the dead isOnboardingBusy/BUSY set, factor the catch-fallback dance into safeReq, and share a single reloadAndConnect helper between PKCE submit, device-code success, external recheck, and api-key save. In the overlay, extract Step / CodeBlock / FlowFooter / CancelBtn / DocsLink atoms so the four sign-in panels share the same chrome instead of repeating it inline. Net effect: fewer literal divs, one place to touch the spacing, and the code-block + footer rows are reusable across future flows. * fix(desktop): mount onboarding from frame 1 to kill the FOUT Default onboarding.configured to null (unknown until the runtime check resolves) and have the onboarding overlay render whenever it's not yet confirmed true. The boot overlay now yields to it, so the very first paint is the Welcome card with a "While we get you set up..." progress strip instead of a flash of the chat shell between boot dismiss and onboarding mount. The picker swaps in cleanly once the gateway opens and the runtime check confirms the user is not configured. Already-configured users see the same prep card briefly while their existing runtime warms up, then the overlay dismisses without touching the chat shell. * fix(desktop): top-align empty sessions placeholder The "Start a chat to build your history." empty state used a min-h-35 grid place-items-center container, which floated the text in a tall dead zone. Render it as a flat paragraph that sits right under the section header like the empty pinned state does. * refactor(desktop): drop dead boot overlay Onboarding overlay subsumes the boot card now that it mounts from frame 1 and renders boot progress inline. The standalone DesktopBootOverlay is unreachable in every flow (yields whenever onboarding has not confirmed configured, dismisses once it has). * fix(desktop): hide pinned/recents sections until first session A fresh sidebar showed the Pinned and Recent chats headers with floating empty-state copy underneath. Drop both sections (and the now-orphan SidebarEmptySessionState) when there are no sessions yet — they reappear after the first chat. Skeletons during initial load are unchanged. * feat(gui): route embedded TUI through dashboard gateway (#21979) Inject HERMES_TUI_GATEWAY_URL into dashboard PTY sessions so embedded ui-tui instances attach to the in-process websocket gateway, with coverage for the new env wiring. * Add desktop remote gateway settings Make the desktop gateway connection configurable from settings so local remains the default while remote backends can be saved, tested, and applied without environment variables. * feat(gui): first-class Messaging page + gateway menu redesign - Add Messaging page to the desktop app with per-platform setup, status, and inline guidance. Catalog derives from gateway.config Platform enum + plugin registry, so every messaging adapter the CLI supports (Telegram, Discord, Slack, Mattermost, Matrix, WhatsApp, Signal, BlueBubbles, Home Assistant, Email, SMS, DingTalk, Feishu, WeCom, Weixin, QQ, Yuanbao, API server, Webhooks, plugins) shows up without per-platform code. - New REST endpoints: GET /api/messaging/platforms, PUT and POST /test on the same path. Secrets go through the existing .env pipeline; enable/disable writes config.yaml. - Replace gateway statusbar dropdown with a richer panel: status row, icon-only restart + system-panel actions, recent activity (with timestamps trimmed in display, full text on hover), platform list. - Auto-poll the messaging page every 6s (paused when hidden) so status updates without a manual check. - Drop Settings / Command Center from the sidebar nav (still reachable via shortcuts and the titlebar cog). - Flatten top corners on Messaging/Skills/Artifacts/Chat panes. - Share new StatusDot component across messaging + gateway menu. - Fix gateway/config.py so an explicit platforms.<name>.enabled=false in config.yaml is honored when env tokens are present. - pb-9 on the chat content area for breathing room above the composer. * Potential fix for pull request finding 'CodeQL / Clear-text logging of sensitive information' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * pin electron version * hide application menu on non-mac systems * interpret compactPreview for non-string vlaues as JSON or an empty string * fix(desktop): keep composer contenteditable mounted across stacked toggle The composer rendered {input} inside two different parent fragments depending on `stacked`. When auto-expand flipped `stacked` (e.g. the moment typed text wrapped past two lines), React reconciled the two branches as different positions and unmounted/remounted the contenteditable. The fresh mount started empty, so any in-flight characters — most reliably reproduced by holding a key — were lost. Replace the conditional with a single CSS Grid whose template-areas swap on `stacked`. The three children (menu, input, controls) keep stable identities across the toggle; only their grid placement changes, which the browser handles without React tearing down the editor. * refactor(desktop): align install layout with install.ps1 / install.sh Make the desktop app's runtime layout match what scripts/install.ps1 and scripts/install.sh produce, so a desktop-only user and a CLI-only user end up with the same files in the same places and can share one install. Layout - ACTIVE_HERMES_ROOT = HERMES_HOME/hermes-agent (was: process.resourcesPath/hermes-agent, read-only) - VENV_ROOT = HERMES_HOME/hermes-agent/venv (was: userData/hermes-runtime) - desktop.log = HERMES_HOME/logs/desktop.log (was: userData/desktop.log) - HERMES_HOME default: %LOCALAPPDATA%\hermes on Windows, ~/.hermes elsewhere The packaged .app/.exe still ships a read-only payload at process.resourcesPath/hermes-agent (FACTORY_HERMES_ROOT). On first launch or after an installer-driven upgrade we sync factory -> active, then provision the venv and run pip install -e . against the active root. Key behaviors - Pin HERMES_HOME in the spawned Python's env so get_hermes_home() resolves to the same path resolveHermesHome() picked. Without this, Python falls back to ~/.hermes on every platform - fine on mac/linux, a split-state bug on Windows where our default is %LOCALAPPDATA%\hermes. - Detect developer installs by .git presence at ACTIVE; never overwrite a user's checkout via factory sync. - Marker at ACTIVE/.hermes-desktop-runtime.json (schema v4) tracks pyproject hash + factory version + runtime schema version. depsFresh fast-paths when nothing changed. - Dev (npm run dev) prefers SOURCE_REPO_ROOT over ACTIVE so devs run their local edits, not whatever's under HERMES_HOME. - Better error messages distinguish "no payload" from "no Python". - Preserve a legacy ~/.hermes on Windows when no %LOCALAPPDATA%\hermes exists, so users with prior pip/manual installs aren't orphaned. pyproject.toml - Promote fastapi, uvicorn[standard], ptyprocess (non-Windows), and pywinpty (Windows) to main dependencies. The dashboard backend (hermes dashboard) needs them at runtime; the previous lazy-import fallback was a footgun for fresh installs. - Empty the [pty] optional-extra; kept as a no-op back-compat alias for any existing pip install hermes-agent[pty] invocations. Drops the hardcoded BUNDLED_RUNTIME_REQUIREMENTS list in main.cjs - the desktop now installs whatever pyproject.toml says, single source of truth. Files - apps/desktop/electron/main.cjs: runtime layout, HERMES_HOME pin, factory->active sync, marker v4 - apps/desktop/scripts/test-desktop.mjs: track new venv location - apps/desktop/README.md: new Setup, Runtime Bootstrap, and Debugging sections - pyproject.toml: fastapi/uvicorn/pty backends in main dependencies; [pty] extra emptied Tested locally on Windows: npm run dev boots cleanly, sessions land at the new location, type-check + lint + test:desktop:platforms all pass. Verified end-to-end on a fresh Win11 VM via dist:win installer. Known gaps (filed as follow-ups, not in this PR): - Skills not seeded on packaged installs (sync_skills only runs in cmd_chat, not cmd_dashboard). Need to move to shared pre-dispatch. - Git Bash not bundled or detected; agent's terminal tool errors out with a useful message but desktop bootstrapper should pre-flight it. - install.ps1 / install.sh should be decomposed into composable phase libraries so the desktop bootstrapper can reuse them as a single source of truth across all install surfaces. * feat(desktop): theme polish, prose chat typography, composer chrome - DS tokens/midground, Backdrop, scoped scrollbars, typography plugin + prose - Composer liquid/radius utilities, thread font parity, tool/thinking cues - File tree label scale, preview flex, thread retry loading + streaming tests * feat(desktop): NSIS prereq detection page + auto-install via winget The packaged Windows installer now detects Python 3.11+ and Git for Windows at install time and offers to install missing prereqs via winget. Mirrors the prereq logic scripts/install.ps1 already runs for CLI installs, so desktop installer users get the same out-of-the-box experience as install.ps1 users. Why - Hermes' terminal tool calls bash.exe directly (tools/environments/ local.py); on Windows that's Git Bash from Git for Windows. Without it, the agent fails on the first terminal() call. - Hermes' Python runtime needs 3.11+. Without it, the desktop bootstrapper errors out at venv creation. - Both gaps surfaced on a fresh Windows 11 VM smoke test: VM had Python pre-installed but no Git, so the agent's first terminal call failed with "Git Bash isn't installed." - install.ps1 has had Install-Git + Install-Uv functions for ages. The desktop installer was the asymmetric outlier. How — NSIS prereq page - New file: apps/desktop/installer/prereq-check.nsh (plugged into electron-builder via build.nsis.include) - Real Wizard page using nsDialogs, inserted via customPageAfterChangeDir hook (between the Directory page and InstFiles). - Group boxes for Python and Git, each showing detection status. - Pre-checked install checkboxes when winget is available. - Auto-skips silently if both prereqs are already installed. - Falls back to manual download URLs when winget itself is missing. - Detection: - Python: probes `py -3.11`/`-3.12`/`-3.13`/`-3.14` via the Python launcher. Microsoft Store "Python stub" (no py.exe) is correctly classified as not-installed. - Git: `where git`. - winget: `where winget` (Win10 1809+ / Win11 with App Installer). - Install execution (in customInstall macro): - Python: nsExec::ExecToLog with `--scope user --silent`. Per-user install, no UAC prompt, output streams to install log. - Git: ExecShellWait via Windows ShellExecute. Critical because Git always installs per-machine and triggers UAC; ShellExecute preserves the foreground focus chain across non-elevated → elevated process spawns, so UAC actually comes to the foreground. nsExec::ExecToLog breaks the chain because winget runs hidden. - Both pass `--disable-interactivity --accept-package-agreements --accept-source-agreements` to suppress winget's own dialogs. - Verification: probes Git's standard install locations via FileExists rather than `where git`. NSIS's process inherits PATH at startup, so a freshly-installed Git won't be visible to `where` until restart. - Silent installs (/S) skip the prompts; managed deploys handle prereqs out-of-band via Group Policy / Intune. How — Electron-side safety net - New findGitBash() in main.cjs, parallel to findSystemPython(). Probes the same locations as tools/environments/local.py:_find_bash() so a positive result here means the agent's terminal tool will work. - ensureRuntime now throws a clear, actionable error on Windows when Git Bash isn't found, matching the existing "Python 3.11+ is required" error path. - Catches users the NSIS page doesn't: .msi installer users (NSIS prereq page doesn't run for MSI), `npm run dev` users, manual installers, anyone who unchecked the install boxes on the NSIS prereq page. - All gated on `IS_WINDOWS`; macOS / Linux unaffected. NSIS build issue (resolved) - electron-builder defaults to `-WX` (warnings as errors). NSIS optimizer emits "warning 6010: function not referenced" for our page functions because Page custom directives don't count as references in its static-analysis pass. The functions ARE called at runtime when NSIS invokes the page; the optimizer just can't see it statically. - Set `build.nsis.warningsAsErrors=false` in package.json so this spurious warning doesn't fail the build. (Documented option from electron-builder's nsisOptions.) Out of scope (filed for future work) - MSI prereq detection: Windows Installer custom actions are a different mechanism. Enterprise deploys typically handle prereqs via GP/Intune. - Bundle PortableGit + python-build-standalone in extraResources for zero-network installs. ~80MB increase. - Mac / Linux GUI prereq flows (different installer formats; Xcode CLT covers most macOS prereqs already; Linux is per-distro hard). Files - apps/desktop/installer/prereq-check.nsh (new, ~290 lines NSIS) - apps/desktop/package.json (build.nsis.include + warningsAsErrors) - apps/desktop/electron/main.cjs (findGitBash + preflight) - apps/desktop/README.md (Runtime prerequisites section) Cross-platform impact - macOS / Linux builds (dist:mac, dist:mac:dmg, dist:mac:zip): nsis config is ignored entirely; .nsh is dormant. - npm run dev: .nsh dormant; main.cjs preflight gated on IS_WINDOWS. - scripts/install.ps1, scripts/install.sh: no reference to any new files; CLI install paths untouched. - Hermes CLI / dashboard / gateway: no reference; runtime untouched. - All checks: node --check on main.cjs and test-desktop.mjs pass; npm run test:desktop:platforms 4/4 passing; node --test green. Tested - npm run dist:win produces signed .exe and .msi without errors. - Fresh Win11 VM (Python pre-installed, no Git): prereq page renders, Python check shows detected, Git checkbox pre-checked. Click Next → Git installs via winget with UAC prompt in foreground. - After install completes, Hermes launches and the agent's terminal tool can run bash commands. Verified Git Bash is detected at `C:\Program Files\Git\bin\bash.exe` by ensureRuntime's preflight. * feat: theme changes, composer tweaks, in app update ux, finesse * fix(cli): seed bundled skills on dashboard + gateway entrypoints `sync_skills(quiet=True)` was only being called from inside `cmd_chat`, which meant `hermes dashboard` (the desktop GUI's backend) and `hermes gateway` (Telegram/Discord/Slack/etc daemons) never seeded the bundled skill library into ~/.hermes/skills/. This surfaced as "No skills found" in the desktop GUI's skills panel on fresh installs, despite the agent having access to the full bundled library when invoked via `hermes chat`. scripts/install.ps1 worked around it by running skills_sync.py as part of Copy-ConfigTemplates, but that's not part of the desktop installer's bootstrap chain. Fix - Extract the skills-sync block from cmd_chat into a module-level `_sync_bundled_skills_quietly()` helper. - Call the helper from cmd_chat (preserving existing behavior), cmd_dashboard (after the --status/--stop early-return paths and fastapi import check, so we don't run skills_sync on management commands or when deps aren't installed), and cmd_gateway. Why these three entrypoints - cmd_chat: the user's primary CLI entrypoint - cmd_dashboard: the desktop GUI's backend; this is what `hermes dashboard --tui` invokes when the desktop bootstrapper spawns Hermes - cmd_gateway: long-running daemons where the user expects the agent to have full skill access Other entrypoints (cmd_config, cmd_doctor, cmd_login, cmd_status, etc.) are management commands that don't need skill discovery and were never running skills_sync in the first place — leaving them alone. Idempotence - tools/skills_sync.py is manifest-based: skipped skills cost milliseconds. Calling it from multiple entrypoints adds no real cost, and users running `hermes chat` then `hermes dashboard` get two fast no-ops on the second call. Failure handling - Helper wraps skills_sync in try/except. Skills are an enhancement, not a hard dependency — Hermes runs fine with an empty skills/ dir. Files - hermes_cli/main.py: + new helper `_sync_bundled_skills_quietly()` at module level + cmd_chat: replace inline block with helper call + cmd_dashboard: add helper call after fastapi import succeeds + cmd_gateway: add helper call before delegating to gateway_command * feat(desktop): hoisted todo widget, JSON tool summaries, history grouping & timer fixes - Hoist todo to first-class widget (shadcn checkboxes, brand colors, no tool-accordion). Header derives label from active task; non-active rows fade. - Replace raw JSON dumps with structured key/value summaries via formatToolResultSummary; nested error extraction for clearer failures. - Fix loaded-session grouping: stitch interleaved assistant/tool iterations into one bubble instead of orphaned synthetic messages. - Stable tool/thinking timers via keyed registry so unmount/scroll doesn't reset elapsed counts; gate "running" on real live thread state. - Reorganize chat-only assistant-ui components under components/chat/. * fix(desktop): address CodeQL alerts on PR #20059 - settings/helpers.ts: harden setNested against prototype pollution. POLLUTING_PATH_PARTS check is now applied at every assignment site (loop + leaf) and uses Object.defineProperty so CodeQL can see the guard inline rather than via a helper function call. - lib/markdown-preprocess.ts: rebuild the dangling-fence close regex from a fence-char + length instead of marker.replace(...). The marker is captured by `(`{3,}|~{3,})` so it can only be backticks or tildes, but CodeQL was tracing tainted input text into the RegExp source and flagging hostname dots from input as part of the pattern (false positive js/incomplete-hostname-regexp on the test fixture URLs). Reconstructing from a literal char breaks the dataflow. - scripts/notarize-artifact.cjs: drop args from the run() rejection message. Args carry --key-id / --issuer / key file path; the existing outer catch already squashes errors to a generic line, but CodeQL was flagging the args.join(' ') as clear-text logging of APPLE_API_KEY_ID. Composer DOM-text-as-HTML alerts (composer/index.tsx:379, :547) are already addressed in |