Commit graph

107 commits

Author SHA1 Message Date
Brooklyn Nicholson
c7542358f2 fix(desktop): remote project picker UX and profile-scoped fs/git routing
Route FS/git REST through the active profile, mount the remote folder picker
at app root, keep the project dialog open while picking, show a first-run
blank state, flip into grouped view on create, and constrain the picker scroll
area so Select stays reachable.
2026-06-28 16:23:39 -05:00
Brooklyn Nicholson
8d8c7111d9 refactor(desktop): keep remote fs routing inside the fs facade
Let UI callers ask for folders/files without knowing remote-picker limits:
selectDesktopPaths now normalizes remote directory selection to a single folder
inside the facade. Project creation and composer context picking no longer branch
on remote mode; they route through desktop-fs helpers just like git callers route
through desktopGit(). Behavior unchanged except remote folder context now works
through the same backend picker path.
2026-06-28 14:39:33 -05:00
Brooklyn Nicholson
453f134b3b refactor(desktop): centralize remote git REST routing
Keep the remote git mirror as a thin facade: route all GETs through gitGet,
all mutations through gitPost, and keep consumers on desktopGit(). On the
backend, route git paths through a single _git_path helper instead of repeating
str(_fs_path(...)) in every endpoint. Behavior unchanged.
2026-06-28 14:37:36 -05:00
Brooklyn Nicholson
4e9439cc3b fix(desktop): route composer context picking through remote-aware fs
Second pass on the remote-project flow: the project dialog and git cockpit were
remote-aware, but the composer's Add file/folder context picker still called the
native Electron picker directly. Route it through selectDesktopPaths so remote
sessions use the backend-aware picker instead of local disk paths; preserve local
multi-select behavior and keep remote folder selection single because the in-app
remote picker only supports one directory.

Also use readDesktopFileDataUrl for image previews so an already-known backend
image path can be read through /api/fs/read-data-url, and add focused coverage
for backend file-diff routing plus the plain-folder git init/worktree path.
2026-06-28 14:35:23 -05:00
Brooklyn Nicholson
fc86e35764 feat(desktop): make the git cockpit work over a remote gateway
After the folder picker fix, an added remote folder was still half-usable:
the desktop's git GUI (coding-rail status, worktree lanes, review pane,
branch switch, file diff) all ran Electron-local git on the USER's machine,
so against a remote-gateway repo they silently degraded to empty.

Mirror the whole surface over the dashboard REST API so it acts on the
BACKEND repo where sessions actually run:

- hermes_cli/web_git.py: git/gh logic (status, worktrees, branches, review
  list/diff/stage/unstage/revert/commit/commit-context/push/ship-info/
  create-pr, file-diff, worktree add/remove, branch switch) shelling to the
  system git, mirroring the Electron ops' shapes.
- web_server.py: /api/git/* routes (same auth gate + _fs_path hardening as
  /api/fs, executor-offloaded, mutations -> 400).
- apps/desktop desktop-git.ts: remote-aware facade exposing the same shape as
  window.hermesDesktop.git; coding-status / review / projects / model /
  desktop-fs route through desktopGit() so local stays Electron, remote hits
  /api/git/*.

Tests: tests/hermes_cli/test_web_server_git.py (real repo: status counts,
review classification, diff incl. untracked all-add, stage+commit roundtrip,
worktree/branch lifecycle, commit-context, gh-absent ship-info, auth) and
desktop-git.test.ts (local vs remote routing, envelope unwrap, POST bodies).
2026-06-28 14:26:09 -05:00
Teknium
163cb24d45
feat(moa): render reference-model blocks in TUI and desktop, not just CLI (#53855)
The MoA reference-block display (each reference model's output shown as a
labelled thinking block before the aggregator responds) previously existed
only in the classic CLI. The facade already emits moa.reference / moa.aggregating
through tool_progress_callback; this wires the TUI and desktop consumers.

- tui_gateway/server.py: _on_tool_progress relays moa.reference (label / text /
  index / count) and moa.aggregating to the Ink/desktop client as their own
  events.
- ui-tui: gatewayTypes adds the two event shapes; createGatewayEventHandler
  routes them; turnController.recordMoaReference pushes a committed
  thinking-style segment tagged with the source model. Shown regardless of
  showReasoning — references ARE the mixture-of-agents process the user opted
  into, not ordinary reasoning. moa.aggregating is a status-only transition
  (no transcript entry).
- apps/desktop: use-message-stream appends each reference as a labelled
  reasoning chunk via the existing reasoning disclosure; GatewayEventPayload
  gains label/index/aggregator.

Tests: tui_gateway emit (3), Ink handler render + showReasoning-independence +
aggregating-no-segment (3). TUI typecheck/lint clean; desktop typecheck/lint
clean.
2026-06-27 18:46:20 -07:00
brooklyn!
ed962104c8
Merge pull request #52935 from NousResearch/bb/desktop-inline-rendering
feat(desktop): inline rich embeds, diagrams & alerts in assistant markdown
2026-06-26 13:36:43 -05:00
Brooklyn Nicholson
8559246bfb feat(desktop): rebuild the clarify prompt to match the chat UI
The inline clarify panel used its own card tokens, an animated ring, and
oversized spacing — out of step with every other tool row. Rebuild it on the
shared --ui-*/--conversation-* tokens: a compact panel, letter-key badges
(A/B/C…) that double as a/b/c… shortcuts, an inline content-sizing "Other" field
(CSS field-sizing — no view swap, no layout shift on focus), and a Continue
button so picking an option selects rather than auto-sends. Selection lives on
the letter badge alone (solid primary; outlined while Other is focused-but-empty).
Also settle the panel into the standard tool block once the turn stops running,
so a stopped turn no longer strands a live, unanswerable prompt.
2026-06-26 03:55:29 -05:00
Brooklyn Nicholson
da0ed979fa feat(desktop): zoomable primitive — open full, pan/zoom, copy
Add a content-agnostic Zoomable primitive (useZoomPan hook + overlay viewer):
click to open full-screen, wheel-zoom toward the cursor, drag to pan, toolbar
zoom/reset, and an optional copy action. Wire Mermaid diagrams into it with
copy-as-PNG; reusable for other inline content later.
2026-06-26 03:40:49 -05:00
Brooklyn Nicholson
62fe9fd101 style(desktop,tui): fix all lint/type/formatting issues
Bring apps/desktop and ui-tui to a clean state for typecheck, eslint,
and prettier:

