Commit graph

1560 commits

Author SHA1 Message Date
brooklyn!
e2fbd0dcd7
Merge pull request #72303 from NousResearch/bb/desktop-session-status
fix(desktop): stop sidebar sessions from lying about whether they're running
2026-07-26 19:19:22 -05:00
brooklyn!
2b38d5ad59
Merge pull request #72308 from NousResearch/bb/tool-group-bounding
fix(desktop): stop reads and edits vetoing tool-call grouping
2026-07-26 19:13:22 -05:00
Brooklyn Nicholson
19d84d1071 test(desktop): pin the sidebar spinner's liveness contract
Investigating the missing-spinner report turned up no defect in the seeding
path: the active_list poll already lights a row for a turn the renderer never
saw start, holds it across polls, and follows a recycled runtime id onto its
new stored session. Pin all three so the reap change can't silently regress
turn-start while fixing turn-end.

Two boundaries worth naming rather than rediscovering:

- `starting` is deliberately NOT working. It means agent_build_started without
  agent_ready, and _start_agent_build runs on any incidental RPC that needs the
  agent — not just a prompt — so treating it as a turn would spin the row on
  merely opening a session.
- $workingSessionIds is keyed by STORED id and drops entries whose
  storedSessionId is null, while message.start flips busy without carrying one.
  A runtime that was never seeded with a stored id therefore goes busy
  invisibly. That is the remaining path by which a spinner can go missing.
2026-07-26 19:02:46 -05:00
Brooklyn Nicholson
058aa34d8a fix(desktop): stop reads and edits vetoing tool-call grouping
A run of 3+ adjacent tool calls collapses into the `.tool-group-scroll`
window, but `shouldBoundToolGroup` took `hasUnboundable` as a run-level
veto: a single exempt row anywhere in the range disabled collapsing for
the entire run. The exempt set was clarify, image_generate, execute_code,
read_file and every file-edit tool — which is most of what a coding
session does, so in practice runs never collapsed. Replaying a real
session's transcript: 84 consecutive calls, 30 of them veto-triggering,
zero windows.

The code-body entries were never needed. Everything ToolEntry renders
carries `data-tool-row`, and the `:has([data-tool-row][data-tool-open])`
rule already lifts the cap and the mask. A diff row mounts open, so it
frees the group the moment it appears; a collapsed row is a one-line
status whose body is not in the DOM at all, so there is nothing to clip.
Collapsing the row drops the group back to a compact window, which the
JS veto could not do.

Narrow the opt-out to the two components that bypass ToolEntry and so
can never emit `data-tool-row`: clarify and image_generate.
2026-07-26 18:49:05 -05:00
Brooklyn Nicholson
6b273f419a fix(desktop): keep thinking traces and tool calls across a mid-turn switch
preserveReasoningParts was gated on exact text equality with the cached row.
Mid-turn the authoritative text has advanced by a delta or two, so the guard
fails and the row is rebuilt from the gateway's inflight projection — which
is text-only. The renderer's cache is the sole carrier of a running turn's
structure, so switching away and back stripped the reasoning and every tool
call, leaving the turn looking inert.

Carry tool calls alongside reasoning, dedupe them on toolCallId, and match on
same-turn (identical text, or authoritative text extending the cached text)
rather than strict equality. Attachment refs and image re-appending stay on
the strict path: those reconcile a settled row, and a growing row is by
definition not settled.
2026-07-26 18:39:11 -05:00
Brooklyn Nicholson
42a30d13db fix(desktop): settle sessions that vanish from the live snapshot
session.active_list is authoritative about absence, but the renderer only
read the rows it returned. A turn that ends while the websocket is degraded
— a remote gateway on a flaky link, a reconnect, a profile swap — drops out
of the gateway's _sessions without Desktop ever seeing the running=false
edge, so the row spins forever and the busy->idle transition that paints the
green unread dot never fires.

