Commit graph

63 commits

Author SHA1 Message Date
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!
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
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!
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 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
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
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!
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
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
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
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
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
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
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
2f5926ed05 fix(desktop): time a stream stall from the last activity
The tail "Hermes is thinking" indicator resets on every flush, but its
timer never did: with no timer key, useElapsedSeconds anchors to mount,
and the indicator mounts with the assistant message. A stall two minutes
into a turn therefore claimed two minutes of silence instead of the two
seconds that had actually passed.

Give the hook an explicit epoch and hand it the timestamp of the activity
the quiet spell followed. Compaction still counts from the turn's start,
which is the span it owns.
2026-07-27 15:47:53 -05:00
Brooklyn Nicholson
93477b2a0c fix(desktop): paint diffs from the theme palette
Diff add/remove lines were hardcoded to Tailwind's emerald/rose while the
overview ruler beside them — and the rest of the app — used --ui-green /
--ui-red, so every diff sat slightly off-brand and stayed put when the
semantic palette moved. Derive the tint, gutter, and text from those two
colors instead. One renderer feeds the tool card, the file preview, and
the review pane, so all three follow.
2026-07-27 15:47:42 -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
ethernet
e026fd61d8 lint(desktop): ban atom-mirrored refs via no-restricted-syntax eslint rule
A ref synced from a nanostores atom via useEffect lags the atom by one
render. Callbacks that read the ref instead of the atom get stale values —
cancelRun sent session.interrupt to the wrong session (#66485), and
steerPrompt/restoreToMessage/editMessage had closure-priority stale reads.

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

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

Rule is scoped to apps/desktop only — ui-tui and tests-js don't use
nanostores and don't have this pattern.
2026-07-25 18:12:48 -07:00
Brooklyn Nicholson
936b407a72 fix(desktop): free code-block scrollbar & last-line selection from the toggle overlay
`ExpandableBlock` used a single full-width `absolute inset-x-0 bottom-0`
button that was BOTH the fade overflow cue and the expand/collapse control.
It sat on top of the whole 28px bottom strip, so it swallowed:

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

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

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

Supersedes #69558, #69428
Fixes #69168

Co-authored-by: alelpoan <alelpoan@proton.me>
Co-authored-by: BerneYue <xiongyue_hnu@163.com>
2026-07-22 23:23:39 -05:00
ethernet
470c7e2a60
fix(desktop): prevent timers from shifting as they count (#68131)
LiveDuration returned a bare string with proportional digits, so the
statusbar reflowed every second as the timer ticked. Extract a shared
StableText component that renders each character in its own 1ch-wide
cell, preventing any digit from shifting the layout — works with the
proportional sans font, no need for font-mono.

Both LiveDuration (statusbar session/running timers) and
ActivityTimerText (tool activity timers) now use StableText, dropping
the font-mono + tabular-nums workaround from the latter.

Also renames statusbar.ts → statusbar.tsx since LiveDuration now
returns JSX.
2026-07-20 16:39:20 +00:00
Brooklyn Nicholson
fb2a35c0b5 perf(desktop): stop tool rows re-rendering on session/cwd change + memo leaves
Two tool-render wins during streaming / on session switch:

1. Every ToolEntry did useStore($activeSessionId)+useStore($currentCwd), so any
   session or cwd change re-rendered *every* mounted tool row — but they're only
   read inside the preview-artifact effect. Read .get() at fire time instead
   (the effect only runs when a previewable target appears); no subscription.

2. memo() AnsiText + CompactMarkdown. Their text props are string values
   (value-equal across renders), so memo skips the re-render — and the per-tick
   ANSI parse / Streamdown re-run — when a parent ToolEntry re-renders on an
   unrelated stream delta.

No behavior change. typecheck + eslint clean; tool fallback tests green (30).
2026-07-19 22:58:22 -05:00
Brooklyn Nicholson
13337edcbe refactor(desktop): merge the two windowed diff returns into one 2026-07-19 21:20:36 -05:00
Brooklyn Nicholson
5ecf06e0ed perf(desktop): virtualize the review-pane diff (no more full-Shiki freeze)
Selecting a large changed file in the review pane froze it: FileDiffPanel with
no fullText + no showLineNumbers rendered SyntaxDiff over EVERY line — a full
Shiki highlight + thousands of mounted DOM nodes — because windowing was tied to
showLineNumbers/fullText and the review call had neither.

Decouple windowing from the gutter:
- `windowed = showLineNumbers || virtualized`; windowed paths always render the
  fixed-row chunked body (TokenizedDiffBody chunked / PreviewDiffRows), never
  SyntaxDiff, so only visible rows mount.
- New `virtualized` prop → windowed scroller WITHOUT the line-number gutter.
- Review passes `virtualized` + the preview's fill className.

Preview (showLineNumbers + fullText) and tool-card (compact) render byte-for-byte
as before — the gutter body just reads the same chunked window it already used,
and the no-fullText+highlight case (previously SyntaxDiff) now windows too.

tsc + eslint clean. Visual paths preserved by construction; needs an in-app
eyeball on a large review diff.
2026-07-19 21:13:08 -05:00
brooklyn!
c856f36459
perf(desktop): kill the layout-thrash cascade on session switch (#66033)
Follow-up to #65890 (router transitions off) and #65898 (structural
compare + first-paint budget): profiling the switch path on real 1000+-
message sessions with a new CDP harness showed the remaining freeze is
NOT markdown rendering — it's a forced-reflow cascade from mount-time
layout reads interleaved with style writes across the transcript's
layout effects, plus the first-paint budget cut landing too late to
stop the full-budget commit.

Measured on the two largest local sessions (996 and 1363 messages),
main-thread longtask totals per switch: warm 2450ms -> 557ms and
1158ms -> 194ms; first paint 1690ms -> 444ms. Harness:
scripts/profile-session-switch.mjs (same CDP family as
profile-real-stream.mjs).

- use-resize-observer: drop the synchronous initial callback and ride
  the observer's spec-guaranteed first delivery instead (same frame,
  after layout, before paint). The sync call ran while the commit's
  layout was dirty, so every size read in a callback forced a full
  reflow — with one instance per user bubble (measureClamp read
  scrollHeight, then WROTE --human-msg-full, re-dirtying layout for the
  next bubble), the switch commit thrashed for over a second. Inside RO
  timing the same reads are free. Composer metrics (2x
  getBoundingClientRect + documentElement style writes) rides the same
  fix.
- Same class, same fix at the remaining call sites profiling surfaced:
  ExpandableBlock and TerminalOutput (dozens per tool-heavy transcript)
  now measure/pin via RO initial delivery; the tool-window and
  thinking-preview pins drop their sync pin() call; the thread
  timeline's initial active-tick compute joins its existing
  scroll-time rAF batching so back-to-back transcript updates coalesce.
- thread/list: cut the render budget in the RENDER phase (state-from-
  props adjustment) instead of the post-commit layout effect. The
  effect-time cut was too late — on a warm switch React first built and
  committed the full 300-part tree, then re-rendered at 60, then bumped
  back to 300, so the expensive commit still happened (and on a cold
  switch the bump rAF usually fired while the transcript was still
  empty, so the prefetched messages rendered at full budget anyway).
  The render-phase cut restarts the component before any child renders;
  a second trigger handles the cold path where messages land later
  under the same sessionKey.
- thread/list: backfill 60 -> 300 inside startTransition so the older
  turns' markdown+shiki render is interruptible background work instead
  of a synchronous freeze one frame after the switch paints. Functional
  Math.max so an urgent "Show earlier" click can't be rebased back down.
- composer focus: skip the rAF/timeout focus retries when the element
  is already focused — focus() runs the full focusing steps (forcing
  layout) even on the active element, ~585ms per switch on a large
  dirty DOM.
- Replace the tautological render-budget test (it re-declared the
  constants locally and asserted 60 < 300) with behavior tests of the
  now-exported buildGroups + firstVisibleGroupIndex.

Verification: apps/desktop `npx tsc --noEmit` clean; full
`npx vitest run` 210 files / 1763 passed; manual CDP check confirms the
deferred backfill commits the full transcript, stays pinned to bottom,
and "Show earlier" still pages.
2026-07-16 22:46:08 -04:00
github-actions[bot]
75f45a0692
fmt(js): npm run fix on merge (#65229)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-15 21:40:56 +00:00
Brooklyn Nicholson
1fa3886bce chore(desktop): remove the DEV Shift+H heart preview
The real trigger (core `reaction` event on affectionate messages) is live, so
drop the dev-only hotkey and its always-mounted listener.
2026-07-10 16:09:23 -05:00
Brooklyn Nicholson
3aaf7e3876 feat(desktop): TikTok-style vibe hearts on a reusable particle system
Add a glyph-agnostic ParticleField (float-up + organic sway/bank + springy
pop-in), skinned as pink pixel hearts. Hearts play on the pet when one is out
(in-window or popped out) and celebrate alongside; otherwise they rise from the
composer. A generic $petReaction bus mirrors the burst to the pop-out overlay
window so it reacts even while the app is minimized.

Consume the core `reaction` event to fire hearts on affectionate messages. DEV
Shift+H previews a burst.
2026-07-10 05:42:14 -05:00
teknium1
7ff86f4458 refactor(desktop): route preview-pane mermaid fences through shared embeds registry
Drop the duplicate mermaid-block.tsx (own mermaid.initialize + render path,
theme frozen at first load) and wire preview-file.tsx's MarkdownCode through
the existing RichCodeBlock registry from #52935 instead. One mermaid init
path, theme-flip re-init, Zoomable + copy-as-PNG, RichBoundary error
fallback — and the preview pane gets svg fences for free. Shiki block stays
as the fallback for all other languages.
2026-07-07 02:39:18 -07:00
teknium1
c0adfd4a67 feat(desktop): render Mermaid code blocks in markdown file preview
Salvaged from #40531; surgically reapplied onto current main (i18n'd
preview-file.tsx). mermaid dep already present on main.

Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
2026-07-07 02:39:18 -07:00
Brooklyn Nicholson
914d19b3a9 fix(desktop,gateway,mcp): post-merge — CI contract, review corrections, hub search
Post-merge follow-ups + several review rounds + a hub-search rework, folded together.

Merge-scuff restores (a stale-base refactor had reverted two live-on-main fixes):
- gateway: SessionStore compression-tip healing + its regression test.
- desktop: messaging session/transcript polling in desktop-controller
  (MESSAGING_POLL / ACTIVE_MESSAGING_SESSION_POLL, refreshMessagingSessions,
  refreshActiveMessagingTranscript, the richer sameCronSignature) so inbound
  platform traffic updates live again instead of freezing until manual refresh.

Profile-switch isolation (epoch/close/guard on every profile-scoped async):
- Hub store clears + in-flight runHubAction bails (and swallows the post-switch
  404 instead of a phantom toast); hub preview/scan/search/sources profile-scoped.
- MCP: probe/auth epoch guards, dirty-draft reset, sidebar mutations blocked
  until config resettles AND every persist re-checks the epoch post-await;
  profilePending clears on config settle incl. error; logs re-key on profile.
- Model settings reload on switch and epoch-guard setModelAssignment /
  saveMoaModels / API-key activation.
- Config draft resets + cancels its autosave on switch; skill editor/archive and
  star-map node dialogs close on switch; openSkillEditor / star-map openEdit
  discard stale fetches; tool-usage analytics loads are profile-guarded/keyed.

Correctness + UX:
- Unique per-skill action names for hub install AND uninstall; hub/​catalog rows
  flip only on a clean exit_code; catalog install polls the background bootstrap
  to completion, reconciles the mcp.json draft (no dropped server), and fails
  loudly on non-zero exit; MCP catalog query keyed by profile.
- /test reports needs-auth for anonymous auth:oauth servers; /auth snapshots +
  restores tokens on a failed re-auth and clears the full 300s callback window.
- config-settings shows a retry on load failure; CodeEditor/JsonDocumentEditor
  go read-only while saving so edits typed mid-save aren't dropped.
- Deep-link highlighter deletes its param only after a successful scroll.
- Restored the PageSearchShell trailing slot → Artifacts refresh button/spinner.
- /settings?tab=mcp redirect keeps server=.

Progressive hub search: fan out one query per backend-searchable source
(index-covered API sources stay unsearchable → no ~70-call GitHub re-hammer),
merge/dedupe by trust as each lands, per-source spinner overlaid on the dimmed
chip — results stream in without blocking on the slowest, no layout shift.

test(web): /api/skills list carries usage + provenance (CI contract).
2026-07-03 15:22:43 -05:00
Brooklyn Nicholson
715aa3de85 refactor(desktop): adopt shared utils + app-wide cleanups
Route the app off its hand-rolled helpers onto lib/{text,time,format,json-format}
and the new primitives, plus assorted small tidy-ups:
- compactNumber for counts/tokens; normalize/capitalize/asText at the many
  filter/label sites; shared Intl date/time formatters; row-hover + framed
  editor adoption; scrollbar-gutter + padding parity on list surfaces.
- Messaging/Artifacts/Cron search hints + narrow-viewport tab dropdown;
  floating-pet adopts useOnProfileSwitch; number formatting in statusbar,
  command-center, agents.
- Electron: native overlay width + backend spawn tidy.
- Settings > Keys: credential fields read as plain subtext (all-unset) until
  the group is focused or expanded, then take full input chrome with no
  horizontal/vertical shift; inline Remove (trash) + Save mirror SearchField's
  trailing-clear pattern instead of a floating hint that overlapped the card;
  Esc still cancels. Drops the now-dead or/escToCancel i18n keys.
- Shared TabDropdown/ResponsiveTabs (components/ui): PageSearchShell and the
  Command Center log file/level filters reuse the one narrow-width collapse.
- OverlayNav: data-driven pane nav — persistent rail on wide, a single dropdown
  riding the titlebar strip on narrow; Settings and Command Center adopt it, and
  the mobile dropdown carries the same section icons as the rail. Fixes narrow
  vertical centering, redundant mobile section titles, gateway-status wrap, and
  Panel master/detail stacking.
- OverlayIconButton is now the titlebar ghost button, matching the close X at
  every size. Settings sub-view nav opens section + sub-view in one navigate so
  API-keys/accounts actually open on narrow.
- Settings > Model: cube icon (was the {} namespace glyph) and a DOM-shaped
  skeleton in place of the centered spinner.
- Command palette / session switcher clear the macOS traffic lights on small
  screens.
- Prettier/eslint sweep across the touched files.
2026-07-03 13:48:44 -05:00
Brooklyn Nicholson
e0325cf769 feat(desktop): Capabilities foundation — shared utils, master-detail, editors, primitives
The reusable base the Capabilities rework sits on:
- lib/{text,time,format,json-format}: consolidate ~30 hand-rolled string/date/
  number/JSON helpers behind one set of tested utilities.
- master-detail scaffold (MasterDetail / ListColumn / DetailColumn / DetailPane /
  CapRow / ListStrip / ToolChip / ICON_BUTTON) + row-hover; tabs-as-data
  PageSearchShell; EmptyState + ErrorBanner; a shared LogTail terminal surface.
- framed CodeEditor + JsonDocumentEditor, wired into every in-app markdown/JSON
  edit surface (profile SOUL.md, memory nodes, right-click sidebar profile).
- shared React Query cache helper (writeCache) + per-profile-switch/ debounce hooks.
2026-07-03 05:08:13 -05:00
Brooklyn Nicholson
b6e57e215b refactor(desktop): share theme-repaint observer; memory-graph depth polish
Extract the copy-pasted "re-resolve on theme repaint" MutationObserver into a
shared hooks/use-theme-epoch (useThemeEpoch + onThemeRepaint) and consume it
from the star map, image-gen placeholder, and useIsDark instead of each hand-
rolling its own root observer. Keeps the post-paint read the canvas probes need
(useTheme() would read stale CSS — child effects run before applyTheme).

Also: light-mode band depth (inner wash), travelling-glow core scramble, and
dark-only timeline bloom.
2026-06-30 02:06:29 -05:00
Brooklyn Nicholson
62fe9fd101 style(desktop,tui): fix all lint/type/formatting issues
Bring apps/desktop and ui-tui to a clean state for typecheck, eslint,
and prettier:

- Run prettier across both trees (printWidth/wrap drift; prettier is not
  CI-enforced for these JS projects, so main had accumulated drift).
- Apply eslint --fix for padding-line-between-statements and perfectionist
  import/export sorting.
- Manual fixes for non-auto-fixable rules:
  - remove unused node:net import in electron/main.cjs (uses Electron net)
  - replace inline `typeof import(...)` annotations with top-level
    `import type * as EnvModule` in two ui-tui test files
  - scoped eslint-disable no-control-regex on intentional sentinel/ANSI
    regexes (mathUnicode.ts, text.ts)
  - resolve react-hooks/exhaustive-deps per-case: correct swapped/missing
    deps, collapse redundant session.* members, and justified disables on
    settings mount-only data-load effects to preserve run-once behavior

No behavior changes; test pass/fail counts are unchanged from the main
baseline.
2026-06-26 01:04:33 -05:00
Brooklyn Nicholson
ff81365988 feat(desktop): in-app spot editor for the file preview pane
Adds a CodeMirror 6 spot editor to the right-rail file preview so users can
make quick edits in-app without leaving for an IDE. Entering edit mode is a
pure in-place swap of the read view — same fixed-height header, same gutter
geometry/typography (mirrors SourceView 1:1) so nothing shifts — toggled via
the Edit button, a bare `e` when the pane is hovered/focused, or the tab.

- Save path is transport-agnostic (writeDesktopFileText): local Electron IPC
  or a new hardened POST /api/fs/write-text on the dashboard server (path
  validation, parent-must-exist, regular-files-only, size cap, atomic
  temp-file + os.replace), behind the existing auth middleware.
- Stale-on-disk guard re-reads before writing and offers overwrite vs
  discard-and-reload instead of clobbering external/agent edits.
- VS Code-style modified dot on the tab; ⌘/Ctrl+S and ⌘/Ctrl+Enter save,
  Esc cancels; GitHub highlight style matched to the read view's Shiki theme.
- Typing stays render-free (draft in a ref; dirty flips once at the boundary).
2026-06-25 19:50:25 -05:00
Brooklyn Nicholson
344415892f feat(desktop): add shared project UI primitives 2026-06-25 16:40:27 -05:00
Brooklyn Nicholson
cb17a9efb2 fix(desktop): stop auto-opening tool previews
Drop gateway-event preview registration so HTML artifacts from tool results
no longer pop the rail. De-dupe the inline preview card label.
2026-06-22 19:21:20 -05:00
Brooklyn Nicholson
61c266b0dc style(desktop): soften dark-mode syntax highlighting
Share one SHIKI_THEME (github-dark-dimmed) across code blocks and inline
diffs so they can't drift, and pull token saturation/brightness back via a
`.shiki` dark-mode filter. The dimmed theme alone only changes the
background — which both surfaces strip — so the bright foregrounds needed
the filter to actually calm down.
2026-06-22 05:16:18 -05:00
Brooklyn Nicholson
ac128af1ce feat(desktop): syntax-highlight inline diffs via Shiki
Unify the diff renderer onto the same Shiki path as code blocks: highlight
the marker-stripped change content in the file's language, then a per-line
transformer layers the add/remove tint + gutter accent on top. Falls back
to the plain color-only renderer when the language is unknown, over budget,
or while Shiki loads.

- shikiLanguageForFilename(): extension → bundled-language id (shared
  filename-token helper with codiconForFilename).
- code display:grid so full-width line tints don't double with newline
  nodes; theme surface stripped so context lines stay transparent.
2026-06-22 05:10:23 -05:00
Brooklyn Nicholson
a61baa9615 feat(desktop): PR-style file diffs in chat
Render write_file/edit_file/patch as a reviewable diff instead of raw
result JSON, closer to a Cursor/T3 per-edit review.

- Unified diff via FileDiffPanel: strip git file-header + @@ hunk noise,
  drop the +/- gutter, color by line with a 2px gutter accent, full-bleed
  to the card, transparent context lines, compact scroll height.
- Header shows filename + language icon + +N/-N stats; full path moves to
  a hover tooltip (no Edited verb, no ms).
- Treat the three file-edit tools uniformly (isFileEditTool); read diff
  from inline_diff or patch's diff field; suppress raw-arg detail.
- Reusable FileTypeIcon primitive sharing the code-block icon mapping
  (codiconForFilename), codicon fallback.
- Per-row scaffolding fade (not the group wrapper, which trapped child
  opacity); expanded edits stay full, collapsed fade; keyboard-only focus
  lift. Hide diff-less rehydrated creates that read as dupes.
2026-06-22 05:04:13 -05:00
Brooklyn Nicholson
eed78d6ebb fix(desktop): composer popout polish — peel-off placement, panels, chip editing
- Peel-off undock drops the floating composer under the cursor (centered
  horizontally, preserving the vertical grab offset) instead of snapping to
  the docked corner.
- Unify the / · @ · ? completion drawer and the attach (+) menu onto one
  shared glassy panel primitive (composerPanelCard): smallest theme font,
  hairline border, nous shadow; floats off the composer, inset from the left.
- Directive chips: Backspace removes the chip + its auto-inserted trailing
  space atomically (no orphaned space), and a phantom trailing block left by
  contenteditable no longer falsely expands the composer to two rows.
- Model picker: scroll area capped at max(150px, 30dvh); footer rows aligned
  (matching icons, dropped a redundant margin).
- Composer focus shifts the border ~15% toward foreground (no fill change);
  input is cursor-text; trimmed control icon/button sizes.
2026-06-20 02:10:38 -05:00
brooklyn!
9a2f2756f7
fix(desktop): allow selecting slash output and shell logs in thread (#49063)
System messages (/debug, /status, etc.) were not in the desktop app's
text-selection allowlist, so log output in the thread could not be copied.
2026-06-19 13:59:09 +00:00
Brooklyn Nicholson
0138282f97 perf(desktop): keep oversized messages from freezing the chat
A multi-MB message (logged bundle, huge tool dump) froze the renderer
before any paint: Streamdown runs `preprocess` + `marked` lex over the
whole string synchronously in a useMemo, an uninterruptible long task
that no try/catch or content-visibility can help (our JS runs before the
browser ever skips layout). Tiered fix:

- Message gate: past 200KB, bypass markdown entirely and render the raw
  text in `content-visibility:auto` line-chunks — synchronous work is
  bounded to a string split, the browser virtualizes layout natively,
  and every line stays in the DOM (selectable, find-in-page).
- Code-block budget: past 3k lines / 150KB, skip Shiki (which emits a
  span per token) and render plain, chunked the same way.
- Collapse/expand: a reusable ExpandableBlock clamps code blocks and the
  huge-text fallback to a 120px preview with a gradient + chevron,
  expanding to 300px. The inner element is always a scroll container so
  the content-visibility chunks stay lazily laid out in both states.

No content is ever dropped; the copy button (card header) always yields
the full block.
2026-06-17 08:25:52 -05:00
Brooklyn Nicholson
b82d2e549f fix(desktop): keep the diffusion placeholder circular at any aspect
Normalise the radial bloom by the shorter side so portrait/square
placeholders aren't squished into an ellipse.
2026-06-13 02:45:34 -05:00
Brooklyn Nicholson
b15dc58064 fix(desktop): keep generated images in the tool slot, not inline
The image-generate tool showed a placeholder, then the model echoed a
(often different) image inline in its prose — a second, jarring copy in
the wrong place, dimmed as tool scaffolding, with a misplaced download
button.

Now the generated image lives only in the tool slot:
- Strip every embedded image/media link from the assistant prose of a
  message that produced an image (the model frequently restates the
  remote URL while the result holds the local path), preserving the
  agent's words. Applied on hydration, live deltas, and completion.
- One stable frame sized from the aspect_ratio arg up front, so the
  diffusion placeholder and the decoded image share the same box and
  crossfade with no layout shift; the box derives its height from the
  true ratio on load (no letterboxing).
- Exempt generated images from the tool-block dim-until-hover rule.
- Extract a shared useImageDownload hook + ImageLightbox so the tool
  image and markdown images share one implementation.
2026-06-13 02:42:15 -05:00
brooklyn!
0a7a81835b
Merge pull request #45255 from NousResearch/bb/desktop-stuck-tool-rows
fix(desktop): dismiss settled tool rows (persistent, caret-safe)
2026-06-13 02:08:01 -05:00
brooklyn!
7d183f6497
fix(desktop): theme the image-gen placeholder instead of a white square (#45354)
The diffusion placeholder read `--dt-*` tokens via
`getComputedStyle().getPropertyValue()`, but those resolve through `var()`
chains into `color-mix(in srgb, …)` — returned verbatim and unparseable, so
every token fell to a hardcoded light fallback (white card). In dark mode the
placeholder rendered as a white square.

Resolve each token through a throwaway probe element's `color` so the browser
computes it to a concrete color, and teach `parseColor` Chromium's
`color(srgb r g b / a)` serialization. Re-resolve on theme repaint via a
MutationObserver rather than per animation frame.
2026-06-12 21:45:24 -05:00
Brooklyn Nicholson
e90672696e feat(desktop): worktree-aware sidebar grouping + composer/sidebar UX fixes
Group recents as parent-repo → worktree → sessions using local git
metadata (probed over IPC, with a path-name heuristic fallback for
remote backends). Single-worktree repos collapse to one level. Sessions
order by creation time and never reshuffle on new messages.

Also: fuse the status stack to the composer border, restore icon actions
in the queue panel, fix sidebar label truncation and drag styling, hide
sticky-message attachments while pinned, and bump the terminal font.
2026-06-12 18:18:39 -05:00
Brooklyn Nicholson
b16e22b8f2 fix(desktop): persist tool-row dismissal across virtualization; keep caret hittable
Salvage of #45240. The dismiss-settled-tool-rows affordance was correct in
intent but had two issues against current main:

- The thread is virtualized, so a row's component unmounts/remounts as it
  scrolls. Component-local `useState` dismissal was forgotten on remount and
  the row popped back. Move dismissal into a session-scoped nanostore keyed by
  the stable disclosure id (mirrors $toolDisclosureOpen), so a dismissed row
  stays gone while scrolling but a reload restores real history instead of
  permanently rewriting it.
- The dismiss button lived in DisclosureRow's absolute `trailing` slot — the
  exact "opacity-0-but-clickable control fights the caret" pattern the trailing
  comment warns against. Add an in-flow `action` slot that lays out at the far
  right so an interactive control never overlaps the caret's hit-target,
  regardless of title length, and move the dismiss button into it.

Adds a remount regression test alongside the existing dismissal coverage.
2026-06-12 17:34:48 -05:00