Commit graph

523 commits

Author SHA1 Message Date
Brooklyn Nicholson
d76c7f5409 perf(desktop): memoize PlatformAvatar + StatusDot to kill messaging churn
The sidebar's messaging section renders one PlatformAvatar (labelIcon) and
StatusDot per platform group. The sidebar re-renders on every streaming tick
($sessions/$workingSessionIds/$messagingSessions churn), and both leaves were
unmemoized — so every platform's avatar + dot re-rendered on each delta even
though their props (platformId/tone/className) never changed.

- PlatformAvatar: memo(forwardRef(...)) — pure fn of platformId/name/class/style
- StatusDot: memo() — pure fn of tone/class

Measured (Messaging open, settled, 2 sessions streaming, 3s window):
  before: 96 wasted (PlatformAvatar 32, StatusDot 32, + brand icons)
  after:  0 wasted

Both are shared primitives; the memo also helps every other consumer
(session rows, gateway menu, session tiles) that renders them under a hot
parent.
2026-07-28 20:51:04 -05:00
Brooklyn Nicholson
9b01c74f00 feat(desktop): render the micro-action and underside strips
Pills pin to the top of the composer's overlay lane, outside the status
card and outside its scroller, so nothing stacks above them and a long
todo list can't scroll them away. The underside slot sits below the
surface. Both share one grid constant and one parent, so their left
edges match by construction rather than by matching numbers in two
files.

The strips take pointer events while their empty space falls through to
the pop-out drag region, which keeps the composer draggable by the band
its badges live in.
2026-07-28 19:20:00 -05:00
brooklyn!
6c2a3b4bb2
Merge pull request #73673 from NousResearch/bb/desktop-eventbus
Desktop: event-driven live sync — gateway change broadcasts replace the always-on polls
2026-07-28 17:39:40 -05:00
Brooklyn Nicholson
fe64cafa33 feat(desktop): event-driven live sync — change broadcasts replace the always-on polls
One controller (store/live-sync.ts, the workspace-events twin for gateway
state): gateway-event.ts routes pet.changed / cron.changed /
sessions.changed into tick atoms, gated on the active profile like
skin.changed; gateway.ready's change_events flag arms them; a gateway wipe
resets the capability.

Consumers subscribe to ticks and demote their polls to slow backstops
(legacy cadence is kept verbatim against older backends):

- session.active_list 1.5s → sessions.changed + 30s backstop
- cron list 30s → cron.changed + 5min backstop
- messaging lists 10s → trailing sessions.changed refresh + legacy poll
  only when events are unavailable
- open messaging transcript 5s → sessions.changed + 30s backstop
- pet.info 3s/15s → pet.changed, NO timer on event-capable backends
  (users with no pet used to poll hardest); enabled=false broadcasts
  clear the mascot with zero round-trips
- cron runs page/peek 8s → cron.changed + 60s backstop

Idle renderer traffic drops from ~89 req/min to the backstops (~5/min).
Part of #73618.
2026-07-28 17:21:58 -05:00
brooklyn!
43ddefd08f
Merge pull request #73675 from NousResearch/bb/edit-models-hover
fix(desktop): match Edit Models row hover to the model picker
2026-07-28 17:19:44 -05:00
brooklyn!
f9d7bca252
fix(desktop): overlay scrollbars on conversation code blocks (#73670)
The app-wide `.scrollbar-dt *` webkit rules force classic always-on
gutters on every descendant scroller. Opt code-card scroll surfaces out
via `.scrollbar-overlay` so Electron keeps platform overlay bars (fade
in on scroll, no reserved track).
2026-07-28 22:13:54 +00:00
Brooklyn Nicholson
6d83a79f93 fix(desktop): match Edit Models row hover to the model picker 2026-07-28 17:13:20 -05:00
Atakan
15317e1fd7 fix(desktop): upload Windows attachments to Linux backends 2026-07-28 12:36:01 -07:00
Max Hsu
181511473e fix(desktop): render video/audio image-markdown as media, not broken <img>
Generated media often arrives as image markdown (![clip](clip.mp4)). The chat
markdown renderer maps the img element to MarkdownImage, which renders a raw
<img>; for a video/audio source the browser cannot paint it and shows a
broken-image icon even though the file is valid and plays fine. Images worked
only because <img src=...png> is valid markup.

Route video/audio sources in MarkdownImage to the existing MediaAttachment,
which already picks the correct <video>/<audio> element (streaming protocol +
open-externally fallback) by media kind. The intended MEDIA:-tag -> #media:
link path is unaffected; this only fixes the bare image-markdown case.

Since #57944 the image path is built on hooks (async src resolution, failure
and loading states), so the routing check cannot be a plain early return
inside it -- it would have to sit after every hook call and would still fire
an image resolve for media never rendered as an image. MarkdownImage is
therefore a thin hookless router in front of MarkdownImageContent, which is
the previous body unchanged.