Track live runtime ids per gateway profile and settle anything that
disappears between polls through publishSessionState so the real transition
fires. Profile scoping is load-bearing: background profiles are served by
other gateways and never appear in this profile's snapshot, so an unscoped
reap would dark out every other profile's running rows.
2026-07-26 18:39:03 -05:00
brooklyn!
bd6437d605
Merge pull request #72288 from NousResearch/bb/composer-undo
Give the composer its own undo stack
2026-07-26 18:38:11 -05:00
brooklyn!
2ec84c5d25
Merge pull request #72245 from NousResearch/bb/desktop-idle-churn
perf(desktop): stop the whole transcript re-rendering on sash drag
2026-07-26 18:36:00 -05:00
Brooklyn Nicholson
92f62bedd7 fix(desktop): extend the composer undo stack to the message edit composer
The edit composer pastes through the same `insertComposerContentsAtCaret` the
main composer uses, so it had the identical bug: the Range-based insert never
reaches Chromium's undo stack and Cmd+Z skipped past the paste to destroy the
edit before it. Its inline-ref and trigger-chip inserts mutate the DOM directly
too, with the same result.

Both surfaces now share `useComposerUndo`. The hook already keys its
document-level `beforeinput` claim off `document.activeElement`, so the two
mounted instances stay independent — only the focused editor's stack responds.
Undo/redo is handled ahead of Escape here, since a stray Cmd+Z falling through
would cancel the whole edit rather than step back one change.