- Run prettier across both trees (printWidth/wrap drift; prettier is not
  CI-enforced for these JS projects, so main had accumulated drift).
- Apply eslint --fix for padding-line-between-statements and perfectionist
  import/export sorting.
- Manual fixes for non-auto-fixable rules:
  - remove unused node:net import in electron/main.cjs (uses Electron net)
  - replace inline `typeof import(...)` annotations with top-level
    `import type * as EnvModule` in two ui-tui test files
  - scoped eslint-disable no-control-regex on intentional sentinel/ANSI
    regexes (mathUnicode.ts, text.ts)
  - resolve react-hooks/exhaustive-deps per-case: correct swapped/missing
    deps, collapse redundant session.* members, and justified disables on
    settings mount-only data-load effects to preserve run-once behavior

No behavior changes; test pass/fail counts are unchanged from the main
baseline.
2026-06-26 01:04:33 -05:00
Brooklyn Nicholson
c456029b4e Merge remote-tracking branch 'origin/main' into bb/editor 2026-06-25 19:53:31 -05:00
Brooklyn Nicholson
ff81365988 feat(desktop): in-app spot editor for the file preview pane
Adds a CodeMirror 6 spot editor to the right-rail file preview so users can
make quick edits in-app without leaving for an IDE. Entering edit mode is a
pure in-place swap of the read view — same fixed-height header, same gutter
geometry/typography (mirrors SourceView 1:1) so nothing shifts — toggled via
the Edit button, a bare `e` when the pane is hovered/focused, or the tab.

- Save path is transport-agnostic (writeDesktopFileText): local Electron IPC
  or a new hardened POST /api/fs/write-text on the dashboard server (path
  validation, parent-must-exist, regular-files-only, size cap, atomic
  temp-file + os.replace), behind the existing auth middleware.
- Stale-on-disk guard re-reads before writing and offers overwrite vs
  discard-and-reload instead of clobbering external/agent edits.
- VS Code-style modified dot on the tab; ⌘/Ctrl+S and ⌘/Ctrl+Enter save,
  Esc cancels; GitHub highlight style matched to the read view's Shiki theme.
- Typing stays render-free (draft in a ref; dirty flips once at the boundary).
2026-06-25 19:50:25 -05:00
brooklyn!
55af6c447a
Merge pull request #52206 from NousResearch/bb/desktop-tools-curation
fix(desktop): hide platform/internal toolsets from the Skills & Tools list
2026-06-25 18:56:04 -05:00
Brooklyn Nicholson
344415892f feat(desktop): add shared project UI primitives 2026-06-25 16:40:27 -05:00
Brooklyn Nicholson
41f302fa73 fix(desktop): compact tool row titles
Make completed desktop tool rows read like useful activity labels instead of raw plumbing: terminal rows use a dispatch-style shell summarizer for agent wrappers, and read_file rows keep the action plus filename and requested line range.

The shell cleanup follows condensed-milk-pi's shape: split command compounds on real separators, strip pipe tails inside each segment, clean redirects/env prefixes, then classify setup/banner/status segments. Multi-command probes render as `first command + N commands`; the full command remains available in copy/detail.

