Commit graph

1533 commits

Author SHA1 Message Date
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
hermes-seaeye[bot]
2686cfa5b4
fmt(js): npm run fix on merge (#71845)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-26 08:20:35 +00:00
Brooklyn Nicholson
e7f4b95b47 refactor(desktop): tighten the surface-var helper
Fold the null fallback into chatSurfaceRoot so both callers share one
target resolution, hoist the var names next to it, and drop the
duplicated rationale from the status-stack comment.
2026-07-26 03:20:12 -05:00
brooklyn!
1aed1f7bf4
Merge pull request #71812 from NousResearch/bb/memory-tool-card
fix(desktop): stop painting healthy tool rows as errors
2026-07-26 03:12:25 -05:00
Brooklyn Nicholson
c52bc5ec95 fix(desktop): scope measured-height vars to each chat surface
The composer and the out-of-flow status stack published their measured
heights onto document.documentElement, but both components mount once per
chat surface. Session tiles render a full ChatView beside the workspace
pane, so N surfaces raced for one value: a background tab with a tall
status stack inflated the foreground thread's bottom clearance and pushed
its jump-to-bottom button into mid-screen. Whichever surface unmounted
last also cleared the var for everyone still showing.

Publish onto the surface's own root instead, and re-declare the clearance
calc there — `:root` substitutes the root measurements once, so scoping
only the inputs would leave every thread reading the same value.
2026-07-26 03:05:30 -05:00
Brooklyn Nicholson
87ee6a52fa fix(desktop): count output_preview as command output
The nonzero-exit guard already knew a failing exit code with real output
usually isn't a failure worth painting red. It only looked at output /
stdout / stderr, but background-process polls report their text under
output_preview — so a poll of any process that exited nonzero rendered
destructive-red with no error to show, even when the output was routine
npm chatter. Half the red process rows in a real session history were
this case.
2026-07-26 02:23:25 -05:00
hermes-seaeye[bot]
a97b6ff8f6
fmt(js): npm run fix on merge (#71818)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-26 07:20:08 +00:00
Hermes Agent
21a2185f86 fix(desktop-e2e): poll for the finished-unread dot instead of sampling once
Sentinel-released processes exposed a second bare-sample assertion in the
same file. The unread-dot check ran a synchronous .count() 140ms after the
running dot cleared:

  15.91s  poll "dot should disappear" -> 2
  16.05s  ... -> 0  (running dot gone)
  16.05s  bare .count() for unread dot -> 0  FAIL

"Finished — unread" is an event-driven transition that lands just after the
running dot clears. The old fixed `sleep 5` happened to leave enough slack
between the two that a single sample usually caught it; releasing the
process deterministically removed that incidental slack and made the latent
race deterministic instead.

Poll for it, matching how sidebar-states.spec.ts already asserts this exact
dot. The split-tile assertion at line 234 stays a bare sample on purpose —
it asserts an absence (toBe(0)), where polling would only wait for something
that must never appear.
2026-07-26 00:12:36 -07:00
Hermes Agent
3a3bc41c7e fix(desktop-e2e): end the sidebar background-dot wall-clock race
The cross-session sidebar specs asserted a state that could expire before
they looked at it, making them the flakiest tests in the suite — two reds
on unrelated PRs within three minutes on 2026-07-26.

Root cause, from the failing run's trace: the tests need a background
process that is still RUNNING after the agent turn finishes, but the
process was a fixed `sleep 5` racing two other clocks — the turn itself
(two model round trips plus a real subagent delegation) and the 4s
success linger before a finished task auto-dismisses. On a loaded runner
the "dot should appear" poll took 7.5s to see the dot; by then `sleep 5`
had already exited, `waitForFunction(finalText)` returned in 0.08s
because the turn was long done, and the next line — a bare synchronous
`.count()`, not a wait — sampled 0.

The process lifetime is now test-controlled: `createBackgroundReleaseHandle()`
mints a sentinel path, the scripted command blocks until that file
appears, and the test releases it exactly when it wants the dot to clear.
One clock instead of three, and the "turn done, process still running"
state is stable rather than a window to catch. The wait is bounded (60s)
so a forgotten release can't hang a worker, and `sleep 5` stays as the
default for callers that pass no handle.

No product code touched — E2E harness only.
2026-07-26 00:12:36 -07:00
brooklyn!
6deb92df52
Merge pull request #71800 from NousResearch/bb/project-sort
fix(desktop): sort projects by real activity and record terminal session cwd
2026-07-26 02:06:00 -05:00
Brooklyn Nicholson
717746ecf8 feat(desktop): give memory tool rows a title and glyph
Memory rows fell back to the auto-derived name, so they read as a bare
"Memory" / "Running memory" next to a generic icon. Add the tool to
TOOL_META with proper copy across all five locales, plus a solid brain
glyph in the existing Phosphor fill set.
2026-07-26 02:00:01 -05:00
Brooklyn Nicholson
6192d3bb57 fix(desktop): stop painting memory writes as errors
A memory tool call rendered destructive-red with an alert glyph and, when
expanded, a raw dump of the whole args + result payload.

Two causes. The red came from toolStatus treating any error text as a
failure: the memory store rejects an over-budget batch so the agent can
retry a smaller one, which is routine bookkeeping, not something the user
acts on. Those now land on the existing amber warning tier.

The payload dump came from memory having no case in toolSubtitle or
toolDetailText, so both fell through to the generic stringify-everything
fallback. Both now surface just the human-readable line.
2026-07-26 01:59:54 -05:00
brooklyn!
6ffd7302bf
Merge pull request #71789 from NousResearch/bb/timeline-idle
perf(desktop): stop the thread timeline working when nothing can see it
2026-07-26 01:58:26 -05:00
brooklyn!
d471fc9560
Merge pull request #71799 from NousResearch/bb/model-picker-perf
perf(desktop): faster model picker open + idle CPU on stacked tabs
2026-07-26 01:51:20 -05:00
Brooklyn Nicholson
428c909d28 fix(desktop): bind a tab's slash command to its own session
A slash command in a ⌘T tab or split pane routes through the primary
chat's dispatcher, which read the FOREGROUND view's identity for two
decisions it had no business asking the foreground about.

Busy: the gate read `busyRef`, a mirror of whatever chat is on screen.
A brand-new tab with zero turns was told "session busy — message queued"
because an unrelated chat was mid-stream, and the converse let a
background send fire into a live turn.

Identity: the output writer bound to the foreground's stored session, so
a tab's transcript writes re-keyed its cache entry onto the primary's
stored id and its queued payload landed on the primary's queue — the
kickoff would then drain into the wrong conversation. `submitText` also
dropped an explicit target when it routed to a slash command, running a
queue drain's command against whatever was in front.

Read the target session's own published state for both. One shared
resolver so submit and slash cannot drift apart again.
2026-07-26 01:44:03 -05:00
Brooklyn Nicholson
7ef3f1407b fix(desktop): gate the slash/submit busy queue on the target session
A slash command runs against the session `resolveTargetSessionId` picks,
which is routinely not the session on screen — a tile, a route rebind, or
a session created by the call itself. Both prompt pipelines gated on
`busyRef`, the FOREGROUND view's busy flag, so one session's send was
gated on another session's turn: a stale foreground `true` (a warm resume
of a still-running chat leaves one behind) parked an idle session's
command on the composer queue and reported "session busy" about a session
doing nothing. The converse also leaked — a background send could fire
mid-turn while the foreground happened to be idle.

Read the published per-session state instead, falling back to the
foreground flag only when the target has no state yet (a just-minted
session whose first publish hasn't landed). One shared resolver so submit
and slash cannot drift apart again.
2026-07-26 01:39:56 -05:00
Brooklyn Nicholson
bf726a7ab8 fix(desktop): drop the fade mask on the live thinking preview
The preview window masked its top 28% to transparent while reasoning
streamed, so a gradient appeared over the thinking text and vanished the
moment the block finished. Remove the mask; the max-height window and
bottom-pinned scroll still keep the preview compact and following the
newest tokens.
2026-07-26 01:21:47 -05:00
Brooklyn Nicholson
19025eb7ce fix(desktop): rank projects by real activity, not disk-scan time
The sidebar's project overview put git checkouts with zero Hermes sessions
above the repos the user actually works in, and dragging a project into place
did not stick.

Two causes. The repo discovery payload folded `discovered_repos.last_seen`
into `last_active`, but `last_seen` is when the disk scan last saw the
directory, so every scanned checkout was stamped "just now" and outranked
real work. Activity is now session-derived only; a repo with no sessions
reports no activity.

The overview also applied the manual drag-order through `orderByIds`, which
floats every id missing from the saved order to the top. That is right for
sessions, where a new chat should not sink, but the overview keeps receiving
newly-scanned repos — so once the user dragged anything, each new discovery
jumped above their hand-picked list. Projects the user has not ordered now
keep their deterministic position: ones with real activity still surface on
top, zero-session discoveries sort below the ordered list.
2026-07-26 01:20:23 -05:00
Brooklyn Nicholson
7dec2c9640 perf(desktop): add model-picker open-latency probes
probe-model-picker.mjs times pill-click → menu painted over N rounds;
profile-model-picker.mjs wraps one open in a CPU profile with a
top-self-time table. Both attach to the perf:serve instance.
2026-07-26 01:10:22 -05:00
Brooklyn Nicholson
0b745bc993 perf(desktop): pause glyph spinners on hidden tabs
Each kept-alive tab whose model hasn't resolved ticks a GlyphSpinner —
setInterval + setState + a React commit every ~80ms, per mounted tab,
for pixels behind the active one. Gate the interval on the pane's
visibility context; the visible tab keeps its spinner, hidden tabs
resume from frame 0 on reveal.
2026-07-26 01:10:22 -05:00
Brooklyn Nicholson
667758ac24 perf(desktop): defer model-row submenu bodies until hover
ModelMenuPanel mounts a ModelEditSubmenu per model row, and each body ran
its hooks and built its JSX eagerly on menu open — ~90 rows of switches,
radio groups, and preset lookups nobody hovered yet, the largest app-code
slice in the open-latency profile. Wrap the body in a child component under
SubContent so Radix's presence gate leaves it unrendered until the submenu
actually opens; open latency drops roughly in half on a 91-model catalog.
2026-07-26 01:10:22 -05:00
Brooklyn Nicholson
91a8fe4a3e perf(desktop): stop the thread timeline working when nothing can see it
The prompt rail mounts in every chat surface, and a tab group keeps
inactive tabs mounted, so a background timeline was stringifying every
user prompt's full text on each store update — including on every
streamed assistant token, since the selector walked all messages — plus
running a scroll listener and a getBoundingClientRect per prompt against
a viewport nobody was looking at.

It now defers each piece until it can be seen. An inactive pane returns
before a single hook is declared (usePaneVisible, the same context the
hidden-tab transcript freeze uses), so no subscription is opened at all.
An active rail subscribes to prompt IDS rather than prompt text and
reads the transcript imperatively only when that signal changes, which
takes streaming off the derivation path entirely; an identical
derivation hands back the previous array so a filtered-out prompt
doesn't restart the measure effect. The offset pass bails below the
render threshold instead of measuring a rail that renders null, ahead of
the existing following-the-bottom fast path, and the hover popover keeps
its shell for the fade but builds its rows on first open.
2026-07-26 00:45:34 -05:00
brooklyn!
be00a7176c
Merge pull request #71780 from NousResearch/bb/multitab-perf
perf(desktop): make multitab streaming sessions fast
2026-07-26 00:41:07 -05:00
Brooklyn Nicholson
9173f589c6 perf(desktop): add a multitab scenario to the perf harness
N session tiles stacked as tabs, all mounted (keep-alive) and all
streaming concurrently through the real publishSessionState path — the
"several PR reviews at once" workload. Frame pacing + longtask metrics,
no backend or credits needed; the workload that exposed both fixes above
and the regression gate that keeps them fixed.
2026-07-26 00:34:00 -05:00
Brooklyn Nicholson
982a425162 perf(desktop): skip the timeline rect walk while following the bottom
ThreadTimeline's scroll compute read getBoundingClientRect for every user
message per scroll frame; interleaved with React's streaming style writes
each read forced a full reflow — the hottest self-time frame in the
multitab profile (620ms over one 5-tab run). While the viewport is pinned
to the bottom the active prompt is simply the last entry, so answer from
data and save the layout reads for actual scrollback.
2026-07-26 00:20:49 -05:00
Brooklyn Nicholson
47f0cdf3a2 perf(desktop): freeze hidden tab transcripts during streaming
Keep-alive keeps every ever-active tab mounted, but each hidden tab's
ChatRuntimeBoundary still subscribed to its view's $messages — so every
streaming delta flush (~30x/s) re-rendered every busy tab's whole thread,
and five concurrent sessions dropped the app to a crawl.

Flow the pane layer's visibility down as PaneVisibleContext and gate the
$messages subscription on it: a hidden tab freezes its transcript (status
dots stay live through the separate status atoms) and catches up in one
commit on reveal, since subscribe fires immediately with the current value.
2026-07-26 00:20:40 -05:00
hermes-seaeye[bot]
0ce9022e03
fmt(js): npm run fix on merge (#71740)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-26 04:11:26 +00:00
brooklyn!
559d0849a9
Merge pull request #71716 from NousResearch/bb/tui-link-label
fix(tui,desktop): stop link-title resolution from overwriting authored link text
2026-07-25 22:59:40 -05:00
Brooklyn Nicholson
5c5f11d23a test(tui,desktop): cover authored link labels and not-found titles
Warm the shared title cache before rendering so a resolved title is
available synchronously — the previous TUI assertion only proved a label
survived when no title had resolved, which passed on the buggy ordering.

Asserts the bug class on both surfaces: an authored label outranks a
resolved title and suppresses the fetch, an unlabeled link still resolves
one, and a "Page not found" title is discarded rather than rendered.

Co-authored-by: Kinkoolino-Hermes <297364961+Kinkoolino-Hermes@users.noreply.github.com>
2026-07-25 22:52:38 -05:00
Brooklyn Nicholson
17b8b3f657 fix(tui,desktop): treat "not found" page titles as unusable
Self-hosted forges answer a missing or private page with a 200 and a
"Page not found" title, which then rendered as the link's text. Both
surfaces keep their own copy of the error-title list, so extend both.

Co-authored-by: Kinkoolino-Hermes <297364961+Kinkoolino-Hermes@users.noreply.github.com>
2026-07-25 22:52:38 -05:00
Brooklyn Nicholson
f244deffee fix(tui,desktop): let authored link text outrank the fetched page title
Both chat renderers resolve link titles over the network and render them
in place of the link's text, unconditionally — so they also replaced text
the agent deliberately wrote. `[#71706](url)` rendered as the whole GitHub
page title, and prose labels were swapped out mid-sentence.

The TUI ordered `fetched || label` and always passed the URL to
useLinkTitle. Desktop looked guarded but wasn't: chat markdown hands
authored text to PrettyLink as `fallbackLabel`, not `label`, so
`useLinkTitle(label ? null : target)` never skipped the fetch and
`fetched || label || fallbackLabel` still let the title win.

Treat an authored label as the intent: it wins, and it skips the fetch. A
label that is just the URL still resolves, since `[url](url)` and `<url>`
are bare links wearing markdown syntax — desktop already applied that same
URL-equality rule before handing the label over.

Co-authored-by: Kinkoolino-Hermes <297364961+Kinkoolino-Hermes@users.noreply.github.com>
2026-07-25 22:52:34 -05:00
brooklyn!
8a71feb84c
Merge pull request #71709 from NousResearch/bb/first-run-local-start-error
fix(desktop): keep the first-run local-start error from being wiped on mount
2026-07-25 22:05:41 -05:00
brooklyn!
8b6ab1fba9
Merge pull request #71714 from NousResearch/bb/gated-health-probe
fix(desktop): connect to gated remote gateways that 401 the readiness probe
2026-07-25 22:03:05 -05:00
Brooklyn Nicholson
75456183b6 fix(desktop): wire the credentialed readiness probe and reauth latch into boot
Connects the preceding seams to the boot path:

- buildReadinessHealthProbe picks the probe credentials from the connection's
  authMode via resolveReadinessProbeAuth, and reports whether the probe is
  credentialed so waitForHermesReady can read a 401 correctly. The bearer
  goes through fetchJson's `bearer` option — a raw `headers` object is
  ignored there, which would have silently probed uncredentialed.
- authMode is threaded into all three waitForHermes call sites: the primary
  remote connect, the profile pool backend, and the SSH tunnel (loopback
  forward, token auth), so an old backend behind a tunnel gets the same
  compatibility path.
- The confirmed reauth failure latches in remoteReauthFailure and
  short-circuits startHermes, and is cleared on every recovery path —
  resetHermesConnection (soft/hard apply and reconnect), bootstrap reset,
  repair, and a CONFIRMED oauth sign-in. A cancelled or closed login window
  leaves the latch set so the overlay stays actionable.
- gatewayAuthProviders reads the public /api/auth/providers (cached per base
  URL, failures return []) so the oauth pre-flight guard can skip its hard
  failure for an all-password gateway while keeping the strict guard
  everywhere else.
2026-07-25 21:52:51 -05:00
Brooklyn Nicholson
36e2228a4c fix(desktop): latch a confirmed remote reauth failure so the overlay stays clickable
shouldLatchBackendStartFailure deliberately never latches a remote failure:
remote faults are usually transient and must stay retryable without an app
restart. A confirmed reauth rejection is the exception — it cannot self-heal,
because nothing changes until the user signs in again.

Worse, not latching actively prevents the recovery it was protecting. A
non-latching remote boot failure re-runs startHermes on every
getConnection/api call, re-emits running: true, and the boot-failure overlay
(visible = Boolean(boot.error) && !boot.running) hides itself — so the
"Sign in" button flickers out from under the user before it can be clicked.

Add shouldLatchRemoteReauthFailure as a separate predicate rather than
changing the existing one, so transient remote failures keep self-healing.
The two latches are complementary and never fire for the same failure.
2026-07-25 21:52:45 -05:00
Brooklyn Nicholson
cf4fb8993f fix(desktop): name the readiness-probe auth and password-gateway guard decisions
Two pure seams in native-auth-decisions.ts, following that module's existing
pattern — the value is the test that pins the contract so the god-file call
sites can't drift back to the buggy shape.

resolveReadinessProbeAuth decides how the boot readiness probe
authenticates, delegating to resolveOauthRestAuth for the oauth case rather
than duplicating the bearer-vs-cookie rule.

oauthGuardMayHardFail fixes a second way a gated gateway is misread.
authModeFromStatus maps the gateway's `auth_required: true` onto 'oauth',
but that flag only means the dashboard is GATED — it says nothing about how
you authenticate. A gateway whose providers are all username/password can
satisfy neither of the pre-flight guard's checks by construction:
start_login raises NotImplementedError, /auth/native/authorize rejects
password providers, and its cookies come from a plain password-login POST
rather than the /auth/callback redirect the OAuth partition is primed for.
The guard therefore threw "uses OAuth, but you are not signed in" one line
before a mintGatewayWsTicket that would have succeeded against that very
partition.

The helper returns false only when EVERY advertised provider is
password-based; an unknown or empty list keeps the strict guard, so backends
predating /api/auth/providers are unaffected.
2026-07-25 21:52:40 -05:00
Brooklyn Nicholson
8d025489cf fix(desktop): read a readiness-probe 401 by whether it was credentialed
The boot readiness probe calls the credential-free /api/health, and only a
404 flips it to the /api/status fallback. Both halves are wrong against a
gated gateway.

/api/health landed in ccab46ca4 (2026-07-24), after the v2026.7.20 tag, so
every container pinned to a release lacks the route. On those backends the
dashboard auth gate runs ahead of the SPA catch-all, so an unknown /api/*
path is rejected as unauthenticated rather than 404 — a credential-free
probe can never observe the 404 that the fallback keys on, and boot loops
until the 45s deadline reporting a healthy backend as "did not become
ready". Upgrading the backend is not a workaround for release-pinned
deployments.

Simulating a 0.19.0 backend (both the route and its PUBLIC_API_PATHS entry
removed, since ccab46ca4 added the two together) shows the probe's 401 means
two different things depending on whether credentials were sent:

  credential-free: /api/health -> 401 no_cookie, /api/status   -> 200
  credentialed:    /api/health -> 404,           /api/sessions -> 200

So the fix is not "fall back on any 401". Uncredentialed, a gate-shaped 401
identifies a missing route and must fall back. Credentialed, a 401/403 is a
rejected session and must fail fast — falling back would hit the public
/api/status, get a 200, and report a dead session as ready, deferring the
no_cookie to the first real API call.

Split the two cases and tag the credentialed rejection as a terminal reauth
error. A generic 401 without the gate shape, plus 429 and 5xx, keep polling
as before.
2026-07-25 21:52:26 -05:00
hermes-seaeye[bot]
6c9718151c
fmt(js): npm run fix on merge (#71706)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-26 02:46:01 +00:00
Brooklyn Nicholson
f97e7086c7 fix(desktop): keep the first-run local-start error from being wiped on mount
Clicking "Install Hermes locally" surfaced no error when the bridge was
missing, if the click landed before React drained the passive effect that
reacted to the first bootstrap snapshot. The effect cleared the transient
button state on every change of setupChoice.activeRoot, and the very first
snapshot counts as a change: it moves the root from null to its initial
value. The choice paints as soon as that snapshot commits, so a fast click
produced an error that the effect then cleared before it ever rendered.

The state now records the root it was produced under and is read back only
under that same root, so leaving a phase still discards it but arriving in
one cannot. Deriving it removes the effect rather than adding a guard to it.

Reproduced by observing the DOM with a MutationObserver instead of findBy*,
which only settles on a timer tick — by then the effect has already run and
the window is closed. Reverting the component change fails the new test.
2026-07-25 21:34:35 -05:00
brooklyn!
6ab5d2df2a
Merge pull request #71664 from NousResearch/bb/composer-slash-trigger
fix(desktop): make skills referenceable anywhere in the composer
2026-07-25 21:26:14 -05:00
brooklyn!
3a06908433
Merge pull request #71688 from NousResearch/bb/profile-gateway-inherit-label
fix(desktop): clarify inherited profile gateway
2026-07-25 21:22:41 -05:00
brooklyn!
a135b278dd
Merge pull request #71601 from helix4u/fix/desktop-model-picker-authority
fix(desktop): keep model picker aligned with session state
2026-07-25 21:20:54 -05:00
Brooklyn Nicholson
28c12c8c94 fix(desktop): localize the inherited-gateway copy for Arabic
The ar locale landed after the inheritance-label fix was written, so it
still told users to set a named profile to Local to inherit the default
and fell back to English for the renamed card.
2026-07-25 21:10:04 -05:00
KaliShodan
4656a1a009 fix(desktop): clarify inherited profile gateway 2026-07-25 21:06:40 -05:00
brooklyn!
a606d24cf2
Merge pull request #71679 from NousResearch/bb/default-effort
fix(desktop): honor the configured reasoning effort instead of assuming medium
2026-07-25 20:58:00 -05:00
Brooklyn Nicholson
43d1088ba2 refactor(desktop): give reasoning effort one owner
The seven effort levels were enumerated four times in four shapes: a
labels map, a radio-option list, a settings value array, and an enum
list for the config field. Each carried its own hardcoded medium
fallback, which is how the picker drifted from the configured default in
the first place.

Collapse them into lib/reasoning-effort, which owns the scale, the
labels, and the resolve/fallback rules. Net -80/+26 in source. Dropping
the effortLabelKey cast also means the i18n keys are now type-checked
against the canonical list instead of asserted into place.
2026-07-25 20:51:18 -05:00
brooklyn!
10160a1808
Merge pull request #71672 from NousResearch/bb/tab-session-titles
fix(desktop): name a Cmd+T session from its first message
2026-07-25 20:51:01 -05:00
Brooklyn Nicholson
412a535433 fix(desktop): scope mid-message references to skills and keep them as chips
Two corrections to the inline slash reference.

Only skills are offered mid-message now. A built-in like `/model` or `/new`
acts on the app, so it reads as nothing useful in the middle of a sentence —
whereas a skill is exactly the thing you want to point at while describing
work. A leading `/` is unchanged and still offers the full command set.

A picked skill also stays a pill in the sent message. The composer already
inserts one, but the message renderer knew nothing about slash references and
flattened it back to raw text on send. It now parses a mid-prose `/skill` into
a chip segment and renders it with the same styling the composer uses. The
submitted text is untouched — the chip still round-trips to the literal
`/clean` the backend expects — so this is presentation only. Path-like tokens
(`/usr/local/bin`) are excluded: unlike the caret-anchored composer trigger,
this scans finished text, so it also has to reject a token that runs on into
a path.
2026-07-25 20:45:11 -05:00
Brooklyn Nicholson
fab0e60570 fix(desktop): name a Cmd+T session from its first message
A Cmd+T tab's session is created unlisted (openNewSessionTile passes
`listed: false`), so it has no $sessions row until its first turn
persists and a refresh surfaces it. For that whole first exchange the
tab strip and the sidebar read "New session".

Cmd+N has no such gap: its session is created per-send, and
createBackendSessionForSend seeds the optimistic row with the user's
text as the preview. A tile never reaches that path — its runtime id is
already bound, so its createBackendSessionForSend seam is a no-op that
returns the existing id.

Seed the row the same way on a tile's first send. The tab strip already
re-syncs on $sessions (watchSessionTiles lists it in `also`), so the tab
and the sidebar both name the session within the first message; the
server's auto-title supersedes the preview when the turn completes.

Guarded so it only ever fires once per session: no-op on empty text and
on an already-listed row, matching across compression by lineage root so
a re-send can't clobber a real title with a raw message preview.
2026-07-25 20:44:19 -05:00
brooklyn!
a288fc341c
Merge pull request #71678 from NousResearch/bb/code-block-overflow
fix(desktop): keep code and diffs out of the tool overflow window
2026-07-25 20:42:22 -05:00