`insertRefStrings` banks through `withUndoPoint` rather than recording after the
insert, which would have snapshotted the state it was meant to restore.
2026-07-26 18:21:50 -05:00
Brooklyn Nicholson
bc2ddf5bab fix(desktop): give the composer its own undo stack so Cmd+Z sees a paste
The rich editor mutates its DOM through `Range` rather than the browser's
editing commands, because `execCommand('insertText')` is ~O(n²) on large
multiline blobs and froze the composer for seconds on a big paste (#45812).
Those mutations never reach Chromium's undo stack, so a paste was invisible to
it — Cmd+Z skipped straight past the pasted text and undid whatever edit came
before it, leaving the paste stranded and the earlier edit destroyed.

Owning the stack outright is the only coherent fix; a half-owned one interleaves
our snapshots with Chromium's own typing entries and undoes them out of order.
Every edit path now banks its pre-edit state, and the editor claims Cmd+Z /
Cmd+Shift+Z (plus Ctrl+Y) instead of letting the native command run. Snapshots
are plain text + a caret offset rather than DOM, since the editor already
round-trips losslessly through composerPlainText/renderComposerContents.

Consecutive keystrokes coalesce inside a 600ms window, so undo steps back by a
typing burst the way a native editor does rather than one character at a time.
Electron's Edit menu `{ role: 'undo' }` fires the native command without a
keystroke the renderer can see, so a capture-phase `beforeinput` listener claims
historyUndo/historyRedo too and keeps the menu item and the shortcut in
agreement. Switching drafts resets the history — undoing into another
conversation's text is worse than having none.

Co-authored-by: David Metcalfe <80915+DavidMetcalfe@users.noreply.github.com>
2026-07-26 18:21:35 -05:00
Brooklyn Nicholson
0226d1162e Revert "perf(desktop): mount tooltips lazily"
Reverts the tooltip half of 4798994dc; keeps the idle-cost scenario.

Lazily mounting Radix on first hover measured well (105k -> 26k
TooltipProvider renders per drag) but broke 18 tests across 12 files.
Those tests are not incidental: the repo has an established convention of
asserting `[data-slot="tooltip-trigger"]` at mount to prove a control
carries a tooltip, and deferring the mount invalidates all of them at
once. There is also a real behavior risk the convention was protecting —
`asChild` puts the slot on the button element itself, so arming REPLACES
the node, which is exactly the kind of identity change that breaks focus
restoration and ref-holding call sites.

A 4x cut in tooltip churn is not worth reworking every tooltip assertion
in the app plus taking that risk, on a component with ~107 call sites.
If it's worth revisiting, the right shape is probably making
TooltipProvider itself cheap (one app-level provider) rather than
deferring the mount per call site — that preserves the DOM contract these
tests encode.

The genuine win in this branch stands on its own: the $layoutTree
subscription fix (commits 83 -> 12 on a sash drag) is unaffected.
2026-07-26 18:00:02 -05:00
Brooklyn Nicholson
8896fc7500 fix(desktop): stop a chip inserted after a word from swallowing its space
`plainTextInRange` serialized the caret's preceding content through a bare
<div>, but `composerPlainText` appends "\n" to any block element that isn't the
editor slot. So `beforeText` always looked like it ended in whitespace and the
separating space was never inserted — dragging a file in after a word produced
`review@file:...` glued together.

Marking the scratch container with RICH_INPUT_SLOT makes it serialize in the
same coordinates as the editor. Same fix lands in the new `caretOffsetInEditor`,
which measures caret offsets the same way.
2026-07-26 17:50:18 -05:00
brooklyn!
48bdde1deb
Merge pull request #72230 from NousResearch/bb/bootstrap-marker-triage
fix: make the bootstrap-complete marker consistent across every install path
2026-07-26 17:48:57 -05:00
Brooklyn Nicholson
f64b719321 perf(desktop): cache short markdown parses too
parseMarkdownIntoBlocksCached bypassed its cache for text under 1024
chars, on the theory that re-lexing a short message is cheap. The lex is
cheap; what it returns is not free. `parseMarkdownIntoBlocks` builds a
fresh array every call (verified in streamdown's dist: `let r=[]` ...
`return r`), and Streamdown mirrors the block list into useState — so a
new array identity for UNCHANGED text re-renders Streamdown and every
Block beneath it.

Most messages are short, so most of the transcript was on the uncached
path. Caching every length cuts the idle cost of five mounted tiles:

  Streamdown   5.2ms -> 2.6ms
  Block        128ms -> 85ms
  Ct           122ms -> 81ms

Cache bumped 64 -> 256 entries to cover the now-larger key space.

This does NOT reduce Streamdown's 105 idle self-renders — array identity
turned out not to be what drives those, and I verified the cache returns
a stable identity, so that root is still open. This is a cost win, not
the churn fix.
2026-07-26 17:02:59 -05:00
Brooklyn Nicholson
edadfcae96 perf(desktop): stop every zone subscribing to the whole layout tree
TreeGroup called useStore($layoutTree) to build its right-click menu's
move/split directions. That subscribes every zone — and therefore every
mounted pane and its entire transcript — to the whole layout tree. A sash
drag rewrites the tree once per frame, so dragging the sidebar re-rendered
all five tiles' message lists on every pointermove, for a context menu
nobody had open.

The directions are only read when the menu renders, so read the tree there
with .get() instead. Same lazy shape the neighbouring `closable` prop
already uses.

Measured over one 60px sash drag with five busy tiles:

  commits          83 -> 12
  ChatView        150 -> 10   (4465ms -> 353ms)
  AuiProvider    9450 -> 630  (9868ms -> 774ms)
  TreeGroup       180 -> 12
  TreeSplit        90 ->  6

Also fixes an observer effect in the harness: idle-cost recorded render
attribution *during* the timed gesture, and the counter walks the fiber
tree on every commit. That was large enough to hide this 15x reduction
behind an unchanged fps, so timing and attribution are separate passes now
and `record` defaults off.

Adds scripts/diag-drag-churn.mjs — the probe that found this. It reports
the transcript chain (who above the messages re-rendered) plus every atom
that notified, which is what named TreeGroup instead of leaving it to be
guessed at. Notably the atom list came back EMPTY: this was never store
churn, so the render-attribution path was the only thing that could have
found it.
2026-07-26 16:53:20 -05:00
Brooklyn Nicholson
f18d9b2843 fix(desktop): make idle-cost's drag actually drag
The synthetic gesture oscillated +/-3px, which nets to zero displacement
and can clamp to a no-op — so it reported a confident fps number for a
drag that never moved the sash. Sweeps monotonically now, dispatches
pointer events React's synthetic system accepts (isPrimary/button/buttons),
and records dragTarget + dragMoved so a drag that silently did nothing is
visible in the output rather than passing as a measurement.

Verified: dragMoved now reports 60px where it previously reported 0.
2026-07-26 16:36:42 -05:00
Brooklyn Nicholson
4798994dce perf(desktop): mount tooltips lazily, and measure the idle cost
Adds an `idle-cost` scenario for the symptom Brooklyn reported: with a
thread spinning, resizing the sidebar feels slow. It holds N tiles busy,
pushes NO tokens, and measures the renderer's self-inflicted commit rate
plus fps while dragging the splitter and while typing.

It reproduces immediately. Five busy tiles, nothing streaming:

  idle commits   17.7/sec   (should be 0 — nothing is happening)
  drag           1.4 fps    p95 812ms, worst frame 1.9s
  typing         61 fps     (fine — this is specific to resize)

Attributing the drag window showed 105,385 TooltipProvider renders and
~15s of component time across a 60-frame gesture. Cause: `Tip` mounts a
full Radix provider + Tooltip per call site, and there are ~107 of them.
Radix's Tooltip holds real state and Popper subscribes to layout, so an
unrelated interaction re-rendered all of them.

Mounts the machinery lazily instead, on first hover/focus. Tooltip churn
drops ~4x (105k -> 26k) and drag doubles to 3fps.

Note `defaultOpen` on the armed Tooltip is load-bearing: the pointerenter
that armed it has already fired, so Radix never sees it and the tip mounts
silently closed. A test caught exactly that, and now guards it.

3fps is still bad — the remaining cost is the whole transcript
re-rendering per resize frame (MessagePrimitive.Parts 12,600 renders /
10.5s, Block/Ct 24,300 each, all 100% wasted). Separate fix.
2026-07-26 16:36:42 -05:00
brooklyn!
79b4a40568
Merge pull request #72220 from NousResearch/bb/tool-row-dedupe
fix(tools): stop the inline tool row stuttering its verb and repeating the command
2026-07-26 16:25:50 -05:00
Brooklyn Nicholson
53707e405c fix(desktop): stop repair from deleting a healthy install's marker
Repair signalled "reinstall me" by deleting the bootstrap marker. That was
already destructive -- repair is reachable from a transient backend error on a
fully working install -- and it stranded users in first-run setup with no way
back short of hand-writing the marker file.

Carry the intent in an explicit flag instead. Repair forces the next resolve
through the installer and clears itself once the reinstall starts, so a forced
reinstall still works without destroying provenance about how the install was
created.

Closes #72166
2026-07-26 16:18:01 -05:00
Brooklyn Nicholson
048e9b2150 fix(desktop): launch a usable active runtime without a bootstrap marker
Marker presence was the launch gate, but the marker is provenance about who
ran the install -- not proof the runtime works. A CLI-installed repo+venv, or
a healthy install whose marker a repair deleted, both read as "never
installed" and dropped the user into first-run bootstrap on every launch.

Split the two questions: classifyActiveRuntime() reports marker validity and
runtime usability separately, and the resolver launches whenever the runtime
is usable, logging when it proceeds without a marker. An unusable runtime
still falls through to bootstrap even with a valid marker, so an interrupted
install can't spawn a dead backend.

Drops isBootstrapComplete(), which had no callers left once the gate moved.

Co-authored-by: iveywest <iveywest@users.noreply.github.com>
Co-authored-by: lihengming <lihengming@users.noreply.github.com>
2026-07-26 16:12:14 -05:00
Brooklyn Nicholson
ec6719b04e fix(desktop): stop the tool row repeating its command three times
The expanded terminal row printed the same string as the title, as the
`$` transcript, and again as detail. shellCommand preferred the
backend's display preview over the real `command` arg, so the transcript
showed a summary of what ran rather than what ran; and a terminal call
with no output fell through to the generic fallback, which echoes
args.context under a transcript already showing it.
2026-07-26 16:03:49 -05:00
brooklyn!
f695ce3461
Merge pull request #72212 from NousResearch/bb/desktop-transcript-renders
fix(desktop): make render-churn measure streaming, not boot churn
2026-07-26 16:00:17 -05:00
Brooklyn Nicholson
f7aee9dc8c fix(desktop): make render-churn measure streaming, not boot churn
Two problems, both found by distrusting the harness's own numbers.

1. The scenario slept a fixed 1s after mounting tabs, then recorded. Boot
   and session hydration are not reliably done by then, so a variable
   amount of unrelated work landed inside the measurement window. Three
   back-to-back runs on identical code spread 2.2x on total_renders and
   3.8x on wasted_renders — wide enough that a single-run before/after
   delta could be mostly noise. Replaced with a quiesce gate that waits
   for commits to hold still before recording, and reports 'quiet:N' or
   'timeout:...' so a contaminated run is visible instead of silent.

2. The counter attributed a context-driven re-render as 'wasted', which
   pointed at memo() as the fix when memo cannot block context at all.
   Adds contextChanged via the fiber's context dependency list, and
   excludes it from wasted.

The gate also turned up a finding worth more than the fix: with five busy
tiles and NO driver running, the renderer still commits ~18x/sec. The
report now names the cascade roots (own state changed, props did not)
rather than leaving them to be guessed at — Streamdown re-renders itself
105 times while idle, which is what drives Block/Ct.
2026-07-26 15:54:18 -05:00
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
456b2f9c7d refactor(desktop): give the rich editor a chip-aware caret insert
`insertPlainTextAtCaret` dropped its text in verbatim, so a caller with
directives in hand had no way to land them as chips. It becomes
`insertComposerContentsAtCaret`, sharing the parse `renderComposerContents`
already uses, and gains a sibling `replaceBeforeCaret` for swapping a
just-typed token for a chip.
2026-07-26 15:23:51 -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
Brooklyn Nicholson
41dd895e12 perf(desktop): stop the statusbar re-rendering per streaming token
The statusbar subscribed to `$focusedSessionState` — a projection of
`$sessionStates`, which is republished on every message delta — but reads
only three fields off it. Every token therefore re-ran useStatusbarItems
and rebuilt all ~9 item objects, and since StatusbarItemView was not
memoized, one changed item (the running timer) re-rendered the whole bar.

Adds `useStoreSelector` beside the existing `useSessionSlice`, the same
narrowing idea for a scalar instead of a keyed array: subscribe to the
store, but bail out unless the selected value changes. Applies it to the
three fields the statusbar actually reads, and memoizes StatusbarItemView.

Measured over five concurrent streaming tabs (`render-churn`):

  total renders   78,385 -> 21,701   (-72%)
  wasted renders  10,432 ->  6,112   (-41%)
  TooltipContent   2,304 ->    143   (-94%)
  StatusbarItemView 2,174 ->      0
2026-07-26 14:21:34 -05:00
brooklyn!
41f2196c53
Merge pull request #71925 from NousResearch/bb/desktop-state-diagnostics
feat(desktop): state diagnostics — render + store churn counters
2026-07-26 14:12:16 -05:00
Brooklyn Nicholson
37ac8ae76a feat(desktop): render-churn perf scenario
Drives the same synthetic multi-tab streaming workload as `multitab`
(publishSessionState per session per flush, no backend, no credits) but
reports render attribution instead of frame pacing — sidebar_renders,
wasted_renders, and wasted_notifies.

Answers 'does the sidebar re-render while an agent is typing' directly.
2026-07-26 06:11:00 -05:00
Brooklyn Nicholson
44211f3608 feat(desktop): dev-only render + store churn counters
Adds two dev-only counters that attribute re-renders and store
notifications during an interaction, so a perf claim can be answered with
a number instead of a hunch:

  window.__RENDER_COUNTS__  what re-rendered, and why (props/state/parent)
  window.__ATOM_CHURN__     which store published it, and whether it mattered

The `wasted` column in each is the fix list — components that re-rendered
with no changed input, and stores that published a value equal to the
last one. Both are inert until start(), so idle cost is one branch per
commit and per notify.

React 19.2 removed injectProfilingHooks from react-dom, so the mark*
profiling family is unavailable and onCommitFiberRoot is the only channel
left. <Profiler> can't answer the question either: React invokes onRender
for every Profiler in a committed tree including subtrees that bailed
out, and a bailed-out subtree still reports nonzero actualDuration. This
uses bippy's didFiberRender instead. bippy over react-scan because
react-scan/lite is a thin wrapper over it while the package pulls ~217
transitive deps and floats two on latest.

main.tsx imports the entry statically above react-dom because react-dom
captures the devtools hook at module init — a late install reports
renderers=0 and observes zero commits. Production exclusion is handled by
a build-time alias to a no-op module rather than tree-shaking, since a
static side-effect import can't be eliminated.
2026-07-26 06:10:54 -05:00
brooklyn!
eb52760564
Merge pull request #71901 from NousResearch/bb/session-click-active
fix(desktop): clicking the active session from a full page returns to the chat
2026-07-26 05:32:43 -05:00
hermes-seaeye[bot]
529ae164ae
fmt(js): npm run fix on merge (#71897)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-26 10:27:29 +00:00
Brooklyn Nicholson
ecf8ef970e fix(desktop): clicking the active session from a full page returns to the chat
While the workspace pane shows a full page (Artifacts, Skills, Messaging, a
plugin route), a sidebar click on the ACTIVE session did nothing: onResumeSession
took focusOpenSession's `true` for the main-session branch as "already on
screen" and skipped the navigate, but fronting the workspace tab doesn't put the
chat back — the page is still routed. The user had to click some other session
and then the active one to get back.

focusOpenSession now reports WHICH surface it fronted ('main' | 'tile' | null),
and focusedSessionNeedsRoute decides: a tile never needs a route (its pane
renders the chat regardless), a main hit does while a page covers the workspace.
2026-07-26 05:26:26 -05:00
brooklyn!
3b9bd0de6d
Merge pull request #71848 from NousResearch/bb/sidebar-plus-tab
Open a tab from the sidebar "+" when a chat is already loaded
2026-07-26 05:19: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!
080ee077a8
Merge pull request #71891 from NousResearch/bb/slash-tab-target
fix(desktop): send a skill's kickoff into the tab that invoked it
2026-07-26 05:15:16 -05:00
Brooklyn Nicholson
f2f5b32531 fix(desktop): expect keep-alive tabs not to repaint on reactivation 2026-07-26 05:11:46 -05:00
Brooklyn Nicholson
2a6368f041 fix(desktop): send a skill's kickoff into the tab that invoked it
`/work` typed into a fresh Cmd+T tab loaded the skill in that tab and
printed " loading skill: work" there, then fired the skill's kickoff
prompt as a user message into whatever conversation was on screen.

The dispatcher resolves its target once, through resolveTargetSessionId,
and every other consumer of that answer already honors it: the output
writer binds to the target's stored id, and the busy gate reads the
target's own state. The send did not — `submitPromptText(message)` passed
no target at all, so submit fell back to `activeSessionIdRef`, which
names the foreground chat. #71805 fixed the two sibling leaks in this
same function; this is the third and the one that actually moved the
user's prompt.

Forward the resolved pair instead. Every target the dispatcher serves —
a tile, a background queue drain, a session this very call created —
was hitting the same fallback, so the fix covers the class rather than
the tab case that surfaced it.
2026-07-26 05:08:37 -05:00
brooklyn!
02721cc1c0
Merge pull request #71872 from NousResearch/bb/desktop-ui-polish
Desktop UI polish: link chips, sidebar arc, tab strip rule
2026-07-26 05:08:08 -05:00
Brooklyn Nicholson
9947a6065b fix(desktop): align external-link icon tests with default-off 2026-07-26 05:00:53 -05:00
Brooklyn Nicholson
76e9ac3915 fix(desktop): preserve correction order in session tabs 2026-07-26 05:00:18 -05: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
6b816ad8c3 fix(desktop): make the running-session arc legible in the sidebar
The arc reduced to a few faint dots on session rows. Two causes: the ring
was outset by 2px into a scroller that clips horizontally, losing its
left and right runs; and its tail color defaults to the chrome
background, which is invisible against the sidebar, leaving only the
bright stop of each gradient pass.

An `arc-row` variant sits flush and ties the tail back to the ring
color. The ring's radius is now derived from its standoff (r_host + gap)
rather than inherited, which keeps any outset host concentric instead of
pinched.
2026-07-26 04:25:25 -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]
953707103f
fmt(js): npm run fix on merge (#71863)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-26 09:04:27 +00:00
Brooklyn Nicholson
fc39c7ac31 test(desktop): scope e2e transcript helpers to the active chat surface
The sidebar "+" now stacks a tab instead of replacing the surface, so the
prior session stays mounted and several chat surfaces can be on the page at
once. Helpers that waited for the old transcript to disappear from the page
timed out, and `.first()` locators / bare `document.querySelector` calls
started resolving against the wrong session (CI's "resolved to 2 elements"
strict-mode violation).

Target the most recently mounted `[data-composer-target]` surface instead,
and assert the NEW surface is empty rather than waiting for the old text to
vanish.
2026-07-26 03:57:49 -05:00
brooklyn!
4dae897265
Merge pull request #71835 from NousResearch/bb/stream-history-cost
perf(desktop): make streaming cost independent of transcript length
2026-07-26 03:56:05 -05:00
brooklyn!
de9196ed0c
Merge pull request #71836 from NousResearch/bb/stream-foreground-leak
Render the workspace pane from its own session slice
2026-07-26 03:55:52 -05:00
brooklyn!
d9f1043c33
Merge pull request #71840 from NousResearch/bb/status-stack-scope
Scope measured-height vars to each chat surface
2026-07-26 03:26:39 -05:00