Commit graph

393 commits

Author SHA1 Message Date
Brooklyn Nicholson
adfaa95e3b feat(desktop): chip a link pasted or typed into the composer
A pasted link went in as raw URL text, wrapping across the composer and
staying inert. It now becomes the same `@url:` reference the "+ → Add URL"
dialog inserts — parsed in place, so a link mid-sentence keeps its position
and the punctuation that ended the sentence stays outside the chip. Typing
one and pressing space commits it the same way.

Both rich-editor surfaces get it: the composer and the message-edit box,
whose paste went through `execCommand` and could not produce a chip at all.
2026-07-26 15:27:06 -05:00
Brooklyn Nicholson
ee09162e45 fix(desktop): label a url chip with its host and path
The composer labeled a `@url:` chip with `refLabel`, which takes the last
path segment — three PR links all read as their number. The transcript had
its own better labeler that stopped at the hostname, so the same reference
read differently before and after send.

One `refChipLabel` now serves both: host without `www.`, path riding along
for the chip's existing truncate to cut, and the full value on the chip's
title so a cut-off link is still readable on hover.
2026-07-26 15:23:51 -05:00
hermes-seaeye[bot]
af217e444b
fmt(js): npm run fix on merge (#71892)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-26 10:16:26 +00:00
Brooklyn Nicholson
8d243c8afa fix(desktop): put the technical tool payload behind a chevron
Technical mode rendered the raw payload two different ways — a bare
block for most rows, a native `<details>` for file edits, whose
browser-drawn marker matches nothing else in the app. Both are now one
collapsed chevron disclosure at a smaller type size, with even padding
against the row body.
2026-07-26 04:25:32 -05:00
Brooklyn Nicholson
f2ac03196f fix(desktop): restore the pane tab strip's bottom rule
The strip's rule is an inset shadow painted in the container's last pixel
row, and full-height tabs covered it — so each tab read as overhanging
the bar by 1px. Inactive tabs compensated with their own border, stacking
a second translucent line that darkened the seam.

Inactive tabs now stop 1px short and draw no bottom border, leaving the
container as the sole owner of one continuous rule; the active tab keeps
full height so it alone cuts through. Hover also darkens rather than
lightens, since lightening moved a hovered tab toward the active
surface's look.
2026-07-26 04:25:29 -05:00
Brooklyn Nicholson
6893faf472 feat(desktop): style inline links as tinted chips
Content links read as a small primary-tinted chip instead of an
underline, dropping the trailing external-link arrow. The tint is
currentColor-relative, so one class carries text and fill in the same
hue across every theme, and `box-decoration-break: clone` gives a
wrapped link a chip per line fragment.
2026-07-26 04:25:16 -05:00
hermes-seaeye[bot]
2686cfa5b4
fmt(js): npm run fix on merge (#71845)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-26 08:20:35 +00:00
brooklyn!
1aed1f7bf4
Merge pull request #71812 from NousResearch/bb/memory-tool-card
fix(desktop): stop painting healthy tool rows as errors
2026-07-26 03:12:25 -05:00
Brooklyn Nicholson
87ee6a52fa fix(desktop): count output_preview as command output
The nonzero-exit guard already knew a failing exit code with real output
usually isn't a failure worth painting red. It only looked at output /
stdout / stderr, but background-process polls report their text under
output_preview — so a poll of any process that exited nonzero rendered
destructive-red with no error to show, even when the output was routine
npm chatter. Half the red process rows in a real session history were
this case.
2026-07-26 02:23:25 -05:00
Brooklyn Nicholson
717746ecf8 feat(desktop): give memory tool rows a title and glyph
Memory rows fell back to the auto-derived name, so they read as a bare
"Memory" / "Running memory" next to a generic icon. Add the tool to
TOOL_META with proper copy across all five locales, plus a solid brain
glyph in the existing Phosphor fill set.
2026-07-26 02:00:01 -05:00
Brooklyn Nicholson
6192d3bb57 fix(desktop): stop painting memory writes as errors
A memory tool call rendered destructive-red with an alert glyph and, when
expanded, a raw dump of the whole args + result payload.

Two causes. The red came from toolStatus treating any error text as a
failure: the memory store rejects an over-budget batch so the agent can
retry a smaller one, which is routine bookkeeping, not something the user
acts on. Those now land on the existing amber warning tier.

The payload dump came from memory having no case in toolSubtitle or
toolDetailText, so both fell through to the generic stringify-everything
fallback. Both now surface just the human-readable line.
2026-07-26 01:59:54 -05:00
brooklyn!
6ffd7302bf
Merge pull request #71789 from NousResearch/bb/timeline-idle
perf(desktop): stop the thread timeline working when nothing can see it
2026-07-26 01:58:26 -05:00
brooklyn!
d471fc9560
Merge pull request #71799 from NousResearch/bb/model-picker-perf
perf(desktop): faster model picker open + idle CPU on stacked tabs
2026-07-26 01:51:20 -05:00
Brooklyn Nicholson
bf726a7ab8 fix(desktop): drop the fade mask on the live thinking preview
The preview window masked its top 28% to transparent while reasoning
streamed, so a gradient appeared over the thinking text and vanished the
moment the block finished. Remove the mask; the max-height window and
bottom-pinned scroll still keep the preview compact and following the
newest tokens.
2026-07-26 01:21:47 -05:00
Brooklyn Nicholson
0b745bc993 perf(desktop): pause glyph spinners on hidden tabs
Each kept-alive tab whose model hasn't resolved ticks a GlyphSpinner —
setInterval + setState + a React commit every ~80ms, per mounted tab,
for pixels behind the active one. Gate the interval on the pane's
visibility context; the visible tab keeps its spinner, hidden tabs
resume from frame 0 on reveal.
2026-07-26 01:10:22 -05:00
Brooklyn Nicholson
91a8fe4a3e perf(desktop): stop the thread timeline working when nothing can see it
The prompt rail mounts in every chat surface, and a tab group keeps
inactive tabs mounted, so a background timeline was stringifying every
user prompt's full text on each store update — including on every
streamed assistant token, since the selector walked all messages — plus
running a scroll listener and a getBoundingClientRect per prompt against
a viewport nobody was looking at.

It now defers each piece until it can be seen. An inactive pane returns
before a single hook is declared (usePaneVisible, the same context the
hidden-tab transcript freeze uses), so no subscription is opened at all.
An active rail subscribes to prompt IDS rather than prompt text and
reads the transcript imperatively only when that signal changes, which
takes streaming off the derivation path entirely; an identical
derivation hands back the previous array so a filtered-out prompt
doesn't restart the measure effect. The offset pass bails below the
render threshold instead of measuring a rail that renders null, ahead of
the existing following-the-bottom fast path, and the hover popover keeps
its shell for the fade but builds its rows on first open.
2026-07-26 00:45:34 -05:00
Brooklyn Nicholson
982a425162 perf(desktop): skip the timeline rect walk while following the bottom
ThreadTimeline's scroll compute read getBoundingClientRect for every user
message per scroll frame; interleaved with React's streaming style writes
each read forced a full reflow — the hottest self-time frame in the
multitab profile (620ms over one 5-tab run). While the viewport is pinned
to the bottom the active prompt is simply the last entry, so answer from
data and save the layout reads for actual scrollback.
2026-07-26 00:20:49 -05:00
Brooklyn Nicholson
47f0cdf3a2 perf(desktop): freeze hidden tab transcripts during streaming
Keep-alive keeps every ever-active tab mounted, but each hidden tab's
ChatRuntimeBoundary still subscribed to its view's $messages — so every
streaming delta flush (~30x/s) re-rendered every busy tab's whole thread,
and five concurrent sessions dropped the app to a crawl.

Flow the pane layer's visibility down as PaneVisibleContext and gate the
$messages subscription on it: a hidden tab freezes its transcript (status
dots stay live through the separate status atoms) and catches up in one
commit on reveal, since subscribe fires immediately with the current value.
2026-07-26 00:20:40 -05:00
Brooklyn Nicholson
f97e7086c7 fix(desktop): keep the first-run local-start error from being wiped on mount
Clicking "Install Hermes locally" surfaced no error when the bridge was
missing, if the click landed before React drained the passive effect that
reacted to the first bootstrap snapshot. The effect cleared the transient
button state on every change of setupChoice.activeRoot, and the very first
snapshot counts as a change: it moves the root from null to its initial
value. The choice paints as soon as that snapshot commits, so a fast click
produced an error that the effect then cleared before it ever rendered.

The state now records the root it was produced under and is read back only
under that same root, so leaving a phase still discards it but arriving in
one cannot. Deriving it removes the effect rather than adding a guard to it.

Reproduced by observing the DOM with a MutationObserver instead of findBy*,
which only settles on a timer tick — by then the effect has already run and
the window is closed. Reverting the component change fails the new test.
2026-07-25 21:34:35 -05:00
brooklyn!
6ab5d2df2a
Merge pull request #71664 from NousResearch/bb/composer-slash-trigger
fix(desktop): make skills referenceable anywhere in the composer
2026-07-25 21:26:14 -05:00
brooklyn!
a135b278dd
Merge pull request #71601 from helix4u/fix/desktop-model-picker-authority
fix(desktop): keep model picker aligned with session state
2026-07-25 21:20:54 -05:00
Brooklyn Nicholson
412a535433 fix(desktop): scope mid-message references to skills and keep them as chips
Two corrections to the inline slash reference.

Only skills are offered mid-message now. A built-in like `/model` or `/new`
acts on the app, so it reads as nothing useful in the middle of a sentence —
whereas a skill is exactly the thing you want to point at while describing
work. A leading `/` is unchanged and still offers the full command set.

A picked skill also stays a pill in the sent message. The composer already
inserts one, but the message renderer knew nothing about slash references and
flattened it back to raw text on send. It now parses a mid-prose `/skill` into
a chip segment and renders it with the same styling the composer uses. The
submitted text is untouched — the chip still round-trips to the literal
`/clean` the backend expects — so this is presentation only. Path-like tokens
(`/usr/local/bin`) are excluded: unlike the caret-anchored composer trigger,
this scans finished text, so it also has to reject a token that runs on into
a path.
2026-07-25 20:45:11 -05:00
brooklyn!
a288fc341c
Merge pull request #71678 from NousResearch/bb/code-block-overflow
fix(desktop): keep code and diffs out of the tool overflow window
2026-07-25 20:42:22 -05:00
Brooklyn Nicholson
7f2971039a fix(desktop): keep code and diffs out of the tool overflow window
A run of 3+ adjacent tool calls collapses into the 6.75rem
`.tool-group-scroll` window. That is right for status rows, but a patch
or execute_code row's body is a code block the user reads, and the
window squeezed it to a ~2-row viewport behind a fade mask.

The CSS break-out (`:has([data-tool-row][data-tool-open])`) already
lifts the cap once a row is expanded, which is why most tools are safe
to bound. It can't help here: the user has to notice the clipped block
and expand it before the code is legible.

Extend the existing UNBOUNDABLE_TOOLS opt-out to the code-bearing tools
behind an `isUnboundableTool` predicate, deriving the file-edit names
from `isFileEditTool` so a newly supported edit tool can't be exempt in
one place and clipped in the other. `terminal` stays boundable: console
output is a log tail whose last lines are the ones that matter, which
is exactly what the window pins.

Verified in Chrome against the app's compiled CSS: a 3-call run
carrying a 520px diff rendered at 108px before, full height after.
2026-07-25 20:35:01 -05:00
ethernet
e026fd61d8 lint(desktop): ban atom-mirrored refs via no-restricted-syntax eslint rule
A ref synced from a nanostores atom via useEffect lags the atom by one
render. Callbacks that read the ref instead of the atom get stale values —
cancelRun sent session.interrupt to the wrong session (#66485), and
steerPrompt/restoreToMessage/editMessage had closure-priority stale reads.

The rule catches the three shapes of this antipattern:
  - useEffect(() => { ref.current = value }, [value])
  - useEffect(() => { ref.current = value; ... }, [value])
  - useEffect(() => { setMutableRef(ref, value) }, [value])

All 79 existing hits get eslint-disable-next-line with a comment. The
legitimate ref writes (DOM instances, mount flags, request tokens,
prev-value tracking, prop mirrors) stay suppressed; the 11 real bug
sites will be fixed in the next commit so their suppressions can be
removed.

Rule is scoped to apps/desktop only — ui-tui and tests-js don't use
nanostores and don't have this pattern.
2026-07-25 18:12:48 -07:00
Gille
c943787bf6 fix(desktop): keep model picker aligned with session state 2026-07-25 16:01:43 -06:00
brooklyn!
5349c7c280
Merge pull request #71162 from NousResearch/bb/session-link-titles
feat(desktop): resolve @session links to titles you can click
2026-07-25 14:03:39 -05:00
Brooklyn Nicholson
9edbc68d0f feat(desktop): open the session a @session ref names
An agent-written ref rendered as a chip that went nowhere on click. It now
renders as an ordinary inline link — the agent wrote it mid-sentence, so it
should read like one — with the funnel icon leading the resolved title.
Clicking either surface (that link, or the chip in the user's own message)
opens the session as a tab, the way its sidebar row does.

The tile store loads on click rather than at import: the composer's rich
editor pulls this module in, so a static import would boot the profile store
and its REST routing along with every transcript render.
2026-07-25 12:09:36 -05:00
Brooklyn Nicholson
0e1332abbb test(desktop): cover hidden-tab resolution for drops, focus, and timeline
Each case mounts a stacked group with a hidden tab whose geometry
matches the visible one — the arrangement that made the original bug
invisible to selector order.
2026-07-25 00:09:30 -05:00
Brooklyn Nicholson
771dfcc083 fix(desktop): keep background tabs out of blur, timeline, and key routing
Same ambiguity in three more lookups: blurComposerInput could blur a
hidden input and leave the focused one alone, the timeline scrolled
whichever viewport it found first, and a clarify card waiting in a
background thread swallowed the foreground composer's letter keys.
2026-07-25 00:09:30 -05:00
Brooklyn Nicholson
9285de4a41 fix(desktop): mark kept-alive panes so document lookups can skip them
Inactive tabs stay mounted and hidden with `visibility`, which preserves
their layout box — so a background tab answers a document-wide selector
with a rect identical to the visible tab's. Tag hidden layers and route
those lookups through visible-scoped query helpers.
2026-07-25 00:09:30 -05:00
Brooklyn Nicholson
92439be351 feat(desktop): render agent-written @session links as chips
Assistant text goes through the markdown renderer, not DirectiveContent,
so a session reference an agent wrote came out as literal text. Rewrite
bare refs into `#session/<value>` links during markdown preprocessing and
dispatch that href to the shared chip in MarkdownLink, alongside the
existing media and preview hrefs.

Preprocessing already skips code fences and inline code, so a ref being
discussed in code stays literal. The pure parsing/href helpers move to
session-refs.ts to keep the resolver's React and API imports out of the
per-flush preprocess path.
2026-07-24 22:52:45 -05:00
Brooklyn Nicholson
dfbc9dbb15 feat(desktop): show resolved titles on @session chips
Route session refs in the transcript through the title resolver so a
dropped session reads as its title instead of a truncated id, and use
Tabler's funnel for the session chip icon.
2026-07-24 22:52:34 -05:00
Brooklyn Nicholson
3d2033db6b refactor(desktop): memoize the directive image-segment filter
Matches the two derived values above it and fixes the indentation.
2026-07-24 21:20:52 -05:00
alelpoan
d0f5ef7041 fix(desktop): persist @image: refs instead of the vision-enrichment text
The desktop gateway passed the vision-enriched, model-only message text
(carrying an `image_url:<path>` hint) straight into run_conversation as
the persisted user turn. The renderer only parses `@image:<path>`, so it
could not rebuild the attachment from history: after a restart the image
was gone and only the caption survived, and on a live session switch the
warm cache disagreed with the authoritative text and the frontend
"rescued" the image by appending it after the caption.

run_conversation already supports persist_user_message for exactly this
"what the model sees" vs "what gets stored" split; it was simply never
wired up for the attachment path.
2026-07-24 21:20:52 -05:00
Brooklyn Nicholson
a8c7a5f70f refactor(desktop): add shared Field form-dialog primitive
Dialog forms each hand-rolled their own label+control+hint stack (or
borrowed the settings-surface ListRow), so gaps and hint styling drifted
between the profile, cron, and webhook dialogs. Add a single Field /
FieldHint primitive for label-over-control dialog fields and adopt it in
the create/rename profile dialogs as the first consumers.
2026-07-24 19:16:45 -05:00
Austin Pickett
0600ac2f77
feat(desktop): add Cron Blueprints to the GUI (#70066)
* feat(desktop): add Cron Blueprints to the GUI

The desktop app had a Cron jobs panel but no Blueprints tab, so the
parameterized automation templates the dashboard offers were unreachable
there (parity matrix: Dashboard=Y, GUI=N).

Adds a Jobs/Blueprints segmented toggle to the cron panel. The Blueprints
tab renders the catalog from the existing GET /api/cron/blueprints endpoint,
one card per template with a typed form (time/enum/weekdays/text slots).
Submitting POSTs to /api/cron/blueprints/instantiate, which fills the slots
and creates a real cron job via the same create_job path as a hand-written
one. The new job is merged into the shared  atom so the Jobs tab
and sidebar reflect it immediately.

Backend already served both endpoints; this is desktop frontend only.
Strings added to all four locales.

* fix(desktop): stop cron blueprint cards clipping and tabs overlapping close X

Blueprint cards were wrapped in PanelBlock (a max-h-48 overflow-auto <pre>
for monospace code), which capped each card and forced an inner scroll,
clipping the copy. Use a plain auto-height card div so rows grow with their
content and the gallery scrolls as one.

PanelHeader actions sat under the overlay's absolutely-positioned close X
(no layout space reserved). Reserve pr-8 clearance when actions are present.

* refactor(desktop): narrow blueprint card i18n dep, document intentional scoping

Address review nits on the Cron Blueprints PR:
- BlueprintCard's submit useCallback depended on the whole t.cron object; it
  only uses the blueprints slice. Bind const b = c.blueprints and use it (plus
  narrow the dep) throughout the card.
- Document the intentional GET-vs-POST profile asymmetry on the blueprint
  endpoints (global catalog vs per-profile instantiate) — the prior comment
  claimed both were profile-scoped.
- Note why the blueprints tab collapses 'all' scope to 'default' (a blueprint
  creates a real per-profile job; 'all' is not a writable target).

* fix(desktop): default blueprint delivery to This desktop, not origin

The blueprint catalog is shared with the dashboard, so its deliver slot
defaults to 'origin' (the chat/home-channel a dashboard or gateway job was
created from). Desktop has no origin chat and no home-channel picker, so the
seeded 'origin' rendered unlabeled and, at delivery, fell through the
home-channel fallback to nowhere when no gateway was configured.

Seed the deliver slot to 'local' (This desktop) when the backend default is
'origin' or empty, drop the origin option from the desktop dropdown, and label
the remaining options with the desktop's own delivery labels — matching the
manual cron editor (local/telegram/discord/slack/email). Also skip the
backend's origin-centric deliver help, which contradicts desktop semantics.

* refactor(desktop): align blueprint cards with the Panel/settings idiom

Address PR review (UI consistency with neighboring surfaces):
- Card container: drop the standalone-card look (bg-foreground/5) for the
  shared in-panel grouping token bg-(--ui-bg-quinary), matching the cron
  editor's in-surface groupings so blueprints sit in the Panel family.
- Form fields: replace the bespoke <label>+<Input> rows with the shared
  ListRow primitive from settings/primitives (label+help on the left, control
  on the right, stacks in a narrow pane) — same idiom as settings/messaging.

No behavior change; blueprint deliver remap and $cronJobs merge untouched.

* fix(desktop): satisfy eslint on the cron blueprint files

CI check:lint failed on import/export ordering and an unused import in the
blueprint changes:
- hermes.ts: sort AutomationBlueprint before AuxiliaryModelsResponse in the
  type import + re-export blocks, and drop the unused AutomationBlueprintField
  import (still re-exported for blueprints.tsx).
- cron/index.tsx: alphabetize the dialog/segmented-control imports and the
  ./blueprints vs ../shell/statusbar-controls group.
- blueprints.tsx: add the required blank lines between statements.

eslint --fix only; no behavior change. typecheck + blueprint tests green.

* refactor(desktop): blueprints reuse the cron editor dialog + shared card

Address review: the blueprint UI was still going its own way on the card and
form. Reuse the app's canonical pieces instead of a bespoke surface.

- CronEditorDialog gains a 'blueprint' mode: EditorState carries the blueprint
  + target profile, the dialog renders the typed slots with the same
  Field/FieldHint/DialogFooter/error-block chrome as manual New cron, and
  submit routes to instantiateAutomationBlueprint. One dialog, one editor state
  machine. Resolves the accordion, the border-t divider, ListRow-vs-Field, the
  ad-hoc buttons, and the plain error <p> in one move.
- Blueprint cards use selectableCardClass({ prominent: true }) (the shared
  theme/pet/gateway card idiom), caller owns padding (p-2), whole card is a
  button that opens the dialog pre-filled. No inline form.
- Gallery renders via PanelDetail, not PanelBody's master/detail row.
- i18n: drop the now-unused blueprints.setUp/cancel, add blueprints.dialogDesc
  across en/ja/zh/zh-hant + types.
- Dropped the stray \u2014 literal comment.

Logic (origin->local deliver, desktopDeliverOptions, merge into $cronJobs) is
unchanged and stays unit-tested. typecheck + eslint + tests green.

* fix(desktop): dropdown no longer closes the cron dialog; unify deliver targets

Two cron-dialog bugs:

1. Dismissing any Select dropdown inside the cron editor dialog closed the whole
   dialog. Radix portals Select/Popover content outside the dialog, so the
   dismiss pointerdown reached the Dialog's DismissableLayer as an
   outside-interaction. Guard DialogContent.onInteractOutside: swallow
   interactions originating from a [data-radix-popper-content-wrapper] (a
   dropdown dismiss inside our own dialog), compose with any caller handler. Fix
   is at the shared Dialog level so every dialog benefits.

2. Blueprint deliver only offered 'This desktop'. The blueprint used the backend
   blueprint field.options (configured gateways only) while the manual editor
   hardcoded local/telegram/discord/slack/email regardless of what's connected.
   Wire the desktop to GET /api/cron/delivery-targets (the documented single
   source of truth, already used by the dashboard) via getCronDeliveryTargets,
   and render both the manual editor and the blueprint deliver slot through one
   shared DeliverSelect. Now all three surfaces agree and only offer connected
   platforms; unconfigured-home-channel targets show a hint.

i18n: add cron.deliverNeedsHomeChannel across en/ja/zh/zh-hant + types.
typecheck + eslint + vitest green.

* fix(desktop): clicking away from an open dropdown no longer closes the dialog

The onInteractOutside guard only caught pointerdowns whose target was inside
the popper wrapper. But dismissing an open Select by clicking elsewhere inside
the dialog also closes the popover, which moves focus — and Radix Dialog reads
that as focusOutside and closes the whole dialog. (Radix Select 2.3.1 has no
modal prop, so that escape hatch isn't available.)

Guard both paths at the shared DialogContent level: onInteractOutside AND
onFocusOutside now swallow the event when it originates from a Radix popper OR
when any [data-radix-popper-content-wrapper] is open at event time (covers the
focus/re-dispatch case where the target is no longer the popper). A genuine
backdrop click with no dropdown open still closes the dialog. Export
isInteractionFromPopper + unit-test the three cases.

typecheck + eslint + vitest green (7 dialog tests).

* fix(desktop): portal popovers into their dialog so dropdowns don't close it

Root cause (affected every dialog, not just cron): Radix Select/Popover/
DropdownMenu portal to document.body — a SIBLING of the dialog, outside its DOM
subtree. Dismissing a dropdown (or clicking another field) moves focus out of
the dialog subtree, which the Dialog's modal FocusScope/DismissableLayer reads
as an outside interaction and closes the whole dialog. Separate body-level
portals also make z-index across the two fragile.

The earlier onInteractOutside/onFocusOutside guards treated symptoms and didn't
hold (and Radix Select 2.3.1 has no modal prop to disable its layer). Real fix
is a layering system: DialogContent publishes its content node via
DialogPortalContainerContext; SelectContent/PopoverContent/DropdownMenuContent
call usePopoverPortalContainer() and portal INTO that node when inside a dialog
(document.body otherwise). The popover is then a true DOM descendant of the
dialog — focus stays in, dismissal no longer closes the dialog, and both share
one stacking context so z-index is deterministic.

Test: with a Dialog open, an open Select's item is a descendant of the dialog
(portalled in), verified in jsdom. typecheck + eslint + component tests green.

* fix(desktop): bump radix-ui so dismissing a dropdown can't close its dialog

Upstream bug, not app-layer: with radix-ui 1.6.0 (dismissable-layer 1.1.13),
an open modal Select sets pointer-events: none on the dialog body, so a click
anywhere inside the dialog hit-tests through to the overlay. The Dialog's
DismissableLayer defers its outside-pointerdown decision to the click, but the
overlay is a registered dismissable surface exempt from the interception check
— so the Select swallowing the press didn't count, the deferred onDismiss
fired, and the dialog closed along with the dropdown.

dismissable-layer 1.1.17 adds shouldHandlePointerDownOutside, which makes the
dialog's layer ignore the press entirely while a higher layer has its pointer
events disabled. Bump radix-ui ^1.4.3 -> ^1.6.5 (dismissable-layer 1.1.17,
dialog 1.1.21, select 2.3.5); lockfile diff is Radix-only.

Repro test fires the pointerdown/up/click sequence on the overlay with a
Select open inside the dialog: red on 1.6.0 (dialog closed), green on 1.6.5.
Counterpart test keeps a genuine overlay click closing the dialog.

typecheck + dialog/select component tests green. Full-suite failures on this
Windows host reproduce identically without the bump (pre-existing env issues).
2026-07-24 14:22:43 -04:00
cat-that's-fat
7dd2b2dc77 feat(desktop): add "Connect to existing Hermes" option to first-run onboarding
Adds a first-run Desktop choice between installing Hermes locally and
connecting to an existing remote Hermes gateway. The choice appears after
backend resolution but before ensureRuntime(), so selecting remote cannot
accidentally trigger local bootstrap.

New modules:
- first-run-setup-gate: concurrent first-run decision gate and reset semantics
- primary-backend-startup: Electron-free orchestration seam (saved remote
  resolution, gate decision, remote re-resolution, local continuation)
- primary-connection-rehome: prevents dual-owner race where both cold boot()
  and renderer softSwitch() could connect simultaneously
- first-run-remote-form: extracted remote form with stale-result guards

Reuses existing connection-config IPC, encrypted token storage, OAuth
session partition, and primary backend resolution.

Fixes #38602
Fixes #36970
2026-07-24 12:55:06 -05:00
hermes-seaeye[bot]
7c9d05267c
fmt(js): npm run fix on merge (#69939)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-23 07:08:53 +00:00
Brooklyn Nicholson
d010220588 fix(desktop): de-dupe credit toast icon, band-color the figure, split detail
Three fixes to how agent credit notices render as toasts:
- Strip the leading severity glyph (the toast already draws a kind icon, so the
  raw text doubled it). Native OS notifications keep the glyph (no icon there).
- Icon top-margin is now 0.42ch (font-relative) instead of a fixed rem.
- Band-color the $used figure (semibold) by $used/$cap: muted <75%,
  --ui-orange >=75%, --ui-red >=90% (depleted red, restored green), reusing the
  existing --ui-* usage palette. Icon shares the accent.
- Split a trailing '. detail' into a muted secondary line (title+description
  convention) instead of an inline middot.
Generic 'accentColor' + 'meta' slots on the notification; degrades gracefully
when a notice has no figure.
2026-07-23 01:28:59 -05:00
ethernet
26f1f6a76b
feat(desktop): improve tool call detail views (#69868)
* fix(desktop): improve fallback tool-call details

Show failed image-generation calls through the normal fallback row, remove duplicate normal-mode web-search JSON, and format Technical Mode payloads as readable JSON.

* feat(desktop): render terminal tool calls as transcripts

Show terminal commands with a prompt and exit status, then reveal ANSI-safe stdout and stderr in the expanded tool row.

* fix(desktop): reconcile tool calls by command

Match context-only tool starts with command-bearing completions when their IDs differ, preventing stale duplicate terminal rows. Show the web-search query above its result cards.
2026-07-23 01:27:38 -04:00
Gille
7cbdcf1ba6
fix(desktop): render remote markdown images in chat (#57944)
* fix(desktop): render remote markdown images in chat

* test(desktop): cover remote markdown image resolution
2026-07-23 00:16:20 -05:00
brooklyn!
e1367b44bc
fix(desktop): let clarify choices and overlays keep their keys from type-to-focus (#69869) 2026-07-23 00:04:12 -05:00
brooklyn!
6096f73ce8
feat(desktop): add keyboard navigation to clarify choices (#69799)
Co-authored-by: Mapurite <272619650+mapu-og@users.noreply.github.com>
2026-07-23 04:48:32 +00:00
brooklyn!
dc861964fc
fix(desktop): clarify options defensive rendering and layout fix (#69796)
- Replace undefined  CSS class with Tailwind v4's
   so long choice text wraps properly
  instead of overflowing the button container.
- Add  validation that strips non-string items,
  blanks, newlines, and text >200 chars — preventing garbage/JSON
  arrays from rendering as raw values.
- Add diagnostic  logging at both the gateway event
  handler and the tool-args parser when choices are dropped, so
  malformed payloads are no longer silent.
- Apply  in both the gateway event handler
  () and the inline tool-args parser
  () for consistent defense in depth.
- Add unit tests for  covering null, non-array,
  mixed-type, blank, multiline, and overlong inputs.

Closes #69122

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
2026-07-23 04:32:33 +00:00
Brooklyn Nicholson
936b407a72 fix(desktop): free code-block scrollbar & last-line selection from the toggle overlay
`ExpandableBlock` used a single full-width `absolute inset-x-0 bottom-0`
button that was BOTH the fade overflow cue and the expand/collapse control.
It sat on top of the whole 28px bottom strip, so it swallowed:

- the horizontal scrollbar of a wide code block (couldn't scroll sideways), and
- pointer/drag events on the block's last line (couldn't select/copy the tail).

Split the two responsibilities: the full-width fade stays as a pure
`pointer-events-none` cue, and the only clickable target is a compact toggle
pinned to the right edge (`pointer-events-auto`), clear of the draggable
scrollbar track. The inner container gains `overflow-x-auto` so wide code
gets a working scrollbar. Applies uniformly to code cards and the plain-text
fallback — no per-call prop needed.

Regression test asserts the pointer-events contract (fade `pointer-events-none`
+ full-width, toggle `pointer-events-auto` + right-pinned, not `inset-x-0`) and
that the toggle still flips `aria-expanded`.

Supersedes #69558, #69428
Fixes #69168

Co-authored-by: alelpoan <alelpoan@proton.me>
Co-authored-by: BerneYue <xiongyue_hnu@163.com>
2026-07-22 23:23:39 -05:00
brooklyn!
88a9557385
Merge pull request #69812 from NousResearch/bb/desktop-tab-close-shift
fix(desktop): session-tab UX — ⌘W tab-shift, draft new-tab, unified status dot, stable lanes & optimistic delete
2026-07-22 23:01:17 -05:00
Brooklyn Nicholson
87aaf87748 fix(desktop): render session status dot from one primitive (sidebar, tiles, main tab)
The sidebar row and the pane tabs each painted their own dot from different data: the sidebar read color from $sessionColorById[id] (map-only, no resolver fallback) layered with live state, while a tab painted a flat 'accent' color via sessionColorFor (map + fallback). A session older than the recents page missed the map, so the same session showed grey in the sidebar and its project color in the tab.

Add a single SessionStatusDot primitive keyed by the stored session id (the key every live-state atom already uses) that resolves color (override -> project, with fallback) and live state (working/needs-input/stalled/unread/background) itself. The sidebar row, session tiles, and the main workspace tab all render it, so a session's status/color can never disagree across surfaces. Tabs gain the full live status (pulse etc.), not just a static color. Collapses the now-orphaned generic 'accent' tab-dot path into the one primitive.
2026-07-22 22:43:51 -05:00
hermes-seaeye[bot]
7d96e602a9
fmt(js): npm run fix on merge (#69805)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-23 03:32:32 +00:00
Brooklyn Nicholson
067cb9e033 feat(desktop): shift next tab into main on ⌘W + always-shown new-session tab
- ⌘W on the main tab promotes the next stacked session tab into main
- '+' / ⌘T open a new 'New session' tab, unlisted until first message
  (no sidebar pollution); multiple new-session tabs allowed
- tile resolves its own row via by-id lookup once it has a message
2026-07-22 22:18:33 -05:00