The regression tests join the existing markdown-text.media.test.tsx added by
#57944 rather than replacing it.

Fixes #40896

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vU45SEvcxLkVeyyEE9gJ2
2026-07-28 11:58:03 -07:00
Ares4Tech
3e47efeb25 fix(desktop): stream remote media through gateway 2026-07-28 11:58:03 -07:00
hermes-seaeye[bot]
2faac36866
fmt(js): npm run fix on merge (#73552)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 18:30:19 +00:00
brooklyn!
87a37b9492
Merge pull request #73247 from NousResearch/bb/skill-chip-only
Render a /skill turn as its invocation, never the skill body
2026-07-28 04:22:07 -05:00
brooklyn!
dcc543af9a
Merge pull request #73172 from NousResearch/bb/featured-models
feat(picker): curated model defaults + collapsible providers + select-all in Edit Models
2026-07-28 04:09:46 -05:00
Brooklyn Nicholson
b3ba5570c9 Merge origin/main into bb/skill-chip-only
Keep skill-invocation projection helpers alongside the auto-continue
legacy display typing from #73250.
2026-07-28 04:06:32 -05:00
Brooklyn Nicholson
9ffb35c3c7 feat(desktop): collapsible providers + select-all + search in Edit Models
Mirror the picker dropdown's provider collapse into the Edit Models dialog so
curating is one click per provider instead of scrolling through 30 models.
Each provider header is a full-width clickable label (same style as the
composer context-menu labels) with a DisclosureCaret next to the text and a
select-all Checkbox (indeterminate when partial). Model rows stay Switches.
The dropdown's collapse is fixed too: the current provider is now
collapsible (was forced open), and the label style matches the rest of the app.
Adds a search icon to the dialog input matching every other search field.
2026-07-28 03:58:33 -05:00
Brooklyn Nicholson
9a6b69d9af feat(ui): indeterminate support on the shared Checkbox
Add data-[state=indeterminate] styling (same primary fill as checked) and a
dash glyph that shows when the root is in the indeterminate state, so a
partial select-all reads as a dash rather than a full check.
2026-07-28 03:52:33 -05:00
Brooklyn Nicholson
ddd6b57938 test(desktop): a leading slash now chips, superseding #71664's exclusion
#71664 asserted a leading slash never chips, correctly: a command only ever
executed, so it never reached a rendered message as text. Projecting a skill
turn back onto its invocation removes that precondition.
2026-07-28 03:47:49 -05:00
Brooklyn Nicholson
20b2022b8c fix(desktop): render a skill send as its invocation everywhere it surfaces
The bubble, the queue panel, and the queue editor all showed a queued or
sent /skill turn's expanded body. Thread the invocation through submit and
the queue entry, and chip a leading slash command the way a mid-prose one
already chips.
2026-07-28 03:44:31 -05:00
brooklyn!
99d766fcac
Merge pull request #73220 from NousResearch/bb/subagent-runs
Show a delegation as its subagents, live
2026-07-28 03:22:43 -05:00
Brooklyn Nicholson
dc50ddbbd6 style(desktop): hold the delegation card to three-quarter width
A list of short rows — goal, model, timer — reads better narrow than stretched across the full reading column. Scoped in styles.css rather than as a utility: the rule above it sets width on every tool block and wins over one.
2026-07-28 03:15:20 -05:00
brooklyn!
e4564586bc
Merge pull request #73218 from NousResearch/bb/focused-zone-tabs
fix(desktop): make the tab verbs follow the focused zone like ⌘1-9
2026-07-28 03:14:03 -05:00
brooklyn!
fd39696ccf
Merge pull request #73216 from NousResearch/bb/desktop-thin-scrollbars
fix(desktop): thin chrome scrollbars without platform chunk
2026-07-28 03:08:06 -05:00
Brooklyn Nicholson
2818c9e514 feat(desktop): render delegate_task as its live subagent list
A fan-out showed up in the transcript as one grey row and a JSON blob, so the several agents it started were invisible until they finished. Give the call its own card: one line per child with the goal, the model running it, and a live timer, over a single ticking line of that child's relayed activity.
2026-07-28 03:05:14 -05:00
Brooklyn Nicholson
ed67b20888 fix(desktop): make the tab verbs follow the focused zone like ⌘1-9
⌘1…⌘9 resolves its tab strip through $activeTreeGroup (the interacted
zone), but ⌘W, ⌘T, ⌘⇧T and the strip's "+" all hardcoded the workspace
pane's group. In a layout with a second chat zone the number keys worked
and every other tab verb acted on main instead — and that zone's strip
had no "+" at all.

Adds focusedSessionGroup() to the tree store, resolving the same zone
$activeTreeGroup names when it hosts a chat strip and falling back to
the workspace otherwise, so focus parked in files/terminal can't make ⌘W
close the file tree. ⌘W closes through it, unanchored openSessionTile()
(⌘T / ⌘⇧T) docks into it, and the "+" renders on any chat strip, noting
its zone on pointerdown so the tab lands where it was clicked.
2026-07-28 03:00:13 -05:00
brooklyn!
1cd8f8c37e
Merge pull request #73180 from NousResearch/bb/composer-popout-tabs
Composer pop-out is scoped to its layout zone
2026-07-28 02:58:33 -05:00
Brooklyn Nicholson
ef8f2e701c fix(desktop): thin chrome scrollbars without platform chunk
Chromium 121+ prefers scrollbar-width over ::-webkit-scrollbar and
ignores the latter when both are set, so our themed thumb never painted
and mac got the chunky platform "thin" bar. Gate standard scrollbar-*
for non-webkit engines only; ship a 4px webkit thumb on .scrollbar-dt
and portal menus.
2026-07-28 02:57:16 -05:00
Brooklyn Nicholson
73e3e0860e feat(desktop): expose a pane's layout zone to its content
PaneGroupContext, alongside PaneVisibleContext: the zone hands each pane
the group id it's rendered in, so state that belongs to a stack of tabs
rather than to a pane can key off it — and follows a pane dragged between
zones, since the provider is whichever zone renders it.
2026-07-28 02:49:46 -05:00
Brooklyn Nicholson
914c3f46c6 fix(desktop): stop the model-pill tooltip popping open after a mouse pick
Picking a model closed the menu and then flashed the pill's tooltip over
the fresh selection. The existing focus guard gated on `:focus-visible`
alone, which does not separate a mouse pick from a Tab: the dropdown
autofocuses its search field and keyboard-navigates its rows, so Chromium
is already in keyboard modality when the menu restores focus to the
trigger, and the guard never fired.

Track the device behind the last real interaction and use it to qualify
`:focus-visible`. A mouse pick reports `pointer` and stays silent; Tab
focus still opens the tip.
2026-07-28 02:27:11 -05:00
hermes-seaeye[bot]
f2a4452c8a
fmt(js): npm run fix on merge (#73165)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 06:47:12 +00:00
Brooklyn Nicholson
69a4f65164 fix(desktop): use xs switch size in model-visibility dialog 2026-07-28 01:32:24 -05:00
hermes-seaeye[bot]
c80b199f36
fmt(js): npm run fix on merge (#73155)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 06:29:04 +00:00
brooklyn!
4289a934b2
Merge pull request #73147 from NousResearch/bb/session-open-in-place
desktop: open sessions where they already are
2026-07-28 01:20:53 -05:00
brooklyn!
befff02e9b
Merge pull request #73150 from NousResearch/bb/session-tab-slots
desktop: ⌘1–9 maps to visible tabs
2026-07-28 01:18:52 -05:00
Brooklyn Nicholson
319bcedfea desktop: open sessions where they already are
Notifications, ⌘K, switcher, session picker, cron/command-center, artifacts,
and @session refs all go through openSession. Plain click/Enter focuses the
existing tile or main; ⌘-click/⌘-Enter opens a new tab; ⇧⌘ still pops a window.
2026-07-28 01:13:30 -05:00
Brooklyn Nicholson
f65fa01926 desktop: ⌘1–9 slots map to visible tabs, not raw pane indices
Focus layout keeps `files` in the workspace group's panes while chrome-
hidden. Slot keys walked that raw array, so ⌘2 landed on the first session
chip and every later number was off-by-one (especially after a ⌘W main-tab
shift). Index the same shown filter the strip paints, and cycle the same.
2026-07-28 01:12:43 -05:00
Brooklyn Nicholson
589fd23947 test(desktop): cover floating pane adoption and live drag
Adoption: a floating contribution stays out of the tree while a docked
sibling in the same pass is adopted, so the exclusion is specific rather
than a broken run.

Live DOM: mounts FloatingPanes and drives real pointer + resize events —
anchored spawn, drag, persistence across remount, edge-riding on resize,
the titlebar floor, collapse, and the no-drag button opt-out.
2026-07-28 01:11:12 -05:00
Brooklyn Nicholson
f78521785f feat(desktop): render floating panes above the layout tree
Adds placement: 'floating' — the one non-tiling pane role. Adoption skips
those contributions so one can never become a track that steals width
from a zone; the tree renders them as fixed cards instead, draggable by
the header, with position and collapse persisted per pane id.

The card reuses HUD_SURFACE (command palette, session switcher) rather
than hand-rolling a second float surface.
2026-07-28 01:11:05 -05:00
Brooklyn Nicholson
6dd1c6c062 feat(desktop): floating pane geometry
Pure clamping/anchoring rules for a pane that lives outside the layout
tree: keep 48px grabbable at either horizontal edge, hard-bound the top
below the titlebar so the drag handle stays reachable, pin top-left
rather than invert when the card outgrows the viewport, and let an
edge-anchored card ride its edge as the window resizes.

No DOM, so the rules are testable directly.
2026-07-28 01:10:59 -05:00
brooklyn!
5b22bd9556
Merge pull request #73105 from NousResearch/bb/tighter-turn-block-gap
Tighten the transcript's scaffolding rhythm, give diffs room
2026-07-27 23:52:49 -05:00
brooklyn!
4cee9aab61
Merge pull request #73075 from NousResearch/bb/drop-leva-backdrop-controls
refactor(desktop): drop leva from the chat backdrop
2026-07-27 23:48:10 -05:00
brooklyn!
0ae2997345
Merge pull request #73110 from NousResearch/bb/composer-at-chip
Chip @ file/folder refs instead of dropping them as plain text
2026-07-27 23:46:14 -05:00
Brooklyn Nicholson
16ef964fdb feat(desktop): rest the thinking caret at a faint hint instead of invisible
A run of thinking headers is the one place the disclosure affordance isn't
otherwise discoverable — every other one sits in a row you're already
reaching for. The resting opacity becomes a token so only that surface opts
in; DisclosureRow is shared with tool rows and run headers, which keep the
invisible-until-hover default.
2026-07-27 23:27:35 -05:00
Brooklyn Nicholson
90797eb224 fix(desktop): chip a bare @path into the ref it means
Tab-descending into a folder from the @ popover re-types the token as a
bare `@apps/desktop/` so the next complete.path lists its children. That
is right while typing, but a bare token is not a reference —
REFERENCE_PATTERN only matches @kind:value — so a draft sent with one
rendered as plain text and attached nothing at all.

Promote it to @file:/@folder: on the way out, the same shape url-refs.ts
uses for bare links: on the committing space, on paste, and on submit for
a path that never got its space. A '/' is required so a handle like
@teknium1 is never mistaken for a path.
2026-07-27 23:13:32 -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
058c4376d2 refactor(desktop): drop leva from the chat backdrop
The backdrop shipped a leva control panel — a Shift+Y dev tweak surface
for opacity, blend mode, filters and a radius scalar — that has never
been touched since the app landed. Its hooks ran in every build, dev and
packaged alike: only the panel's visibility was gated on
`import.meta.env.DEV`, not the two `useControls` calls or the zustand
store behind them. Inline the values it was already rendering with and
delete the dependency; leva was the only thing importing it in the tree.

One of those controls was doing real damage. `--radius-scalar` shipped as
0.6 in styles.css, but the slider's 0.2 default was written onto the root
element on mount, so every window that mounts the backdrop rendered at
0.2 and every window that doesn't kept 0.6. The token now declares 0.2 —
the value the chat has actually rendered at all along.

The remaining static values (0.025 opacity, difference blend, 160dvh, top
left, invert) become classes; saturate(1)/brightness(1) were identity and
are dropped.
2026-07-27 22:18:51 -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 Nicholson
015353d970 test(desktop): cover short actions labels and tip-less close buttons 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
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