Read rows now render as `Read package.json` or `Read main.ts L25-34`, using requested positive offset/limit and returned line numbers only as fallback for negative/unknown offsets.
2026-06-25 00:01:11 -05:00
xxxigm
4aeaba6922 test(desktop): cover undefined/null attachment holes in ref helpers
Regression for the refText crash: attachmentDisplayText and
optimisticAttachmentRef must return null (not throw) when handed an
undefined/null attachment hole, so the submit path can't reproduce
"Cannot read properties of undefined (reading 'refText')".
2026-06-24 18:22:01 -07:00
xxxigm
7e2db0a140 fix(desktop): stop refText crash on undefined composer attachment holes
A session switch or draft restore can leave undefined/null holes in the
composer attachments array. AttachmentList was guarded against this in
#49624, but the sibling submit path was not: submitPromptText maps the
same array through attachmentDisplayText/optimisticAttachmentRef and
buildContextText (a.kind / a.label / a.refText), so a hole threw
"Cannot read properties of undefined (reading 'refText')" — an uncaught
renderer error that blanks the chat pane and shows "Desktop app link
offline".

Close the whole bug class:
- attachmentDisplayText / optimisticAttachmentRef no-op on a falsy
  attachment (shared chokepoint, also protects thread.tsx drop handler).
- submitPromptText filters falsy entries from the source array, and
  buildContextText filters its (possibly post-sync) input before reading
  fields.
2026-06-24 18:22:01 -07:00
brooklyn!
7157b213f5
Merge pull request #47959 from NousResearch/bb/pets-gen
Pet generation: frame-perfect hatch flow, backend picker, CPU-safe chroma, and CI-hardening
2026-06-24 19:41:34 -05:00
Brooklyn Nicholson
00779800f6 fix(desktop): hide platform/internal toolsets from the Skills & Tools list
GET /api/tools/toolsets returns the full CONFIGURABLE_TOOLSETS set with no
desktop curation, so the Skills & Tools → Toolsets list shows entries that
don't belong in a flat per-user toggle: platform-coupled toolsets (discord,
discord_admin, yuanbao — which `hermes tools` already platform-restricts off
the CLI) and internal plumbing (context_engine, moa). `hermes tools` curates
these out; the desktop didn't.

Add a small documented block-list + predicate (mirroring
desktop-slash-commands.ts) and apply it in the toolset list filter. Hiding a
row is cosmetic — enabled state and runtime gating are untouched.
2026-06-24 19:18:29 -05:00
Brooklyn Nicholson
1fe013ee16 feat(pets): polish generate flow and reduce hatch CPU pressure
Ship the final pet-generation UX polish (provider picker behavior, step-2 cancel flow, banner integration, and visual consistency) and make saturated-chroma background removal C-op driven so hatch processing no longer hammers the machine during long runs.
2026-06-24 19:08:06 -05:00
Brooklyn Nicholson
8d1706ae5c fix(desktop): wire Ctrl+B voice, declutter voice settings, stop endless TTS hang
Three voice-mode papercuts in the desktop app:

1. Ctrl+B did nothing. The docs + `voice.record_key` advertise Ctrl+B to
   talk, but the desktop never bound it (only ⌘B = sidebar existed). Add a
   rebindable `composer.voice` action that toggles the voice conversation,
   defaulting to ⌃B on macOS (distinct from ⌘B; off-macOS `ctrl` folds to
   the sidebar chord, so it ships unbound there to avoid stealing it). The
   global keybind reaches the composer through a new focus-bus event.

2. The Voice settings page rendered every provider's options at once (~30
   fields). Filter to the *selected* TTS/STT provider's sub-fields; STT
   provider fields hide when STT is off. Picking "edge" now shows just the
   Edge voice, making it obvious voice chat also needs STT enabled.

3. Voice mode could hang "speaking" forever. Free Edge TTS sometimes returns
   audio that never fires `playing`/`ended`/`error`, so the playback promise
   never settled. Add a stall watchdog (rearmed on each progress tick, so
   long speech is never cut off) that rejects a stuck stream, letting the
   loop recover with a clear error.
2026-06-24 18:26:14 -05:00
Brooklyn Nicholson
743985bf1e feat(pets): Pokédex generate UI — overlay, animated egg, hatch FX, manage
Dedicated generate modal (Cmd-K → Pets → Generate): prompt → 2×2 draft
grid → egg hatch → preview → adopt, width fits each phase.

- Reuses shared primitives (Button/Input/Dialog/Alert/GenerateButton);
  cards use selectableCardClass; only canvas + range stay raw.
- Animated creme pixel egg + PetStarShower hatch celebration (canvas).
- Live streamed drafts with a real Stop (AbortSignal); clean default name.
- Manage generated pets: badge + top ranking, rename (optimistic), safe
  delete (confirm + drop), export — in both the Cmd-K and Settings lists.
