Commit graph

1770 commits

Author SHA1 Message Date
Brooklyn Nicholson
070093a318 feat(desktop): on by default for dev-server runs
Gating this behind an opt-in was the wrong call. A dev server already
executes arbitrary local JS — vite's module graph, every postinstall in
node_modules — so a loopback debugging port does not meaningfully widen
what a `npm run dev` session can already do, and `perf:serve` has opened
one unconditionally all along.

Requiring the variable also defeated the point: the tooling exists to be
reached for mid-task, and a capability you must remember to enable before
launching is one you don't have when you need it.

So the port opens on 9222 — the same port scripts/eval.mjs and
scripts/perf/lib/cdp.mjs already default to — for any dev-server run.
HERMES_DESKTOP_CDP_PORT stops being an on-switch and becomes an
override: a different port, or `off` to disable.

The hard gate is unchanged and still checked first: a packaged build
never opens the port, and no env value talks it into it. Neither does an
unpackaged `electron .` against dist/, which is how the packaged app
gets smoke tested.

Refusals only log when they contradict something the developer asked for
(a typo'd port, an explicit `off`). Packaged and dist runs are closed by
design and stay quiet.
2026-07-27 23:52:33 -05:00
Brooklyn Nicholson
6254c568c8 feat(desktop): opt-in renderer debugging port for dev runs
The renderer is a Chromium page, and apps/desktop already carries a whole
CDP toolkit for it — scripts/eval.mjs, scripts/perf/lib/cdp.mjs with its
shared SELECTORS map, and the diag-*/probe-* family. None of it can
attach to `hgui` or `npm run dev`, because neither passes
--remote-debugging-port. The only launcher that opens one is
`npm run perf:serve`, which is a separate isolated instance rather than
the app you're looking at.

Add HERMES_DESKTOP_CDP_PORT. When set, the shell opens a CDP port on
loopback so that existing tooling can read the live DOM: computed
styles, geometry, which rule actually won.

Three independent gates, all required, resolved by a pure function in
electron/dev-cdp.ts so the policy is testable without an Electron app:

  1. not packaged — a shipped build never opens the port, and this is
     checked first so no env combination can talk it into doing so;
  2. HERMES_DESKTOP_DEV_SERVER present — an unpackaged `electron .`
     against dist/ is how the packaged app gets smoke tested, so it
     behaves like the packaged app here;
  3. the port explicitly requested and a valid integer.

Default `npm run dev` is unchanged and silent: no port, no nag. An
opt-in that gets refused always logs why, so nobody loses an hour
wondering what isn't listening.

The address is pinned to 127.0.0.1 rather than left to Chromium's
default, and is deliberately not configurable — there's no reason to
expose a renderer debugger off-host and offering the knob invites
someone to try.

scripts/eval.mjs hardcoded :9222 and threw a raw ECONNREFUSED stack when
nothing was there. It now honours the same variable and explains itself.
2026-07-27 23:23:47 -05:00
hermes-seaeye[bot]
3af7b867fd
fmt(js): npm run fix on merge (#73107)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 03:59:15 +00:00
brooklyn!
56aacbdb01
Merge pull request #73101 from NousResearch/bb/queue-double-send
feat(desktop): double-Enter sends the queued turn
2026-07-27 22:50:51 -05:00
Brooklyn Nicholson
ac8310bcc2 test(desktop): cover the busy empty-Enter double-send 2026-07-27 22:43:42 -05:00
hermes-seaeye[bot]
9f4a6fdf4a
fmt(js): npm run fix on merge (#73096)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 03:43:07 +00:00
Brooklyn Nicholson
84d77b608c feat(desktop): double-Enter sends the queued turn, and the row says Enter
Empty Enter while busy was a hard no-op, so a queued turn could only be
sent early via the panel's send arrow or Cmd+Shift+K. With prompts
queued, a second Enter now promotes the head and interrupts, mirroring
the idle empty-Enter drain. Nothing queued keeps the old no-op.

The panel's send-now row action switches from a bare up-arrow to the
return glyph, so the row states the keybind the double-send uses.
2026-07-27 22:42:21 -05:00
brooklyn!
ef26701134
Merge pull request #73089 from NousResearch/bb/desktop-sidebar-counts
Drop the remaining session counts from the sidebar
2026-07-27 22:33:09 -05:00
Brooklyn Nicholson
67ea00327d style(desktop): drop the remaining sidebar session counts
#72336 removed the count from the flat list and #72912 from the entered
project, but three sites still tallied rows: search results, each
messaging group (Telegram, Discord, …), and cron jobs.

With them gone SidebarCount never renders a count — its one caller is
the projects-loading spinner — so rename it SidebarSectionMeta.
2026-07-27 22:27:26 -05:00
brooklyn!
202140db53
Merge pull request #73074 from NousResearch/bb/slash-after-command
fix(desktop): keep slash completion alive after a leading command
2026-07-27 22:16:35 -05:00
brooklyn!
c1964f977b
Merge pull request #73047 from NousResearch/bb/link-brand-icons
feat(desktop): brand icons on links to known domains
2026-07-27 22:16:24 -05:00
brooklyn!
9ed212e096
Merge pull request #73062 from NousResearch/bb/sane-tooltips
fix(desktop): stop tip-wrapping kebab menu triggers
2026-07-27 22:16:10 -05:00
brooklyn!
ea0775eb8b
Merge pull request #73073 from NousResearch/bb/composer-image-lightbox
fix(desktop): open a composer image attachment in the lightbox, not the rail
2026-07-27 22:16:01 -05:00
Brooklyn Nicholson
015353d970 test(desktop): cover short actions labels and tip-less close buttons 2026-07-27 22:03:37 -05:00
Brooklyn Nicholson
970c4b5336 fix(desktop): shorten Actions aria-labels and tautological copy
Use short static labels (Session actions / Actions) instead of
Actions for <name>. Trim toast fallback, settings echoes, YOLO click
lectures, and fat gateway/appearance/notifications intros.
2026-07-27 22:03:37 -05:00
Brooklyn Nicholson
9b5c15727c fix(desktop): strip Close-X tips and statusbar tip lectures
Drop tips on dialog/overlay/find-bar/review/master-detail close buttons.
Stop tip-wrapping connection/gateway/timer/context/cron/webhook chrome
that already names itself on screen; keep tip only when there's a real
gateway reason. Drop the titlebar swap paraphrase.
2026-07-27 22:03:37 -05:00
Brooklyn Nicholson
5dadacfce7 docs(desktop): ban Close-X tips and click-to chrome lectures
Extend the tip rule past kebabs — no tip on dismiss X, and no tips that
only paraphrase a visible label or say "click to…".
2026-07-27 22:03:37 -05:00
Brooklyn Nicholson
eb851d619e fix(desktop): keep slash completion alive after a leading command
Typing a second slash command in the composer went dead whenever the
message started with one. `/work /cle` offered nothing, while `do /work
then /cle` completed fine — which read as an intermittent glitch rather
than a rule.

Two regexes detect a slash, and the `^`-anchored command shape was tried
first. Its argument tail (`(?:\s+\S*)*`) swallows the rest of the line,
so a later `/skill` parsed as an argument to the first command; a command
that takes no options then suppresses the popover outright, and every
slash after the first was unreachable.

Only the first slash can be an invocation, so detect the inline shape
first. It requires a whitespace-preceded slash sitting at the caret, so
it can't take over ordinary argument completion (`/personality alic` has
no second slash) — it fires only where completion was already dead.
2026-07-27 22:02:06 -05:00
Brooklyn Nicholson
71e66f3a11 test(desktop): cover the composer attachment click routing
An image attachment opens the lightbox and leaves the preview rail empty; a
file attachment still opens a rail tab. The lightbox case fails on main.
2026-07-27 22:00:31 -05:00
Brooklyn Nicholson
d8cb73b4ab fix(desktop): open a composer image attachment in the lightbox, not the rail
Clicking an attached image routed it through normalizeOrLocalPreviewTarget and
into the right rail, where it rendered as a small contained <img> inside a
pane built for reading and editing files. The bytes were already in hand as a
data URL, so the rail's whole read/edit/reload apparatus was doing nothing for
a picture the user just wanted to look at.

Route images with a resolved thumbnail to ImageLightbox — the same overlay the
thread uses — so the attachment previews at full size with the download action,
and drop the dataUrl/previewKind graft that only existed to make the rail
render bytes it shouldn't have been handed. Non-image attachments, and images
whose thumbnail never resolved, still fall through to the rail unchanged.
2026-07-27 22:00:31 -05:00
Brooklyn Nicholson
2e61feb94e fix(desktop): anchor the inline image download button to the image, not the block
The <img> carried max-w-[min(100%,var(--image-preview-max-width))] while its
container was w-fit max-w-full. A percentage max-width resolves to none while
the container measures its fit-content width, so the container took the full
column while the image stayed capped — and the absolutely positioned download
button, which anchors to the container, drifted into the margin on any image
wider than it is tall.

Move the width cap onto the container and leave the image at max-w-full. The
container now shrink-wraps the rendered image, so right-2/top-2 lands on the
image's own corner at every aspect ratio.
2026-07-27 21:54:05 -05:00
Brooklyn Nicholson
df7352aec6 style(desktop): quiet inline links to color-only until hover
The resting chip fill and the bold weight both fought the brand mark for
attention. Links now sit in the theme's primary color at body weight, and
the tint fades in on hover.

Weight is overridden on .link-chip itself: `@tailwindcss/typography` sets
`prose a { font-weight: 500 }`, which outranks a utility class on the anchor,
so the per-call-site classes could never win.
2026-07-27 21:45:42 -05:00
Brooklyn Nicholson
1910d613ee test(desktop): assert kebabs are not wrapped in Tip
Flip the #67500 structure tests that required tooltip-trigger on menu
kebabs; keep coverage that the menus still open on click.
2026-07-27 21:43:57 -05:00
Brooklyn Nicholson
26413bbf57 fix(desktop): stop tip-wrapping kebab menu triggers
Remove the ActionsMenu tooltip prop and Tip wrappers on session, project,
workspace, panel, and credential ⋯ menus. aria-label stays for a11y; drop
the unused credentialActions string.
2026-07-27 21:43:57 -05:00
Brooklyn Nicholson
58714e0e2a docs(desktop): tip only when hover teaches something new
Drop the blanket "every icon* button needs a Tip" rule — it produced
tautological kebab tips like "Actions for <row title>". Menu triggers keep
aria-label; tips stay for unlabeled discovery chrome and keybind hints.
2026-07-27 21:43:56 -05:00
brooklyn!
84858d76ba
Merge pull request #73054 from NousResearch/bb/codeblock-chromeless
Chromeless markdown code blocks
2026-07-27 21:42:51 -05:00
Brooklyn Nicholson
b3174092f6 chore(desktop): drop the now-unused assistant.tool.code string
The code-block header was its only consumer.
2026-07-27 21:28:00 -05:00
Brooklyn Nicholson
39bde52014 fix(desktop): let the expandable fade match its own surface
The overflow fade hardcoded --ui-chat-surface-background, so inside a code
block it smeared the chat background over the block's own fill. Read an
--expandable-fade-from override with the chat surface as the default.
2026-07-27 21:28:00 -05:00
Brooklyn Nicholson
b7fc36cf3c feat(desktop): strip the header chrome off markdown code blocks
A fenced block carried a bordered card with a "Code · <lang>" title row and a
pinned copy button, which read as an attached artifact next to the reply. Drop
the border and the header: the block is now a tinted slab on --ui-bg-editor
with syntax highlighting and a copy button that reveals on hover. The streaming
glow moves entirely to box-shadow since there is no border left to animate.
2026-07-27 21:28:00 -05:00
Brooklyn Nicholson
23459c0361 feat(desktop): show brand icons on links to known domains
PrettyLink leads with the site's mark, so a GitHub PR link reads as a GitHub
link at a glance. The artifacts pane uses the same lookup in place of its
generic chain glyph; unknown hosts render as before.
2026-07-27 21:23:46 -05:00
Brooklyn Nicholson
0d19e93f5d feat(desktop): resolve a hostname to its Simple Icons brand mark
A lookup table of ~170 hosts plus a resolver that walks the hostname's
suffixes, so subdomains inherit their parent's mark (gist.github.com) while
a more specific entry (docs.google.com) still wins over the general one.
2026-07-27 21:23:46 -05:00
brooklyn!
fa7b0fcf5d
Merge pull request #73035 from NousResearch/bb/clarify-composer-enter
fix(desktop): let a typed message answer past a clarify prompt
2026-07-27 21:09:16 -05:00
brooklyn!
cfd79e5b1b
Merge pull request #73038 from NousResearch/bb/palette-active-theme
fix(desktop): show a check on the active theme and color mode in ⌘K
2026-07-27 21:07:21 -05:00
brooklyn!
7100e8d539
Merge pull request #73041 from NousResearch/bb/tab-strip-scroll-into-view
fix(desktop): keep the active tab and the "+" in view when the tab strip scrolls
2026-07-27 21:04:26 -05:00
brooklyn!
b6244959a6
Merge pull request #73040 from NousResearch/bb/context-menu-parity-2
Right-click parity for cron, webhooks, and profile rows
2026-07-27 21:03:32 -05:00
Brooklyn Nicholson
dfe3a23a60 fix(desktop): scroll the tab strip so a new tab and the "+" stay in view
Opening a tab in a zone with more tabs than fit appended it past the right
edge of the scrolling strip — the new tab and the "+" that created it were
both off-screen, so a second new tab meant scrolling back by hand. Activating
a tab from a keybind had the same problem in the other direction.

The zone header now scrolls its active tab into view on activation, and
scrolls all the way to the end when that tab is the last one so the trailing
"+" comes with it.
2026-07-27 20:57:32 -05:00
Brooklyn Nicholson
9b84c6a046 feat(desktop): right-click actions on the sidebar cron rows
The sidebar cron rows exposed only hover trigger/manage buttons and no
right-click. Add a context menu — trigger now, pause/resume, manage, and
delete — driven against the shared $cronJobs atom so the sidebar and cron
overlay stay in sync.
2026-07-27 20:56:57 -05:00
Brooklyn Nicholson
48368bc4a7 feat(desktop): right-click parity for every panel list row
Teach PanelListRow a menuItems prop that renders BOTH the hover kebab and a
matching right-click menu from one PanelMenuItem[] (via the shared actions-menu
primitive), so a panel row's two menus can't drift. Migrate the cron, webhooks,
and profiles panels onto it — right-clicking a row now opens the same
edit/delete/enable actions as its kebab.
2026-07-27 20:56:48 -05:00
Brooklyn Nicholson
69f72f2b7c fix(desktop): show a check on the active theme and color mode in ⌘K
The theme picker computed `active` per row but nothing rendered it — the
`active?: boolean` field and its trailing check were stripped in 8f73d0d94
and the computation was left behind, so the palette offered no way to tell
which skin was already applied. That matters more than it sounds: the
desktop persists its own skin in localStorage and only seeds (never
applies) the backend's `skin:` at connect, so config and window can
legitimately disagree.

Restore the field and render a trailing check, and mark the root-search
theme rows and both color-mode lists the same way so every appearance
picker in the palette reports its own current value.
2026-07-27 20:56:03 -05:00
Brooklyn Nicholson
c0dd6e1f3f fix(desktop): stop a clarify card swallowing keys it doesn't bind
The card marked itself as owning every printable key, so the first letter
of a typed-out answer vanished and the composer never focused. It now
publishes its row count and yields only Enter plus the 1..N+1 / A.. rows
it actually renders; everything else reaches the composer, which skips
the question on send.
2026-07-27 20:50:32 -05:00
Brooklyn Nicholson
251b668f4d fix(desktop): let the composer answer past a clarify card
Typing a real message instead of picking an option left the agent parked:
the clarify blocks inside its tool batch waiting on clarify.respond, so a
follow-up routed through steer/queue sat undelivered until the 5-minute
clarify timeout. skipClarifyRequest answers the parked question with the
same empty answer the card's own Skip button sends, so the tool returns
and the turn carries on with the user's words.
2026-07-27 20:50:32 -05:00
Brooklyn Nicholson
1baea21cfb perf(desktop): load shiki and @streamdown/code on first use, not at boot
The chunk split only helps if nothing on the entry graph statically
imports the heavy libs. Three seams did:

- react-shiki: now behind one lazy() boundary (shiki-block.tsx is its only
  static importer; LazyShiki wraps it with a PlainCode fallback).
- diff-lines.tsx: useShikiHighlighter hook extracted to a lazily-loaded
  syntax-diff.tsx (plain DiffBody fallback — same output Shiki's own
  pre-resolve state showed); codeToTokens becomes a dynamic import that
  runs only once a highlightable diff is on screen.
- @streamdown/code: statically ran createJavaScriptRegexEngine() and built
  full language registries at module scope on every launch. It now loads
  via useCodePlugin() on first markdown mount and swaps into the plugin
  table when it lands; fenced code renders plain until then, identical to
  the delay fallback users already see during streaming.

Interleaved A/B vs main (prod build, warm V8 cache, median of 4, two
alternating rounds): FCP 1100->940ms, DOMContentLoaded 618->542ms,
nav-to-interactive 1312->1123ms on the quiet round; same direction with
larger gaps under load. ~160-190ms off every renderer boot metric.
2026-07-27 20:44:35 -05:00
Brooklyn Nicholson
6fb5d2d89c perf(desktop): split heavy lazy-only libs out of the renderer entry chunk
codeSplitting:false inlined every lazy()/dynamic import into the entry, so
the whole 28.5 MB bundle — including 19 MB of shiki grammars and 3.1 MB of
mermaid that nothing rendered — was parsed and evaluated on every cold
start. The original single-chunk rationale (#38888: electron-builder OOMs
scanning shiki's thousands of default chunks) doesn't require ONE chunk,
just few: rolldown advancedChunks groups keep the file count at ~180.

Ordering matters and is the subtle part: shared foundations (react, hast/
mdast utils, lodash-es/d3 commons) must match BEFORE the heavy groups,
because rolldown merges an unmatched shared module INTO the heavy chunk
that uses it — and then the entry statically imports 19 MB of shiki just
to reach react, putting the whole chunk back on the boot path.

Statically-reachable boot graph: 26.9 MB -> 7.7 MB.
2026-07-27 20:44:35 -05:00
hermes-seaeye[bot]
71e7eb3c16
fmt(js): npm run fix on merge (#73023)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 01:20:15 +00:00
brooklyn!
fe71e32a87
Merge pull request #72893 from NousResearch/bb/desktop-activity-grouping
Collapse a turn's tool activity into one grouped, live-ticking line
2026-07-27 20:11:28 -05:00
Brooklyn Nicholson
83cc5831fd fix(desktop): resolve punctuation keybinds through the active layout
Letter chords now follow `event.key`, but punctuation was still anchored
to the physical QWERTY position, so the shipped punctuation defaults stayed
unreachable on a remapped layout. On Dvorak `mod+.` (command center) reads
the physical V key, and `mod+,` / `mod+/` land on `w` / `[`.

Take `event.key` for unshifted punctuation too. Shift stays excluded because
a shifted `event.key` is the shifted glyph ("?" for "/") and combos are
anchored to the unshifted token. Digits stay physical: AZERTY types "&" on
the unshifted "1" key, so `event.code` is what keeps `mod+1` bound. Glyphs
we do not ship as tokens — Option output, dead keys, non-Latin scripts —
fail both checks and fall back to the physical code.

The punctuation set derives from CODE_TO_KEY so the two cannot drift.
2026-07-27 19:57:51 -05:00
LeonSGP43
b2bfab4837 fix(desktop): honor layout-aware letter keybinds 2026-07-27 19:54:55 -05:00
Brooklyn Nicholson
bc8933042f fix(desktop): stop the enter animation pinning opacity over the stylesheet
Two identical tool rows, one above the other, rendered at two different
opacities — and no amount of hovering would even them out.

The enter animation fills forwards, so its final keyframe is held in the
animation origin of the cascade for as long as the element lives, above the
author stylesheet. Naming `opacity: 1` there didn't just end the fade, it
permanently overruled the resting opacity of every element the sheet dims.
Transcript scaffolding is dimmed exactly that way, so a row kept whichever
opacity it happened to mount with: full if it animated in during the turn,
faded if it was rehydrated or remounted past its one-shot key. Same for
thinking headers, which is why "Thought" never matched the rows near it.

Leave the end opacity out of the keyframe. It animates up to whatever CSS
asks for and keeps answering to it, hover included.

Then close the way the surfaces drifted in the first place: the fade named
each one in its own selector, so the live status line — added later, and
neither tool nor thinking nor prose — matched none of them and sat a shade
brighter than the rows either side. One `data-conversation-scaffold` mark
now carries it, and every surface opts in.
2026-07-27 19:54:47 -05:00
hermes-seaeye[bot]
d89512107e
fmt(js): npm run fix on merge (#73004)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 00:46:46 +00:00
Brooklyn Nicholson
7a10e48e2f fix(desktop): one weight and one gap for transcript scaffolding
The scaffold rows shared a colour but not a weight: tool summaries and the
ticker rendered medium against the reply's normal-weight prose, which read
as emphasis on the quietest lines in the column.

Spacing had the matching problem. The block-gap rule listed which *pairs* of
blocks qualified, so the live status line — neither tool, thinking, nor
prose — fell through every branch onto its own half-size margin. And because
a streaming turn is sealed into several bubbles as it goes but rehydrates
into fewer, two blocks are siblings inside one bubble or split across two
depending on when you look; the flex gap between bubbles is half the block
gap, so the rhythm tightened and relaxed as a turn settled. Cover every
top-level block with one rule, keep prose-to-prose on paragraph rhythm, and
top the between-bubble gap up to match.
2026-07-27 19:35:04 -05:00