Commit Graph

4143 Commits

Author SHA1 Message Date
Tom 7b25941b0e fix(bot-mode): unwrap connections registry object so 'Create on' picker renders
host.connections() resolves the IPC handler hermes:connections:list, which
returns the registry OBJECT ({version, primary, connections: [...]}) — not a
bare array. CreateAgentDialog did setConnections(Array.isArray(value) ? value
: []), so on a multi-connection desktop the picker gate
(Array.isArray(connections) && connections.length > 1) never fired and the
'Create on' picker stayed hidden, making cross-machine bot creation
impossible despite the multi-connection feature being documented.

Any new agent is still created on the active gateway (unchanged behaviour);
the picker is the only path that regressed. The built-in Connections UI
(refreshConnectionsRegistry) consumes the same registry object, so the IPC
handler contract is left untouched.

Adds a regression test asserting the unwrap and that the IPC contract is
preserved. Plugin suite: 309/309 pass.
2026-08-19 15:06:57 -07:00
hermes-seaeye[bot] 657550716f fmt(js): npm run fix on merge (#90248)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-19 20:21:18 +00:00
Sara Burke a946169533 Merge latest upstream into fix/remote-bot-routing
Carry the verified connection-bound routing branch through the remaining
upstream changes at fe2752a05 without rewriting prior signed commits.
2026-08-19 16:16:26 -04:00
Sara Burke 336b3216d5 Merge current upstream into fix/remote-bot-routing
Preserve the connection-bound Bot and session routing implementation while
adopting upstream's modular Desktop API split and all changes through
b2057c168.
2026-08-19 16:14:41 -04:00
Brooklyn Nicholson 7f73978ee4 feat(desktop): close preview tabs on preview.close
Omit url to drop the whole rail; a url/path/label match closes that tab.
Background sessions still cannot yank the user's preview shut.
2026-08-19 14:59:52 -05:00
ethernet fdf6f1d4c8 feat(desktop): add an Appearance toggle that disables the intro splash
The wordmark and tagline on an empty chat had no off switch. Add an
Appearance row, Intro Splash, that hides it. The setting is on by
default, so the current experience does not change.

The splash is renderer chrome and no other Hermes surface can change
it, so the state stays local (localStorage) like the Chat Backdrop
toggle beside it. It does not mirror into gateway config.

Move the visibility condition out of the chat god-component into
shouldShowIntro(), next to the isRouteSessionMismatch() helper. The
tests prove that the toggle outranks every window and session clause:
off is off.
2026-08-19 15:22:49 -04:00
brooklyn! 87e32b6b30 Merge pull request #90070 from dcdexhome/fix/desktop-pin-sync-reentrant-crash
fix(desktop): stop re-entrant pin-sync reconcile from overflowing nanostores
2026-08-19 12:45:05 -05:00
hermes-seaeye[bot] eab087c06f fmt(js): npm run fix on merge (#90140)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-19 16:56:58 +00:00
brooklyn! d21cd51d86 Merge pull request #88887 from helix4u/fix/desktop-repair-missing-venv
fix(desktop): repair missing Windows runtime
2026-08-19 11:46:26 -05:00
brooklyn! 0a0497060a Merge pull request #90014 from xxxigm/fix/desktop-wrap-toast-titles
fix(desktop): wrap toast titles so long errors stay readable
2026-08-19 11:41:30 -05:00
brooklyn! 28803e68b4 Merge pull request #90113 from NousResearch/bb/review-submit-isolation
fix(desktop): isolate review submit to one composer (supersedes #90097, #85911)
2026-08-19 11:15:43 -05:00
brooklyn! 00e5a361b6 Merge pull request #89611 from NousResearch/bb/desktop-godfiles
refactor(desktop): decompose god files into atomic modules
2026-08-19 11:05:01 -05:00
Brooklyn Nicholson 830108c8d9 fix(desktop): send review agent-ship to the composer that opened it
The ship button always targeted `main`, so a tile Review still prompted
the workspace session. Remember the originating composer target with the
pane's cwd, capture the live surface at click, and toast if that chat
isn't on screen instead of dropping the click.

Co-authored-by: unsupportedpastels <theoldwizard123@pm.me>
Co-authored-by: youtiaowei <youtiaowei@users.noreply.github.com>
2026-08-19 11:01:58 -05:00
Brooklyn Nicholson 04956c1a87 fix(desktop): isolate composer submit to one visible surface
Review "Ask Hermes to open PR" was a window-level event that every mounted
composer claimed with `target === 'main'`, so one click shipped every open
session and project with dirty files. Bind the request to the visible
surface captured at click time.

Co-authored-by: unsupportedpastels <theoldwizard123@pm.me>
Co-authored-by: youtiaowei <youtiaowei@users.noreply.github.com>
2026-08-19 11:01:54 -05:00
brooklyn! d697457f38 Merge pull request #89837 from NousResearch/bb/glass-acrylic
feat(desktop): back window glass with Windows 11 materials, and scope its controls per OS
2026-08-19 11:00:57 -05:00
brooklyn! 1087381345 Merge pull request #89817 from NousResearch/bb/sidebar-all-profiles-scope
fix(desktop): keep recents when ALL-profiles scope has one profile (supersedes #84313)
2026-08-19 11:00:49 -05:00
Brooklyn Nicholson 808fde106d test(desktop): share the duplicated fixtures
The same AST sweep over specs found fixtures maintained in parallel
across suites that have no reason to know about each other.

Twenty-one specs each mounted useMessageStream themselves and ten of the
harnesses were byte-identical; twenty now take renderMessageStream, with
overrides for the seams that genuinely vary. The SessionInfo builder was
spelled out field-by-field in seven specs, so a new backend field broke
seven files instead of one. Twenty specs carried their own inert
ResizeObserver and eleven repeated the animation-frame, CSS.escape,
scrollTo and WAAPI stubs the transcript needs to mount at all — split by
scope into src/test/jsdom for what any component might need and the
assistant-ui folder's own kit for the transcript. Plus the window-state
bridge, deferred, the external-store thread runtime, the manual
createRoot harness, and the per-folder caret, env-var, provider and
session fixtures.

Left alone on purpose: the store suites' makePrimary, where the vi.mock
harness around it is the actual duplication and cannot be hoisted out of
a hoisted factory; electron's deferred, where reaching into src/ from
the main process would invert the layering for eight lines; and the two
suites that compose another hook alongside the stream.
2026-08-19 10:51:12 -05:00
Brooklyn Nicholson 9ab5d92fac fix(desktop): sort translucency named exports for eslint
Perfectionist wants values before types in the glass/Windows barrel.
2026-08-19 10:50:52 -05:00
Brooklyn Nicholson 7ed3a5cbe2 docs(desktop): repoint comments at the modules that now own the code
Comments naming gateway-event.ts, chat-messages.ts and hermes.ts as the
place to look, for files those symbols no longer live in.
2026-08-19 10:50:52 -05:00
Brooklyn Nicholson a8d87ac1d9 fix(desktop): keep the preload bridge alive under the sandbox
Deciding whether the OS can back glass needs os.release(), but every
Hermes window runs its preload with sandbox: true, where require is a
polyfill limited to electron, events, timers and url. The node:os import
threw before contextBridge ran, so window.hermesDesktop was never defined
and the app booted straight into "Desktop IPC bridge is unavailable".

Main already computes both verdicts, so preload asks for them over a
synchronous channel instead. No reply degrades to no glass, which is an
ordinary opaque window rather than a page thinned over nothing.
2026-08-19 10:50:52 -05:00
Brooklyn Nicholson 5a027a0081 refactor(desktop): give duplicated helpers one owner
Splitting the god files made a pile of copy-paste helpers visible and,
for the first time, fixable — sharing them previously meant importing a
god file. Hashing function bodies through the TypeScript AST found
twelve groups desktop-wide; production code is now at zero duplicates.

Each helper went to the module that already owns its concern:
firstStringField to lib/text, the two REST 404 predicates to
lib/gateway-rpc beside isMissingRpcMethod, useDebounced and
prefersReducedMotion to their hooks, the superseded-bootstrap guard to
electron/ssh-connection, the composer keyup handler to the trigger hook
that owns the rest of that state machine, and clampDataUrlReadMaxMb to
apps/shared, replacing a "keep these in sync" comment between two
copies.

Only helpers with no existing owner got a new file: lib/mcp-servers,
lib/audio-context, lib/keyed-timeouts, lib/pointer-drag, and the command
palette's status row. Error-shape predicates are the worst thing to
copy — when the backend changes how it reports a missing route, every
copy has to be found.
2026-08-19 10:50:52 -05:00
Brooklyn Nicholson 8a918bdc64 feat(desktop): scope the translucency controls to what each OS can do
The row offered one unlabelled 0-100 slider whose meaning changed with the
mode. Under Clear it is window opacity; under Glass it was never opacity at
all — it sets how much of the theme tint stays painted over the material.
Same track, same percent readout, two different things.

Glass now gets a labelled panel: Tint keeps the renderer lever, Fade is a
real native opacity on the ramp Clear uses, defaulting to 0 because fading
a glass window fades its text — the thing Glass exists to avoid. Frost
offers only the rungs the OS renders distinctly, so Windows shows three
instead of two buttons that composite identically; a frost saved on a Mac
highlights the button that renders the same backdrop rather than leaving
the picker blank, and is not rewritten.

Linux loses the row entirely, from the page and from settings search.
setOpacity is a documented no-op there and there is no material, so both
halves were dead — a lever that moved a number and changed nothing.
2026-08-19 10:50:52 -05:00
Brooklyn Nicholson f6e63237d4 refactor(desktop): extract IPC clusters from electron/main.ts
52 handlers move into five registrars — git, pet overlay, hud, fs and
terminal. Each takes injected deps (window handles, binary resolvers,
path hardening) following the existing electron/ module pattern rather
than closing over main.ts locals, and terminal-ipc returns its dispose
helpers so SSH teardown and app shutdown keep working.
2026-08-19 10:50:52 -05:00
Brooklyn Nicholson 149ef3ed2b refactor(desktop): split lib/chat-messages.ts into concern modules
Types, part builders, tool parts, hydration and reconciliation, behind a
barrel that keeps the @/lib/chat-messages path.

The folder was added without removing chat-messages.ts, so resolution
preferred the file and all its importers kept hitting the monolith while
the new modules sat dead. Deleting it surfaced a missing preset field on
GatewayEventPayload that layout.apply needs, hidden until the folder
actually resolved, and a completeOpenStreamParts helper copied into two
modules when only one calls it.
2026-08-19 10:50:52 -05:00
Brooklyn Nicholson 0553a06728 refactor(desktop): split gateway-event.ts into per-family handler modules
The monolithic if/else-if dispatcher becomes nine modules by event
family. The routing preamble runs once, then each handler consumes its
own types and reports whether it did, so dispatch stops at the first
taker. Families are mutually exclusive by type, so ordering between them
is inert; ordering within a family is unchanged.

Restores two things the extraction dropped against a moving base: the
layout.apply handler, and the multi-question clarify.request path. A
batch clarify was consumed and never parked, so the agent blocked on
clarify.respond with no card rendered — the existing tests passed
because they assert "exactly one clarify card", which is also true when
the request is dropped and only the tool.start row exists.
2026-08-19 10:50:52 -05:00
Brooklyn Nicholson 759d320eeb fix(desktop): keep recents when ALL-profiles scope has one profile
Grouping → Profile persists ALL even with a single profile. Recents
filtered that pool against the __all__ sentinel and emptied the list.
Cron and messaging already used filterSessionsByProfileScope; recents
now does too.

Co-authored-by: andyst-dev <150129844+andyst-dev@users.noreply.github.com>
2026-08-19 10:50:51 -05:00
Brooklyn Nicholson eb52328857 feat(desktop): back window glass with Windows 11 system materials
Glass was macOS-only because it rode setVibrancy. Windows 11 22H2 has a
first-party equivalent in setBackgroundMaterial, so the mode now resolves
its backing per platform instead of per-OS-check: macOS keeps vibrancy,
Windows 11 gets DWM acrylic / tabbed / mica, and everything older stays on
Clear. No third-party native addon.

Two Windows-specific details the mapping has to respect. DWM only paints
the client area of a transparent window (electron#49443), so glass-capable
Windows chat windows are born transparent with the opaque themed
backgroundColor covering them while glass is off — a live Clear/Glass
toggle then needs no window recreate. And Windows exposes three backdrops
for four frost rungs, so the two heaviest both resolve to mica; the mapping
stays total so a frost saved on a Mac still renders.

Glass support is computed once from os.release() and shared: main uses it
for the persisted default and every window, preload publishes it to the
renderer so the UI can't offer a mode the window can't back.
2026-08-19 10:50:51 -05:00
Brooklyn Nicholson aa20dbe73e refactor(desktop): split src/hermes.ts into src/api/ domain modules
2,248 lines of gateway REST client become twelve modules by domain, with
hermes.ts left as a barrel so all 144 importers stay put. The import
graph is a star — every domain module imports only ./client, and client
imports nothing back — so there are no cycles.

The barrel names client's public exports rather than re-exporting it
wholesale. Splitting a module forces its private helpers into exports so
siblings can reach them, and export * would then republish them:
profileScoped, connectionScoped and capabilityScoped were private to
hermes.ts and have to stay that way, or a call site can assemble its own
request scope and drift from the api layer.
2026-08-19 10:50:51 -05:00
Brooklyn Nicholson d959ba5601 fix(desktop): scope session lookup to the active profile
Unscoped getSession hits the primary backend. A 404 then skipped the
active profile in the remaining probes, so chats on a non-default
profile never loaded.

Co-authored-by: Michael McAllister <michael@empowerlo.com>
2026-08-19 10:50:44 -05:00
dcdexhome 0599b66de7 fix(desktop): stop re-entrant pin-sync reconcile from overflowing nanostores
The Desktop renderer crashes with `RangeError: Invalid array length` thrown
from `Array.push` inside nanostores' `notify()`. The shared `listenerQueue`
grows without bound because `reconcile()` is subscribed to BOTH `$sessions`
and `$pinnedSessionIds`, and `pullRemotePins()` mutates `$pinnedSessionIds`
(via `pinSession`/`unpinSession`), which fires `reconcile()` again
synchronously.

The existing `mirrored`/`pending`/`unconfirmed` fences only cover a *bounded*
single-toggle echo. They do not cover the *unbounded* oscillation that occurs
when two profiles share a session id with conflicting `pinned` flags (copied or
imported profile databases). A profile-blind pull then pins and unpins the same
durable id in one pass, re-firing `reconcile` forever until the queue overflows
and the renderer dies.

Two changes:

1. `rowsByPinId()` collapses the cross-profile session list to one
   authoritative row per durable pin id, preferring the active gateway's
   profile (the same tie-break `resolveLoadedRow` uses). `pullRemotePins()`
   iterates the deduped rows, so a conflicting duplicate can no longer pin then
   unpin the same id in a single pass.

2. A re-entrancy guard on `reconcile()` so a synchronous re-entry (from the
   `$pinnedSessionIds` listener firing during `pullRemotePins`) returns
   immediately instead of recursing.

Also fixes a latent TDZ `ReferenceError` in `session-unread.ts`: `isPlainRecord`
was declared after its first use through `persistentAtom`, so decoding a
persisted value could throw `Cannot access 'isPlainRecord' before
initialization`.

Regression tests cover the duplicate-id oscillation and the active-profile
tie-break.
2026-08-19 10:45:27 -04:00
Sara Burke 723dbda041 Merge upstream main into fix/remote-bot-routing
Preserve current upstream Desktop and Bot Mode behavior while carrying the
connection-bound remote Bot routing implementation forward without rewriting
the signed feature commit.
2026-08-19 09:58:05 -04:00
xxxigm ab6e7b93da test(desktop): keep long toast titles readable
Cover the wrap override and height cap so a one-line clamp cannot hide the rest of an error toast again.
2026-08-19 20:10:18 +07:00
xxxigm ca20ee90bb fix(desktop): wrap toast titles so long errors stay readable
AlertTitle clamps to one line, so Desktop error toasts hide the rest of the message behind an ellipsis. Override that clamp, let the title wrap, and cap height so a huge error scrolls instead of covering the chat.
2026-08-19 20:10:18 +07:00
Sara Burke 9b7ab9d65a feat(desktop): route remote bot actions by connection
Carry immutable connection and profile ownership through Bot Mode actions,
session hydration, transcript loading, new chats, and profile deletion.
Keep same-named local and remote profiles isolated, preserve the full context
menu, and migrate Bot metadata to connection-qualified storage.

Add adversarial routing, alias, migration, cache, and compatibility coverage.
2026-08-19 08:01:29 -04:00
hermes-seaeye[bot] 13ce0c5c67 fmt(js): npm run fix on merge (#89914)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-19 09:57:27 +00:00
Teknium 1a19fedb5a fix(desktop): Bot Mode chats no longer paint blank when switching between running bots
resolveStoredSession never probed the ACTIVE profile: the unscoped
/api/sessions GET routes to the PRIMARY backend (not the active
gateway's), and the cross-profile probe loop explicitly skipped the
active key. A hidden Bot Mode canonical chat — never present in the
sidebar cache — owned by the focused bot therefore resolved to
undefined on every switch. The transcript prefetch then went unscoped
to the primary backend, 404'd, and the thread painted empty until the
user opened the session explicitly via right-click → Sessions (which
seeds the cache with a profile-stamped row).

Probe the active profile first in the by-id ladder, so hidden and
uncached sessions on the focused profile resolve with ownership and the
prefetch routes to the owning backend.

Live-repro'd headless via CDP with 3 bot profiles mid-turn: before the
fix every focus-switch painted a blank thread ('Waking up <bot>…');
after, the full transcript paints. Reported by @tbkbossswaglord.
2026-08-19 02:47:07 -07:00
Teknium ac3d7ddae7 fix(desktop): skill hub installs on non-default profiles no longer 404, and failed installs surface
Three fixes for the "Install on this agent" pipeline, covering the whole
split-brain class between action-spawning endpoints and their status polls:

1. electron/connection-config.ts — the /api/actions/{name}/status poll family
   now routes to the same backend as every action-spawning route. Before,
   POST /api/skills/hub/install ran on the PRIMARY backend (scoped route)
   while the follow-up status poll for a non-default profile routed to the
   profile's POOLED backend, which never registered the dynamic action name
   (skills-install-<slug>-<hash> lives only in the spawning process's
   memory) -> 404 "Unknown action" toast even though the install succeeded.
   POST /api/mcp/catalog/install joins the scoped table for the same reason.

2. src/store/hub-actions.ts — a non-zero subprocess exit now rejects with the
   action log tail so the caller's catch toasts it. Before, a failed install
   (scan gate, network, bad identifier) stopped silently: no toast, no row
   flip, and the unchanged skills list read as "install did nothing".

3. src/contrib/runtime-loader.ts — a disk plugin copy shadowed by a bundled
   twin now publishes a visible "(stale disk copy)" inventory row carrying
   the folder path, instead of a console.info nobody sees. Stale
   desktop-plugins/ leftovers from dev deploys are the same folders that
   actively break the feature on shells without the bundled twin.
2026-08-19 02:42:34 -07:00
royzhrxy-glitch a787bfeab9 fix(desktop): restore profile/agent switching in release builds (nanostores 1.4.2)
nanostores 1.4.0-1.4.1 annotate batch() @__NO_SIDE_EFFECTS__. Rollup
(via vite build) honors that and erases a result-unused batch(...) call
as dead code -- callback included. Since d57f94a33/053eb7aab/4e520f085
moved the gateway-switch publication (activate() +
+ ) inside batch(), packaged desktop builds lost the entire
publication: clicking a profile in the rail did nothing at all.

Dev builds and vitest run unminified, so only the packaged app broke.

nanostores 1.4.2 removes the annotation from batch() (it stays on the
creation functions, where it is correct). Bump all three pinned copies
(apps/desktop, apps/bootstrap-installer, ui-tui) and add a regression
test asserting the installed nanostores never re-annotates batch.
2026-08-19 01:58:21 -07:00
brooklyn! 63565fa26b fix(desktop): keep auto-speak silent across the stream-id rewrite
Supersedes #75649, #86637, #87672, #88642.

Fixes #86601
Fixes #87652
Fixes #87823

Co-authored-by: Charmmy <lilShawtty@qq.com>
Co-authored-by: chelsealong <chelsealong@126.com>
Co-authored-by: Olympusbuildz <Olympus.roots@outlook.com>
Co-authored-by: Ricardo Mendes <ricardo.mendes@maiolabs.ai>
2026-08-19 02:56:55 -05:00
brooklyn! 70e818c6e3 Merge pull request #89814 from NousResearch/bb/session-group-icon-fix
fix(desktop): close the blank hole in the Sessions header
2026-08-19 02:49:01 -05:00
brooklyn! ca4a0c4358 Merge pull request #88233 from helix4u/fix/get-windows-recovery-command
fix(desktop): recover missing get-windows binding
2026-08-19 02:46:05 -05:00
Brooklyn Nicholson 0223710d00 test(desktop): compare get-windows installer path via realpath
require.resolve returns the macOS realpath (/private/var/...) while
os.tmpdir() stays on the /var symlink, so a raw join() deepEqual failed
even though the spawn was correct.
2026-08-19 02:39:44 -05:00
brooklyn! b82f8b0654 Merge pull request #89257 from xxxigm/fix/desktop-files-panel-remote-download
fix(desktop): let Files panel download remote backend files
2026-08-19 02:38:30 -05:00
Teknium 2507bc649f fix(desktop): atomic profile-switch publication v2 — fail open, lease mid-dial entries (#89622, #46651)
Re-lands the goals of the reverted atomic-publish series (#89483, reverted
in #89785) without the fail-closed decline path that killed profile clicks,
and fixes the underlying pruner race the original series exposed.

Two changes relative to the restored (pre-series) behavior:

- Publication is atomic and mode-safe (#46651): the switch resolves the
  target's connection descriptor CONCURRENTLY with the socket work and
  publishes $activeGatewayProfile + $connection in one nanostores batch()
  frame, so no request or plugin mode-listener observes the new gateway
  beside the previous profile's descriptor. Unlike the reverted series, a
  failed descriptor lookup fails OPEN — the switch still lands, the previous
  descriptor stays, boot/reconnect resyncs it later, and the failure is
  logged instead of swallowed.
- The live-work pruner can no longer kill a switch mid-dial (#89622's root
  race): ensureGatewayForProfile / ensureGatewayForAgent lease their entry
  (activationLeaseUntil, 30s bound) for the duration of the dial and
  pruneSecondaryGateways spares leased entries. A switch target is not yet
  active, has no live sessions and holds no request lease, so any prune
  recompute during a cold pool spawn used to dispose the dialing entry.
  Bounded lease: an orphaned one self-expires.

The agent door logs a non-landing activation (target removed mid-dial)
instead of resolving silently, but never fails the switch closed.

Live E2E (Electron over CDP, 3 local profiles, cold + warm): 10 sequential
switches, rapid two-click interleave (last click wins), 6-click stress run —
all land, overlay always clears, fresh pool backends spawn on cold switches.

Tests: gateway-activation-prune-lease suite (mid-dial prune survival on both
doors, lease release, lease expiry); the invalidated-registry-identity pin
updated for the concurrent (read-only) descriptor probe.
2026-08-19 00:03:35 -07:00
Brooklyn Nicholson 0e980e8049 fix(desktop): keep mark-all flush with the sessions header actions
The unread check-all was a sibling of the +/filter cluster. The header
is justify-between, so that extra child sat in the middle as a blank
24px hole until hover.
2026-08-19 01:31:11 -05:00
brooklyn! 38f79e25a0 Merge pull request #89798 from NousResearch/bb/tour-demo-remove
Drop the dev-only tour demo
2026-08-19 01:27:34 -05:00
brooklyn! e4771b92a7 Merge pull request #89808 from NousResearch/bb/turn-activity-timer
Time the gaps a working turn spends producing nothing
2026-08-19 01:25:37 -05:00
Brooklyn Nicholson ff8c4159e8 chore(desktop): drop the dev-only tour demo
The demo existed to iterate on the tour UI without spending an agent turn per
look. That work is done and merged, so it is scaffolding now — a floating
button, a hotkey, a palette row and a window hook, none of which anyone needs
again.

Removing it restores the credit-notice demo's original single-disposer wiring
and drops the Compass icon export, which the demo was the only consumer of.
The tour feature itself is untouched.
2026-08-19 01:20:39 -05:00
Brooklyn Nicholson ccaa4872d8 fix(desktop): time the gaps a working turn spends producing nothing
The transcript's activity row went silent for most of an agentic turn. Two
gates were too narrow. It mounted only while the tail bubble's own status was
`running`, so a turn that seals a bubble mid-flight (message.interim) or
finishes one while the agent keeps going left a settled message at the tail and
unmounted the row entirely. And its activity signature was part count plus text
length, which a landing tool result does not change — a result mutates the part
that was already there — so the pause after a call ended was read as more of
the same silence and dated from whenever the call started.

Between them, the app spent whole stretches with the composer's arc border lit
and Stop armed while the thread showed nothing and counted nothing.

The row now follows the same busy signal the composer does, is mounted by the
tail unconditionally, and decides for itself whether the turn owes the user a
line. A named wait (compaction, provider wait, a tool being drafted) says so
immediately; an unnamed gap earns two seconds of quiet first, so a run of quick
calls doesn't strobe. It still defers to the two waits already accounted for
elsewhere: a prompt the user is answering, and a tool call in flight carrying
its own row and timer — anywhere in the message now, not just the last part,
and excluding the silent tools that render nothing to defer to.

Renamed to TurnActivityIndicator / `aui_turn-activity`: it hasn't measured a
stall since it started measuring the whole turn.
2026-08-19 01:19:33 -05:00
Brooklyn Nicholson 7c3c2d112b refactor(desktop): give each chat surface its own turn clock
`turnStartedAt` was only reachable through the global atom, which mirrors
whichever session is currently viewed — the same trap `$busy` and `$messages`
were moved out of. Anything in a tile that measures a turn was therefore
timing the primary chat's.

Put it on SessionView beside the other per-surface signals: the primary falls
back to the draft atom while a new chat has no slice yet, a tile reads its own.
2026-08-19 01:19:21 -05:00