Commit graph

1728 commits

Author SHA1 Message Date
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
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
Brooklyn Nicholson
46df6ca705 fix(desktop): keep a run live between sequential calls
A run counted as live only while one of its calls was unresolved, which is
false for the instant between one sequential call finishing and the next
arriving — and for a string of commands that instant is most of the run. It
settled and re-opened between every call, so the ticker unmounted and came
back at the top of its reel instead of scrolling, and the summary flipped to
past tense while work was still going.

Live is now "the turn is working and nothing follows this run", with the
tail bound still settling a run the agent has moved past. The summary takes
its present-tense clause from the most recent call when none is pending —
the same call the ticker is showing. The stall spinner stays out of the way
while a run narrates, rather than stacking a second timer under it.
2026-07-27 19:35:04 -05:00
Brooklyn Nicholson
0b0e53cee1 fix(desktop): retire the drafting label when the model moves on
`tool.generating` names the tool whose arguments are streaming, and nothing
ever closed that claim: there is no stop-drafting event, and a draft can be
abandoned without reaching `tool.start` when a mid-stream retry drops a
partial call or a guardrail blocks the tool. Enumerating the ways a draft
ends left those holes open, so "Editing" sat under the transcript for the
rest of a multi-iteration turn.

Invert the rule — the claim only covers what the model is emitting right
now, so any other output from that session retires it. Stopping the turn
clears it too, and a `tool.generating` that arrives after the stop is
ignored on the same condition `mutateStream` already drops late tool rows.
2026-07-27 19:34:54 -05:00
Brooklyn Nicholson
ca605174cf fix(desktop): zero Tip skipDelayDuration so every tip waits its delay 2026-07-27 19:31:55 -05:00
brooklyn!
f800aa3cae
Merge pull request #72995 from NousResearch/bb/edit-composer-submit
fix(desktop): send a message edit when the arrow is clicked
2026-07-27 19:27:34 -05:00
hermes-seaeye[bot]
c89d21189a
fmt(js): npm run fix on merge (#72992)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 00:21:21 +00:00
Brooklyn Nicholson
c880672846 fix(desktop): send a message edit when the arrow is clicked
Clicking the edit composer's send arrow did nothing — the edit only went
through via revert. On macOS a <button> takes no focus on mousedown, so the
arrow-click blurred the contenteditable, the blur's 80ms timer cancelled the
edit and tore down the assistant-ui composer core, and the click's send() then
threw "Composer is not available" against the dead core. submitEdit had already
set submitting=true, so the arrow wedged and only revert worked.

Guard focus on the send button with onPointerDown preventDefault, the same way
the restore button does, so the click never blurs the editor. Also wrap the
send() and the blur-timer cancel() in the #49903 unguarded-core swallow so a
raced teardown can never wedge the arrow or leak an uncaught renderer error.
2026-07-27 19:17:36 -05:00
brooklyn!
67e592b015
Merge pull request #72987 from NousResearch/bb/context-menu-parity
Mirror every desktop kebab menu to right-click
2026-07-27 19:12:04 -05:00
Brooklyn Nicholson
53a81cfe50 fix(desktop): give tool rows the scaffold colour they were meant to share
TOOL_HEADER_TITLE_CLASS was byte-identical to SCAFFOLD_LABEL_CLASS apart
from the colour — secondary (74%) against the scaffold's 64% — so a "Ran wc
-l" row sat visibly brighter than the "Thought for 1s" line above it. Same
for the trailing duration: tertiary against scaffold meta.

The primitive existed but only the thinking header and run summary were
routed through it. Point the tool row at it too and delete the duplicates,
so there is one place left to change. Search hit titles keep the brighter
grey under their own name: they are result content, not scaffolding.
2026-07-27 19:11:48 -05:00
Brooklyn Nicholson
a67b2d93cd fix(desktop): paint the live status line as scaffolding
The drafting/stall status row kept its own type and colour — text-sm at
muted-foreground/70, with the hint at /55 — so "Editing" while the model
drafts a call rendered a full step larger than the "Explored 3 files" line
it turns into a moment later, in a different grey.

Route it through the scaffold label token so the whole left column reads as
one kind of line. The timer keeps its midground tint: that belongs to the
live-signal cluster with the dither block, not to the scaffolding.
2026-07-27 19:07:24 -05:00
Brooklyn Nicholson
84a25212de feat(desktop): mirror every kebab menu to right-click
Wire the shared actions-menu into the remaining kebabs so right-clicking a
row opens the same actions as its ⋯ button: project rows (appearance moves
to a submenu via the new shared ProjectAppearancePicker), worktree lanes,
the composer branch bar, and settings credential rows.
2026-07-27 19:04:45 -05:00
Brooklyn Nicholson
0d14864b94 refactor(desktop): extract a shared kebab + right-click actions-menu primitive
Promote the session row's inline MenuKit device into components/ui/
actions-menu.tsx: one ActionsMenu (kebab) + ActionsContextMenu (right-click)
pair driven by a single items(kit) render function, so a row's dropdown and
its context menu can't drift. Refactor the session menu onto it and let
StatusRow forward ref/onContextMenu so any row can host a context menu.
2026-07-27 19:04:39 -05:00
Brooklyn Nicholson
705b20a57d fix(desktop): delay Tip hover-open by 200ms so tips stop flashing 2026-07-27 19:02:23 -05:00
Brooklyn Nicholson
b1f4b95761 fix(desktop): measure reasoning per block instead of per turn
The timer registry hands every caller of a key the same origin, and every
reasoning block in a turn was keyed `reasoning:<messageId>`. So the second and
third blocks measured from the first one's start and each reported the running
total as its own duration — the "6s, 6s, 16s" down a single turn.

Key per block, and move the measurement into `useMeasuredDuration`, which
keeps the number beside the origin that produced it. The thread virtualizes,
so the component that watched a block finish is usually gone by the time
anyone scrolls back to read it; component state forgot the duration on
unmount and the row fell back to having none.

A block that genuinely was never watched running — history from an earlier app
session, or reasoning that arrived already complete — still has no duration to
report, and now says "Thought" rather than sitting in the present tense at a
turn that ended.

Also drops the run summary's aggregate +N/−M: a run can no longer contain a
file edit, so it was always zero. Each edit carries its own count on its card.
2026-07-27 18:50:20 -05:00
Brooklyn Nicholson
f08b0e5606 Merge remote-tracking branch 'origin/main' into bb/desktop-activity-grouping 2026-07-27 18:42:49 -05:00
Brooklyn Nicholson
a3c46ac09e refactor(desktop): give live tool runs a one-line ticker
A live run was the settled view under a CSS max-height: the real rows,
capped at ~6.75rem, with an escape hatch that let an open diff lift the cap
entirely. So it was neither a single line nor an honest expanded block, and a
run could balloon mid-turn.

Split the two presentations instead. Live, a run is its summary plus a
one-line reel that slides each finished action up and out, so a turn touching
thirty files reads as one line ticking over in place. Settled, the summary is
the whole of it until opened. Cards — file edits, clarify, image_generate —
leave the run entirely and stay on screen where they happened, since the diff
or the question IS the point of the turn.

Drops useToolWindow, the bounded-window threshold, the scroll container, the
fade mask and the :has([data-tool-open]) escape hatch, all of which existed to
make "the real rows, but shorter" work.

Also unifies transcript scaffolding on one colour: thinking headers painted
--ui-text-secondary and tool summaries --ui-text-tertiary under a shared
opacity, which is two greys for one kind of line. Both now render through
ScaffoldRow at a token pitched between them.
2026-07-27 18:42:46 -05:00
hermes-seaeye[bot]
96db67b849
fmt(js): npm run fix on merge (#72967)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-27 23:12:24 +00:00
brooklyn!
a19cfa4531
Merge pull request #72960 from NousResearch/bb/desktop-statusbar-toggle
feat(desktop): let the status bar be hidden
2026-07-27 18:10:28 -05:00
Brooklyn Nicholson
6ba2f3e644 fix(desktop): say a sub-second reasoning block was brief
The elapsed timer counts whole seconds, so reasoning that finished
inside one rendered as "Thought for 0s" — accurate and useless, and on a
turn with several short blocks it repeats down the transcript. Drop the
number below a second and say it was brief instead.
2026-07-27 18:10:27 -05:00
Brooklyn Nicholson
f96997c36f fix(desktop): settle a tool run whose calls never resolved
A run inferred "still working" from a missing result alone, so a call
left unresolved — by an interrupted turn, or an agent that moved on —
pinned its run as live forever. That stranded the summary in the present
tense ("Exploring 2 files" on a finished turn) and, because a live run
withholds its toggle so approvals can't hide, left the run permanently
expanded with no way to collapse it.

Qualify liveness the way ToolEntry already qualifies a row's: a missing
result only means pending while the run is the tail of a running
message. Liveness is now passed into summarizeToolRun rather than read
off the calls, since it isn't a property of the calls.
2026-07-27 18:10:27 -05:00
Brooklyn Nicholson
f7d6c1be8f docs(desktop): explain what UNBOUNDABLE_TOOLS now guards
The list gates two behaviours since the settled-run summary landed, but
the comment still justified it only in terms of the scroll window's
height cap. Record the pending-row rule that keeps approvals visible,
and why file edits stay off the list.
2026-07-27 18:10:26 -05:00
Brooklyn Nicholson
a77c75f3e0 feat(desktop): report how long the model thought
A settled reasoning block reads "Thought for 5s" instead of staying
"Thinking" forever. Nothing in the persisted turn records the duration,
so the number is frozen when the block finishes on screen and simply
omitted on a rehydrated turn, rather than reporting whatever a timer that
never ran would say.
2026-07-27 18:10:26 -05:00
Brooklyn Nicholson
7151ea4c77 feat(desktop): collapse a settled tool run to its summary line
A run of two or more tool calls now renders behind its summary once it
has finished, so a long transcript reads as what the agent did rather
than as a wall of rows. The run is keyed by its first tool call instead
of its part index: live and rehydrated turns agree on which calls belong
together but not on the indices they land at, and keying by index is what
made the previous attempt reshuffle on settle.

A run holding anything still pending always renders its rows, which is
what keeps a clarify question or an approval bar out from behind a
chevron. The approval-group tests move to tool-group and grow coverage
for both halves of that rule.
2026-07-27 18:10:26 -05:00
Brooklyn Nicholson
97d790d8b1 feat(desktop): summarize a run of tool calls as one line
Adds the grammar behind "Edited wiring.tsx, explored 3 files, ran 5
commands": one clause per category of work, a name when the category
holds a single thing and a count otherwise, and the present tense for
whichever category is still running.

The continuity test is the load-bearing part. Tool grouping was reverted
once because it reshuffled the moment a turn settled, so this replays the
same turn twice — as the gateway event stream the live view builds
bubbles from, and as the rows toChatMessages rehydrates on resume — and
asserts both produce the same runs.
2026-07-27 18:10:26 -05:00
brooklyn!
9f02bb207d
Merge pull request #72965 from NousResearch/bb/resume-freetext-search
fix(desktop): keep /resume's free-text search typeable
2026-07-27 18:09:50 -05:00
brooklyn!
fbd8d1a93d
Merge pull request #72963 from NousResearch/bb/artifacts-real-preview
Unify the preview rail onto one tab list
2026-07-27 18:04:18 -05:00
Brooklyn Nicholson
3c74f463d0 fix(desktop): keep /resume's free-text search typeable
/resume was classified as an options command, but its argument is a
free-text query the picker fuzzy-matches against session titles and
previews. Its completion list also always ends in a "Browse all
sessions…" action row, so Space-to-accept never fell through to an
empty list: the first space in a multi-word query emptied the composer
and threw the user into the overlay, query and all.

Classify it as mixed so spaces type through, matches keep narrowing as
you refine, and Tab or arrow-then-Enter still accept a highlighted
session.
2026-07-27 18:03:07 -05:00
Brooklyn Nicholson
f4e042f466 style(desktop): sit every pane tab strip on the sidebar surface
The strips painted with `--theme-card-seed` — the raw, unmixed seed rather
than a surface token — so they read as their own band beside the sidebar and
titlebar, which share `--ui-bg-sidebar`.

Fixed at the default in `PaneTab` rather than per strip. The right-rail
preview strip never set the vars at all, so its inactive tabs fell through
to the seed even though its container was already on the sidebar surface;
correcting the fallback fixes that one for free and keeps the next strip
from regressing. With the default right, the two zone strips no longer need
to redeclare the var and paint the token directly.
2026-07-27 18:02:20 -05:00
brooklyn!
7de4fbd493
Merge pull request #72956 from NousResearch/bb/slash-freetext-enter
fix(desktop): don't let Enter swap a free-text slash argument for a completion
2026-07-27 17:56:45 -05:00
Brooklyn Nicholson
cd25a9f405 fix(desktop): open a preview without dragging the file tree open
The preview pane shares a collapsible column with the file tree, and
`revealTreePane` un-collapses a column through that column's bound store —
which on the right is `$fileBrowserOpen`, the tree's own ⌘J toggle. So
every preview open literally called `setFileBrowserOpen(true)` and the tree
came with it.

`revealPreview` now un-collapses the column directly and leaves the toggle
alone. The tree pane's visibility binding gains `$fileBrowserOpen` to match,
since its presence was tracking only the column's collapse — without that it
would still render the moment anything opened the column.
2026-07-27 17:55:46 -05:00
Brooklyn Nicholson
003ff53fb4 fix(desktop): scope composer and transcript state to their own session
`$activeSessionId` only ever holds the primary chat's session, but surfaces
that render once per transcript were reading it as if it meant "the session
on screen." A preview produced inside a session tile was recorded under the
main chat's key and surfaced in the main chat's composer, which is what
prompted this.

The tool row now records under its own `SessionView`, and the same fix
applies to the other readers of that atom that render per surface:
attachment pills and inline preview links resolve relative paths against
their session's cwd, composer voice and auto-speak read and subscribe to
their own transcript, and the thread's compaction label, prompt-wait gate
and turn timer follow the session that mounted them. `ComposerScope` now
carries a `$messages` atom rather than a read closure so both the
imperative read and the subscription come from one place.
2026-07-27 17:55:46 -05:00
Brooklyn Nicholson
96999b116b refactor(desktop): put every preview on one rail tab list
The right rail held two things at once: a list of file tabs, and a
privileged "live preview" slot with a hardcoded `preview` tab id backed by
a separate session-keyed registry. The two were written under different
session-id rules and reconciled against each other, so an `open_preview`
from a session whose stored id hadn't landed yet was set and then
immediately cleared — the pane flashed and vanished. Artifacts arrived as
a third list with their own pane and renderers.

Now everything the rail can show is a `PreviewTarget` in `$previewTabs`,
and `openPreview` is the only way in. `$previewTarget` is a computed read
of the active tab, the session registry and its reconciler are gone, and
artifacts render in the real preview pane through the shared mode switcher
and source view instead of a parallel one. Artifact tabs stay memory-only
since the registry rebuilds from the transcript.
2026-07-27 17:55:27 -05:00
Brooklyn Nicholson
43571601aa fix(desktop): don't let Enter swap a free-text slash argument for a completion
`/goal` keeps its completion popover open across arbitrary prose so its
subcommands stay reachable. The popover highlights its first row on open, and
Enter accepted that highlight unconditionally — so pressing Enter to send
`/goal ship the redesign` would replace the sentence with a row the user never
chose. Space was already guarded; Enter and Tab were not.

Enter now accepts only after the user has arrowed to a row deliberately, so
the highlight never lies about what Enter will do. Tab stays an unconditional
accept, since it means nothing else in the composer.

This is latent rather than reproducible today: `/goal` is absent from
`SUBCOMMANDS` (its `args_hint` pipes are spaced, so the extraction regex
misses them), so the backend returns no arg completions and the branch never
runs. Giving `/goal` the subcommands it already advertises would resurrect
the #71963 symptom in a worse form — losing the prose instead of chipping it.
2026-07-27 17:50:18 -05:00
Brooklyn Nicholson
e3acdfb21d refactor(desktop): lift the completion-accept decision out of the keydown ladder
Which keys accept the highlighted completion was an inline condition in the
composer's keydown god-function, untestable without a DOM harness. Move it to
a pure helper beside the other slash-query utilities.
2026-07-27 17:50:12 -05:00
Brooklyn Nicholson
58c8d86bd5 feat(desktop): let the status bar be hidden
The bar is always-on chrome today. Hiding it is `⌘⇧S`, the ⌘K palette, or
the bottom row of its own right-click menu — VS Code's set of doors, minus
their unbound default (they ship `toggleStatusbarVisibility` with no
keybinding and Hermes has no chord dispatcher for a `⌘K ⌘S` two-stroke).

Hidden unmounts the bar rather than hiding it, so the 15s status poll and
the per-turn readouts stop with it. Visibility persists per window profile
and defaults on.
2026-07-27 17:47:45 -05:00
Gille
b429194478
refactor(desktop): simplify free-text slash mode check (#72815) 2026-07-27 17:45:03 -05:00
brooklyn!
7c532e1006
Merge pull request #72889 from NousResearch/bb/composer-at-paths
Fix `@` path navigation, folder completion, and chip baseline in the composer
2026-07-27 17:13:00 -05:00
brooklyn!
42c308ecdc
Merge pull request #72897 from NousResearch/bb/desktop-drift-fixes
Desktop: fix diff color drift, replayed notifications, stall timing, and quit-on-active-work
2026-07-27 17:12:49 -05:00
brooklyn!
14cb0507a7
Merge pull request #72912 from NousResearch/bb/desktop-project-count
Drop the leftover session counts inside an entered project
2026-07-27 17:12:39 -05:00
Gille
dbc18c6d62
fix(desktop): preserve live model after settings save (#72903) 2026-07-27 17:52:37 -04:00
brooklyn!
c63be0daf7
Merge pull request #72900 from NousResearch/bb/desktop-home-project
Add a Home project at the top of the desktop sidebar
2026-07-27 16:29:57 -05:00
Brooklyn Nicholson
e04ed64637 style(desktop): drop the session counts inside an entered project
The sidebar's flat session list lost its `x/<total>` chip in #72336, but
the project drill-in kept counting: WorkspaceHeader still rendered a
SidebarCount for every repo and every branch/worktree lane. Entering a
project put a number next to each label again.

Remove the header's count slot and both call sites, along with the now
dead repo total.
2026-07-27 16:25:51 -05:00
Brooklyn Nicholson
e00901259c feat(desktop): Tab into a folder from the @ popover
Tab and Enter shared one branch, so picking a folder always committed a
chip and closed the menu — the list could show `apps/` but never open it.
Reaching a nested path meant typing every segment by hand.

Split the two intents. Tab re-types the token as a bare path so the next
completion lists that folder's children; Enter still commits the folder
itself as a chip. Files ignore the distinction — there's nowhere deeper
to go. Backspace mirrors the descent, dropping one path segment per press
instead of one character, so climbing out costs the same as going in.
2026-07-27 16:20:58 -05:00