- pet-gallery routes every RPC through profile-scoped petRpc; i18n ×5.
2026-06-24 13:51:34 -05:00
brooklyn!
35e9c63d89
Merge pull request #52008 from infinitycrew39/fix/desktop-nous-onboarding-stale-provider
fix(desktop): stop Nous Portal onboarding from validating stale Anthropic config
2026-06-24 13:12:44 -05:00
infinitycrew39
d8fe1c0b41 test(desktop): cover scoped onboarding runtime readiness checks
Assert setup.runtime_check honors provider params and that Nous OAuth
onboarding persists model config before validating the connected provider.
2026-06-24 23:19:51 +07:00
infinitycrew39
6da615c77c fix(desktop): scope onboarding runtime check to connected provider
Let setup.runtime_check accept an optional provider, persist the selected
provider/model before the gate, and validate the provider the user just
connected instead of a stale config entry such as anthropic.
2026-06-24 23:19:45 +07:00
Brooklyn Nicholson
e495b33bf1 Merge remote-tracking branch 'origin/main' into bb/pets-merge
# Conflicts:
#	hermes_cli/commands.py
#	tui_gateway/server.py
2026-06-23 19:05:22 -05:00
Brooklyn Nicholson
a6b670d4a2 fix(desktop): avoid stack overflow on embedded image replay
Replace the giant embedded-image regex with a bounded scanner so opening sessions with multi-megabyte data URLs does not crash the renderer.
2026-06-22 18:19:36 -05:00
Brooklyn Nicholson
5342eccf12 Merge remote-tracking branch 'origin/main' into bb/pets 2026-06-22 05:25:49 -05:00
brooklyn!
04a1d9efd7
feat(desktop): PR-style file diffs in chat (#50731)
* feat(desktop): add Update now button to About panel

The About > Updates panel only surfaced "See what's new" when an update
was available, which just opens the changelog overlay — there was no way
to start the install directly from About. Add an "Update now" primary
button that opens the updates overlay (for apply progress) and kicks off
the install for the active target (backend in remote mode, else client).

* feat(desktop): PR-style file diffs in chat

Render write_file/edit_file/patch as a reviewable diff instead of raw
result JSON, closer to a Cursor/T3 per-edit review.

- Unified diff via FileDiffPanel: strip git file-header + @@ hunk noise,
  drop the +/- gutter, color by line with a 2px gutter accent, full-bleed
  to the card, transparent context lines, compact scroll height.
- Header shows filename + language icon + +N/-N stats; full path moves to
  a hover tooltip (no Edited verb, no ms).
- Treat the three file-edit tools uniformly (isFileEditTool); read diff
  from inline_diff or patch's diff field; suppress raw-arg detail.
- Reusable FileTypeIcon primitive sharing the code-block icon mapping
  (codiconForFilename), codicon fallback.
- Per-row scaffolding fade (not the group wrapper, which trapped child
  opacity); expanded edits stay full, collapsed fade; keyboard-only focus
  lift. Hide diff-less rehydrated creates that read as dupes.

* style(desktop): lead --dt-font-mono with bundled JetBrains Mono

Code/diff blocks preferred a system Cascadia Code before the bundled
JetBrains Mono, so they drifted from the terminal (which leads with
JetBrains Mono) on machines where Cascadia is installed. Reorder so every
mono surface uses the face we actually ship.

* feat(desktop): syntax-highlight inline diffs via Shiki

Unify the diff renderer onto the same Shiki path as code blocks: highlight
the marker-stripped change content in the file's language, then a per-line
transformer layers the add/remove tint + gutter accent on top. Falls back
to the plain color-only renderer when the language is unknown, over budget,
or while Shiki loads.

- shikiLanguageForFilename(): extension → bundled-language id (shared
  filename-token helper with codiconForFilename).
- code display:grid so full-width line tints don't double with newline
  nodes; theme surface stripped so context lines stay transparent.

* style(desktop): use github-dark-dimmed for inline diffs

The vivid github-dark-default tokens read harsh behind the add/remove
tint in dark mode; switch the diff's dark theme to GitHub's lower-contrast
dimmed palette. Light mode and code blocks are unchanged.

* style(desktop): dim code-block syntax theme + share with diffs

Apply github-dark-dimmed to code blocks too (not just inline diffs) and
export one shared SHIKI_THEME so the two highlighters can't drift. Lower
contrast reads easier at our small code size in dark mode.

* style(desktop): soften shiki token contrast in dark mode

github-dark-dimmed only dims the background, which the diff/code surfaces
strip — so the bright token foregrounds were unchanged. Pull saturation +
brightness back a touch (hues preserved) on .shiki in dark mode for both
code blocks and inline diffs.
2026-06-22 05:22:23 -05:00
Brooklyn Nicholson
ac128af1ce feat(desktop): syntax-highlight inline diffs via Shiki
Unify the diff renderer onto the same Shiki path as code blocks: highlight
the marker-stripped change content in the file's language, then a per-line
transformer layers the add/remove tint + gutter accent on top. Falls back
to the plain color-only renderer when the language is unknown, over budget,
or while Shiki loads.

- shikiLanguageForFilename(): extension → bundled-language id (shared
  filename-token helper with codiconForFilename).
- code display:grid so full-width line tints don't double with newline
  nodes; theme surface stripped so context lines stay transparent.
2026-06-22 05:10:23 -05:00
Brooklyn Nicholson
a61baa9615 feat(desktop): PR-style file diffs in chat
Render write_file/edit_file/patch as a reviewable diff instead of raw
result JSON, closer to a Cursor/T3 per-edit review.

- Unified diff via FileDiffPanel: strip git file-header + @@ hunk noise,
  drop the +/- gutter, color by line with a 2px gutter accent, full-bleed
  to the card, transparent context lines, compact scroll height.
- Header shows filename + language icon + +N/-N stats; full path moves to
  a hover tooltip (no Edited verb, no ms).
- Treat the three file-edit tools uniformly (isFileEditTool); read diff
  from inline_diff or patch's diff field; suppress raw-arg detail.
- Reusable FileTypeIcon primitive sharing the code-block icon mapping
  (codiconForFilename), codicon fallback.
- Per-row scaffolding fade (not the group wrapper, which trapped child
  opacity); expanded edits stay full, collapsed fade; keyboard-only focus
  lift. Hide diff-less rehydrated creates that read as dupes.
2026-06-22 05:04:13 -05:00
Teknium
7130d60861
feat(providers): remove google-gemini-cli + google-antigravity OAuth providers (#50492)
* feat(providers): remove google-gemini-cli + google-antigravity OAuth providers

Google now actively bans accounts for third-party tools that piggyback on
Gemini CLI / Antigravity / Code Assist OAuth, and because abuse prevention
sits at a backend layer the ban can extend to the entire Google account
(Gmail/Drive), with a second violation being permanent.
Ref: https://github.com/google-gemini/gemini-cli/discussions/20632

Removes both OAuth inference providers entirely (modules, provider profiles,
auth/runtime/config/models wiring, the /gquota Code Assist quota command,
the antigravity-cli optional skill, desktop + docs surface in en + zh-Hans).
The API-key 'gemini' provider (GOOGLE_API_KEY/GEMINI_API_KEY against
generativelanguage.googleapis.com) is unaffected and stays fully supported.

* fix(skills): keep the antigravity-cli skill — only the OAuth provider is removed

The antigravity-cli optional skill orchestrates the external `agy` binary as
a coding-agent tool via the terminal tool — it does NOT wrap Hermes inference
through the banned google-antigravity OAuth provider, so it carries none of
the account-ban risk that motivated removing that provider. Restore the skill,
its docs page, the sidebar entry, and the optional-skills catalog row. The
google-antigravity / google-gemini-cli inference providers stay fully removed.
2026-06-21 19:53:27 -07:00
Brooklyn Nicholson
86b990fe0f feat(desktop): floating pet, pop-out overlay + Cmd+K picker
Add the in-window floating pet (sprite, speech bubble, contact shadow,
profile-scoped, resize-safe) and a pop-out always-on-top overlay window
with gestures and notifications. Add the Cmd+K pet picker page plus the
appearance gallery and size slider in settings. Includes the pet stores,
electron overlay wiring, i18n strings, and store tests.
2026-06-20 14:18:40 -05:00
Teknium
b936f92b25
fix(desktop): render send/prefill directive notices (/goal, /undo) (#49073)
The desktop slash dispatcher dropped the `notice` field on `send` and
never handled `prefill` directives at all. `/goal <text>` returns
{type: send, notice: "⊙ Goal set …", message} from command.dispatch —
the desktop submitted the goal text as a plain prompt with no feedback,
so the goal looked like it did nothing. `/undo` returns a prefill
directive that fell through to "invalid response".

- types: add `notice?` to SendCommandDispatchResponse; add
  PrefillCommandDispatchResponse to the union.
- parseCommandDispatch: keep `notice` on send, parse prefill.
- runExec dispatcher: render the notice as a system line before acting,
  and handle prefill by dropping the message into the composer for
  editing (mirrors the TUI's createSlashHandler).

Tests: parseCommandDispatch send-notice / prefill cases.
2026-06-19 07:28:50 -07:00
xxxigm
069011dd0c test(desktop): cover runtime->stored notification id resolution
Unit-test `storedSessionIdForNotification`: runtime ids resolve to their
stored id, unknown ids and empty maps pass through unchanged, the right
stored id is picked among several sessions, and stored ids (map keys) are
never rewritten.
2026-06-19 17:50:35 +05:30
xxxigm
f9ffe0bc3f fix(desktop): resume stored session id on notification click
Native notifications (approval / sudo / secret / clarify) are tagged with
the gateway *runtime* session id — the key under which the session lives in
the gateway's in-memory `_sessions` map and the id every event carries
(`tui_gateway/server.py` `_emit(event, sid, ...)`). The chat route, however,
is keyed by the *stored* session id (`stored_session_id`), which is a
different value: a new chat gets its runtime id immediately but its stored id
only once the first turn persists.

`onFocusSession` navigated straight to `sessionRoute(<runtime id>)`, so
clicking a notification (e.g. an approval prompt) sent the route-resume path a
runtime id where it expects a stored id. `useRouteResume` then resumed it as a
stored session -> REST `/api/sessions/<runtime id>` 404 "session not found",
and the running session was navigated away, which the user experiences as the
session being destroyed.

Translate runtime -> stored before navigating via the existing
`runtimeIdByStoredSessionId` map (new `storedSessionIdForNotification`
helper), falling back to the id as-is when no mapping is known. The
Approve/Reject notification button path is untouched: `approval.respond` is
routed by the runtime id (`_sess()` -> `_sessions[session_id]`), so it must
keep carrying the runtime id.
2026-06-19 17:50:35 +05:30
Brooklyn Nicholson
0138282f97 perf(desktop): keep oversized messages from freezing the chat
A multi-MB message (logged bundle, huge tool dump) froze the renderer
before any paint: Streamdown runs `preprocess` + `marked` lex over the
whole string synchronously in a useMemo, an uninterruptible long task
that no try/catch or content-visibility can help (our JS runs before the
browser ever skips layout). Tiered fix:

- Message gate: past 200KB, bypass markdown entirely and render the raw
  text in `content-visibility:auto` line-chunks — synchronous work is
  bounded to a string split, the browser virtualizes layout natively,
  and every line stays in the DOM (selectable, find-in-page).
- Code-block budget: past 3k lines / 150KB, skip Shiki (which emits a
  span per token) and render plain, chunked the same way.
- Collapse/expand: a reusable ExpandableBlock clamps code blocks and the
  huge-text fallback to a 120px preview with a gradient + chevron,
  expanding to 300px. The inner element is always a scroll container so
  the content-visibility chunks stay lazily laid out in both states.

No content is ever dropped; the copy button (card header) always yields
the full block.
2026-06-17 08:25:52 -05:00
Brooklyn Nicholson
547a014e7e fix(desktop): avoid stack overflow rendering huge fenced blocks
`normalizeFenceBlocks`/`pushProseFence` appended block bodies with
`out.push(...lines)`, which spreads every line as a separate call
argument. A single message carrying a large fenced block (a logged
minified bundle, base64 blob, or big tool dump — common in long
sessions) overflows V8's argument-count limit and throws
`RangeError: Maximum call stack size exceeded`, breaking the transcript
render. Compression doesn't save us: it gates on tokens vs. window, not
a single message's line count, and the protected recent tail renders
verbatim regardless.

Append iteratively via a small `extend()` helper. Behavior is identical
for normal-sized blocks.
2026-06-17 00:34:59 -05:00
brooklyn!
b7f0c9cd52
fix(desktop): honor pre-session model pick + restore global reasoning/speed defaults (#47447)
* fix(desktop): keep the pre-session model pick selected in the picker

The composer picker derived its "current" row from `model.options ?? store`,
so model.options always won. Pre-session that query returns the PROFILE
DEFAULT, not the sticky composer pick — so selecting a model before a session
exists left the checkmark (and the picker's "current" line) on the default,
making the pick look ignored even though the pill updated.

Add `currentPickerSelection()`: with a live session the gateway's model.options
is authoritative; pre-session the sticky `$currentModel`/`$currentProvider`
wins, falling back to options. Wire it into ModelMenuPanel and ModelPickerDialog.

* feat(desktop): global reasoning/speed defaults in Settings → Model

The composer picker is now sticky-UI/per-session only and never writes the
profile default (#46959), but Settings → Model had no reasoning/speed control
and `agent.reasoning_effort` wasn't in the curated config surface at all
(`service_tier` was buried in Advanced) — so there was nowhere to set the
profile default that crons/subagents/messaging resolve from.

Add capability-gated Reasoning (effort) + Fast controls beside the main model,
gated by the applied model's reported capabilities (reasoning defaults on, fast
off when unreported — same as the composer). They read/write `agent.reasoning_effort`
and `agent.service_tier` by round-tripping the config record, matching the
gateway's value semantics (service_tier "fast"/"priority"/"on" ⇒ fast).

* refactor(desktop): don't open the reasoning select from its row label

A <label> wrapping the Select forwarded text clicks to the trigger, opening
the dropdown unexpectedly. Plain row for reasoning; Fast stays a <label> so
clicking its text toggles the switch (expected for a checkbox-like control).
2026-06-16 16:22:09 -05:00
brooklyn!
c6e99ab375
Merge pull request #46959 from NousResearch/bb/composer-model-selector
feat(desktop): composer model selector, per-model presets & external-provider disconnect
2026-06-16 09:55:57 -05:00
Teknium
a68ac0c49a
feat(desktop): allow /browser connect on a local gateway (#47245)
* fix(skills): guard recursive skill delete against tree-escape

Port from Kilo-Org/kilocode#11240. Their issue #11227 lost a user's entire
working directory: a built-in-skill sentinel location resolved to the server
cwd and the skill-removal endpoint ran a recursive delete on it.

Hermes' /skills uninstall path (skills_hub.py) is already hardened, but the
agent-facing skill_manage(action='delete') path did a bare
shutil.rmtree(skill_dir) with no last-line validation. Add _validate_delete_target():
refuse to rmtree a path that (1) isn't strictly inside a known skills root,
(2) is a skills root itself, or (3) is reached via a symlink/junction.

Tests: 4 cases (normal delete works; symlinked dir, skills-root, out-of-tree
all refused). E2E verified with real symlink + file I/O.

* feat(desktop): allow /browser connect on a local gateway

/browser was hardcoded as terminal-only in the desktop slash palette, so
the chat GUI rejected it with "only available in the terminal interface."
The TUI already drives the live CDP connection via the browser.manage RPC.

Wire the same RPC into the desktop dispatcher as a /browser action handler,
gated to local-gateway connections ($connection.mode !== 'remote'). connect
mutates BROWSER_CDP_URL (and may launch Chrome) in the gateway process, so
it's only meaningful when that process runs on this machine; a remote
gateway gets a clear "local gateway only" message instead.
2026-06-16 09:03:43 -05:00
Brooklyn Nicholson
989d5d0cb7 fix(desktop): declutter date-pinned model snapshots in the picker
Provider catalogs surface date-pinned snapshots (`…-20251101`) that the
picker rendered as standalone rows with the date baked into the name
("Opus 4 5 20251101"). Strip the trailing date from display names, and
fold a snapshot out of the list when its rolling alias is present so the
alias stays selectable/searchable while the exact dated id isn't shown
as its own row.
2026-06-15 23:53:41 -05:00
brooklyn!
c6b0eb4de0
fix(desktop): open remote-gateway artifacts via authenticated download (#46895)
Some checks failed
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Docker Build and Publish / build-amd64 (push) Waiting to run
Docker Build and Publish / build-arm64 (push) Waiting to run
Docker Build and Publish / merge (push) Blocked by required conditions
Lint (ruff + ty) / ruff + ty diff (push) Waiting to run
Lint (ruff + ty) / ruff enforcement (blocking) (push) Waiting to run
Lint (ruff + ty) / Windows footguns (blocking) (push) Waiting to run
Tests / test (1) (push) Waiting to run
Tests / test (2) (push) Waiting to run
Tests / test (3) (push) Waiting to run
Tests / test (4) (push) Waiting to run
Tests / test (5) (push) Waiting to run
Tests / test (6) (push) Waiting to run
Tests / save-durations (push) Blocked by required conditions
Tests / e2e (push) Waiting to run
Typecheck / typecheck (apps/bootstrap-installer) (push) Waiting to run
Typecheck / typecheck (apps/desktop) (push) Waiting to run
Typecheck / typecheck (apps/shared) (push) Waiting to run
Typecheck / typecheck (ui-tui) (push) Waiting to run
Typecheck / typecheck (web) (push) Waiting to run
Typecheck / desktop-build (push) Waiting to run
Docker / shell lint / Lint Dockerfile (hadolint) (push) Has been cancelled
Docker / shell lint / Lint docker/ shell scripts (shellcheck) (push) Has been cancelled
OSV-Scanner / Scan lockfiles (push) Has been cancelled
uv.lock check / uv lock --check (push) Has been cancelled
On a remote gateway connection, agent-written files live on the gateway
host, not the desktop's disk, so the Artifacts view's file:// hrefs failed
("Invalid external URL") and image thumbnails broke.

Make mediaExternalUrl() remote-aware in one place: in remote mode it
rewrites gateway-local paths to GET /api/files/download (a new endpoint
that streams the file as a Content-Disposition: attachment). The artifacts
view now resolves through it, and so do the existing chat-media and
generated-image callers, for free.

The download endpoint stays auth-gated; auth_middleware additionally
accepts the session token as a ?token= query param for this one path so a
shell/browser-opened download (which can't set the session header) still
authenticates — the same query-token tradeoff as the /api/pty WebSocket.
It is NOT added to PUBLIC_API_PATHS.

Salvages #46663 (which carried ~19k lines of CRLF noise and made the
endpoint public). Reimplemented on a clean LF base with the security hole
closed and tests added.

Co-authored-by: qingshan89 <qs2816661685@gmail.com>
2026-06-15 23:50:19 -05:00
Brooklyn Nicholson
98c294126b feat(desktop): open new sessions in compact windows
Add the Electron IPC bridge and rebindable shortcut for opening an unkeyed scratch window on the new-session draft.
2026-06-15 20:59:57 -05:00
Brooklyn Nicholson
37d717054e refactor(desktop): unify stream-part coalescing into one helper
Collapse segmentMergeIndex + mergeTextInto + the three append helpers
into a single segment-aware appendStreamPart core plus a part-factory
table. Same behavior, DRY.
2026-06-15 18:13:52 -05:00
Brooklyn Nicholson
1cb75b7971 fix(desktop): coalesce interleaved reasoning/content stream parts
Models that interleave their reasoning_content and content token streams
(Kimi/DeepSeek/GLM-style routes) emit text -> reasoning -> text deltas
within a single tool-bounded segment. Appending each delta as its own
part shredded one sentence into "Let me" / Thinking / "verify the file",
with a Thinking disclosure wedged mid-sentence.

Coalesce streaming deltas into the most recent same-type part within the
current segment (bounded by any non-streaming part, e.g. a tool call).
The opposite streaming channel is transparent, so a reasoning burst
between two content deltas no longer opens a fresh text part, while a
real tool call still starts a new segment and preserves narration order.

Data-layer only; the renderer already groups consecutive reasoning.
2026-06-15 17:48:35 -05:00
Brooklyn Nicholson
630a4ef03c feat(desktop): native OS notifications with per-type toggles
Adds a native OS notification system (Electron Notification, routed cross-OS)
distinct from the in-app toast feed. Before this, one hardcoded cue existed
(message.complete while document.hidden) with no settings or event coverage.

- Engine (store/native-notifications.ts): localStorage-backed prefs (master
  switch + per-kind toggles) and a gated dispatcher over five kinds — approval,
  input, turnDone, turnError, backgroundDone — with a 1s per-(kind,session)
  self-evicting throttle.
- Gating: "backgrounded" = document.hidden OR !document.hasFocus(), so an
  alt-tabbed window still counts as away. Completion kinds fire only when
  backgrounded and for the active session (no spam from a busy gateway);
  attention kinds (approval/input) also break through for off-screen sessions.
- Wired into real event sites (use-message-stream.ts): message.complete, error,
  approval/clarify/sudo/secret.request; backgroundDone from composer-status at
  the running -> exited transition.
- Click focuses the window and jumps to the originating session; approval
  notifications carry Approve/Reject buttons that resolve in place over
  approval.respond, mirroring the in-app Run/Reject bar.
- Settings: new Notifications panel (master + per-kind switches, test button
  with real OS-result feedback). Full i18n (en/ja/zh/zh-hant).
2026-06-14 00:31:03 -05:00
brooklyn!
b4ba3f5e3b
feat(desktop): add curated completion cue for agent turn completion (#42480)
* feat(desktop): add curated completion sound bank for turn completion

Replace the prior haptic-only completion cue with a curated Web Audio completion sound flow, defaulting to the minimal two-note comfort preset while keeping alternate presets available for quick iteration. Play the cue on every message completion event (including background sessions) so turn-end feedback is consistent across active and non-active chats.

* refactor(desktop): drop done1 byte sample from completion bank

Keep the curated Web Audio presets only; the embedded sample added bulk without shipping as the default cue.

* feat(desktop): expand completion sounds and add Appearance picker

Add fourteen synthesized turn-end presets with preview in settings, persisted variant selection, and softer default mixing for late-night use.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(desktop): dedupe completion-sound resolver, trim audio comments

Make the store the single source of truth for the variant default + range
validation and have the sound lib import it (one-way lib→store edge, no
cycle), instead of two divergent copies. Extract the shared white-noise
buffer used by the air/whoosh voices and cut the synth comments down to
why-only notes.

---------

Co-authored-by: Austin Pickett <pickett.austin@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 00:21:40 -05:00
Brooklyn Nicholson
b15dc58064 fix(desktop): keep generated images in the tool slot, not inline
The image-generate tool showed a placeholder, then the model echoed a
(often different) image inline in its prose — a second, jarring copy in
the wrong place, dimmed as tool scaffolding, with a misplaced download
button.

Now the generated image lives only in the tool slot:
- Strip every embedded image/media link from the assistant prose of a
  message that produced an image (the model frequently restates the
  remote URL while the result holds the local path), preserving the
  agent's words. Applied on hydration, live deltas, and completion.
- One stable frame sized from the aspect_ratio arg up front, so the
  diffusion placeholder and the decoded image share the same box and
  crossfade with no layout shift; the box derives its height from the
  true ratio on load (no letterboxing).
- Exempt generated images from the tool-block dim-until-hover rule.
- Extract a shared useImageDownload hook + ImageLightbox so the tool
  image and markdown images share one implementation.
2026-06-13 02:42:15 -05:00
Gille
1e755ff556
fix(desktop): keep recents sorted unless manually reordered (#45404) 2026-06-13 00:38:10 -05:00