Commit Graph

22221 Commits

Author SHA1 Message Date
Teknium 212e84176d fix(compression): charge stale thinking to the tail budget only on the newest assistant turn (#73624)
Generic thinking fields (reasoning / reasoning_content + the
reasoning_details text charge) are replayed for at most the NEWEST
assistant turn on every transport: Anthropic strips all-but-newest at
convert time, Bedrock Converse never replays thinking, and strict
chat-completions providers reject or one-space-pad the field. The tail
budget walks charged them on every message anyway, spending 19-24% of
the budget (per the issue's 1,025-message measurement) on bytes that
provably never reach the wire — so the tail cut landed early and each
compaction discarded more real transcript than configured.

_estimate_msg_budget_tokens now partitions the replay keys:

* _ALWAYS_REPLAYED_BUDGET_KEYS (codex_reasoning_items,
  codex_message_items) — charged unconditionally. These ride the wire
  on every retained turn (#55572), and codex_reasoning_items now also
  carries native server-side compaction checkpoints (#81747).
* _NEWEST_TURN_ONLY_BUDGET_KEYS (reasoning, reasoning_content) + the
  reasoning_details text charge — charged only for the newest assistant
  turn via charge_stale_thinking, resolved by the three budget walks
  (tail cut, raw-budget re-walk, proactive-prune boundary).

Default stays the conservative full charge for callers without
turn-position context. A partition invariant test pins that any future
_REPLAY_BUDGET_KEYS entry must be classified into exactly one class.

Direction credit: #73669 (@x7peeps) and #73730 (@webtecnica) both
attacked this; the keep_open reviews asked for provider/API-mode-aware
accounting that keeps Codex carriers charged — this implements that
shape.
2026-08-08 17:42:34 -07:00
brooklyn! 5f62b6c1ab
Merge pull request #82113 from NousResearch/bb/hud-unsticky
fix(desktop): keep tool rows and notices out of the HUD band
2026-08-08 19:39:55 -05:00
cryptoyasenka f2d03c1f2a fix(state,cli,tui-gateway): keep reasoning fields intact across forks and branches
get_messages() only deserializes content and tool_calls; the structured
reasoning columns (reasoning_details, codex_reasoning_items,
codex_message_items) come back as the raw TEXT they were stored as.
Feeding those rows straight back into a write, which is exactly what
the POST /api/sessions/{id}/fork handler does by piping get_messages()
into replace_messages(), hit an unguarded json.dumps() and stored the
already-serialized string encoded a second time. On replay of the fork,
json.loads() then yields the inner string instead of a list, and every
consumer's isinstance(..., list) gate silently drops it: preserved
Anthropic thinking blocks, Codex encrypted-reasoning/message-item
replay, and OpenRouter multi-turn reasoning context are all lost after
a fork, with one more encoding layer added per fork.

The /branch copy loop had the same defect from the other side: it
forwarded reasoning but none of the structured columns, and both TUI
branch writers persisted role/content alone, dropping reasoning and
reasoning_content along with them.

Route the six dumps sites in append_message and _insert_message_rows
through a shared guard that keeps already-serialized strings as-is;
structured values from the live runtime are dumped exactly as before.
Forward the reasoning fields in all three branch writers, matching the
set gateway/slash_commands.py already forwards on its own /branch path.
2026-08-08 17:37:26 -07:00
owner 3a8d95a93b fix(desktop): give the delivery-target group an accessible name
Field renders its visible label as <label htmlFor>, but that only
associates with labelable elements (input, select, ...), not the
div[role="group"] DeliverCheckboxes renders. The checkbox group had
no accessible name for assistive tech.

Field now stamps an id on its label (`${htmlFor}-label`) and
DeliverCheckboxes references it via aria-labelledby, so the group
picks up the same visible label text instead of duplicating it.

Addresses review feedback on PR #73886.
2026-08-08 17:33:22 -07:00
Carbon 67927808be feat(desktop): support multiple cron delivery targets 2026-08-08 17:33:22 -07:00
Brooklyn Nicholson d91f08a39a fix(desktop): keep tool rows and notices out of the HUD band
The band is a few lines of conversation floating over another app, not a
transcript. Tool blocks, file-diff panels, and background notices ("Self-
improvement review: patched ...") pushed the actual answer out of the
capped band and read as junk pinned over the window below.
2026-08-08 19:32:54 -05:00
brooklyn! d408fdbfc4
Merge pull request #82104 from NousResearch/bb/hud-note-trail
fix(desktop): keep earlier HUD windows in scope for the turn
2026-08-08 19:20:41 -05:00
brooklyn! 37bb73d2cf
Merge pull request #82101 from NousResearch/bb/hud-slash-popover
fix(desktop): make the slash list usable in HUD mode
2026-08-08 19:20:20 -05:00
Brooklyn Nicholson 86b50c6a29 fix(desktop): keep earlier HUD windows in scope for the turn
The HUD-mode note tells the model that an unqualified "this" means the app
behind the strip. It says nothing about the app that was behind it a minute
ago, and the user drags the strip from app to app mid-thought: parked over
Spotify, "pause that and play X here" is one request spanning two apps, and
only the second half has a window under it.

Those earlier windows are already in context as read_window_below results, so
the note only has to say they still count. Without it the latest window reads
as the only one and half the request is silently dropped.

No new tool names, so the existing gating tests cover it unchanged.
2026-08-08 19:17:30 -05:00
teknium1 9e19931770 docs(telegram): explain rich draft final delivery
Document the actual transport split: legacy editable drafts by default, optional rich drafts, persistent rich final sends, and in-place rich final edits for edit-based streams.
2026-08-08 17:15:45 -07:00
Brooklyn Nicholson 05c5a337f6 fix(desktop): settle the HUD band's glanceable hold at 1.1s
Anything longer reads as the band waiting for something. Also drops the note
claiming a sub-second hold disappears into the fade — that was written when
losing window focus never started the hold at all, so what looked like too
short a stage was no stage.
2026-08-08 19:12:27 -05:00
hermes-seaeye[bot] 99d1f18377
fmt(js): `npm run fix` on merge (#82099)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-09 00:11:46 +00:00
Brooklyn Nicholson 1040cfe286 fix(desktop): shorten the HUD band's glanceable hold to 1.75s
2.5s was long enough to feel like the band was waiting for something.
2026-08-08 19:10:35 -05:00
Brooklyn Nicholson 3d5f173440 feat(desktop): lift the HUD bar off what it lies over
A hairline shadow under the bar's bottom edge. Without it the HUD reads as
pasted onto the other app rather than floating above it; kept tight and faint
so it never becomes a glow around a card.
2026-08-08 19:10:35 -05:00
Brooklyn Nicholson 051a7fb1e2 fix(desktop): open the HUD completion list where there is room for it
The composer's completion list — `/`, `@`, `:`, and the help hint — hangs off
the top of the bar. That is right everywhere the composer has a window above
it, and wrong in the one place the bar is parked against the screen's top edge:
the list rendered off screen, so `/` looked like it did nothing at all.

Flip it below the bar in that orientation and cap it to the room it actually
has, since the app's own cap assumes a full window.
2026-08-08 19:08:46 -05:00
Brooklyn Nicholson 5ff506896f feat(desktop): sit the HUD band back when a completion list opens
The list hangs over the band, and both were half-lit over a third thing — the
app underneath. No pair of opacities reads well in that stack.

So the list goes fully opaque and the band falls back behind it: dimmer,
fractionally smaller, slightly out of focus, scaled from the bar's edge so it
reads as depth rather than as the panel shrinking.
2026-08-08 19:08:45 -05:00
Teknium 5a16635f40 feat(cli): show session titles in status bars 2026-08-08 17:02:55 -07:00
Teknium 05330e804a fix(video): bind managed SeedVR to source request 2026-08-08 17:01:59 -07:00
teknium1 70d165222c fix(gateway): cover all finalized stream sends
Apply the same metadata invariant to sealed split chunks, keep expect_edits on live previews, and exercise the real Telegram adapter path with rich messages enabled but rich drafts disabled.\n\nCredits PR #78525 by @Slobaka for the reproduced rich_messages/rich_drafts combination.
2026-08-08 16:59:42 -07:00
liuhao1024 0f22727167 fix(gateway): omit expect_edits on finalized draft sends
A finalized native draft is the first persistent send and will not be edited again. Omitting expect_edits lets Telegram use sendRichMessage for the persistent final instead of degrading tables through MarkdownV2.\n\nAdapted from PR #46536.
2026-08-08 16:59:42 -07:00
rob-maron 7065407411
Add more FAL models to nous portal (#82019)
* add more FAL models to nous portal

* fix test

* minor fixes

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 19:59:12 -04:00
brooklyn! 34f3135879
Merge pull request #82077 from NousResearch/bb/hud-drag-and-chrome
fix(desktop): make the HUD grabbable, and stop it painting over dead space
2026-08-08 18:58:10 -05:00
Brooklyn Nicholson d2dc0ce707 fix(desktop): hold the HUD band when the window loses focus
Clicking away to another app is the commonest way the HUD gets let go of, and
it fires no focusout — the composer stays document.activeElement while the
window is inactive. Chrome stops matching `:focus` on an unfocused window all
the same, so the band lost its focus state with no hold running and snapped
shut instead of stepping down to the glanceable stage.

The sheet also goes heavier than the text in front of it. There is no blur to
separate the band from what it lies over, so it is the only thing keeping
half-opacity text off someone else's UI.
2026-08-08 18:49:19 -05:00
Brooklyn Nicholson bed3960e9d fix(desktop): give the HUD band a real glanceable stage before it goes
The middle state — half-opacity text over the tinted sheet, after a turn lands
or after you click away from the composer — held for 700ms, which reads as part
of the fade rather than as a state you can still finish reading in. 2.5s.

Clicking away always buys the full window, streaming output still does not, so
an unfocused HUD fades on the same clock whether or not the agent is working.
2026-08-08 18:44:11 -05:00
Brooklyn Nicholson c5494ddddb chore(desktop): satisfy eslint on the HUD drag additions
Prop ordering on the composer root, and a disable directive for a rule the
effect no longer trips.
2026-08-08 18:44:11 -05:00
Brooklyn Nicholson eee38db824 fix(desktop): let the HUD band fade while a turn is still running
Two things pinned the transcript open for the length of a reply, so
unfocusing the HUD left it sitting open across the screen for as long as the
agent worked — the state the fade exists to avoid.

A busy session no longer counts as held. Held is for a question the agent
cannot continue without (clarify, approval, sudo, secret), where fading hands
you a prompt that is neither readable nor clickable; watching a turn write
itself is what focus is for, and the answer landing flashes the band anyway.

And activity only re-arms the hold while the composer has focus. $messages
republishes ~30×/s mid-stream, so an unfocused HUD was being held open by its
own streaming output.
2026-08-08 18:39:52 -05:00
Brooklyn Nicholson 7e3deea0a8 fix(desktop): make the HUD band a real box so it scrolls
The band ran the full window and was clipped down for paint and hit-testing,
which was fine while its height was whatever the transcript measured. Capping
it broke that: the scroll container was still window-tall, so content shorter
than the window never overflowed and never scrolled, while the clip hid
everything past the cap. Half the transcript was unreachable.

Give the band the geometry it was only pretending to have — anchored to the
bar, as tall as --hud-band-height, inset at the sides — and drop the clip-path
along with the bar-height clearance that only existed to hold text out from
under a composer the box no longer runs beneath.
2026-08-08 18:36:56 -05:00
Brooklyn Nicholson 048907fb66 feat(desktop): show work and focus on the HUD bar itself
In HUD mode the bar is often the only thing on screen, so the states that
matter have to live on it.

Working gets the travelling arc the sidebar's active session already uses, at
2px and pill-rounded to match. Focus recolours the border the bar already
draws: the bar sits flush against the window, so a ring or a shadow is sawn off
by the window before any CSS can shape it, and buying the clearance moves the
bar.

The exit control moves off the band and into the open space above the composer,
right-aligned, appearing on focus only — anchored to the band it drifted to
wherever the transcript happened to end, and a turn landing is not a reason to
offer the window controls. Placed from --hud-bar-height rather than CSS
anchor(), because Lightning CSS drops an entire rule containing an `anchor()`
on the vertical axis: the flipped-edge override never reached the browser and
the control rendered off screen. In that orientation the bar hugs the window's
top edge, so the strip it sits in is reserved as dock padding.
2026-08-08 18:31:57 -05:00
Brooklyn Nicholson 4424a5a60f fix(desktop): stop the HUD painting sheet over empty space
Four separate ways the band claimed room it had nothing to put in:

The height was measured to the viewport's edge, and the scroll container is
`min-height: 100%`, so the whole window counted as transcript. Measure the
message rows instead, and treat zero-height rows as no transcript at all — a
fresh thread still renders scaffolding in the content box, which was enough to
buy the 12px overhang and leave a sliver of sheet hanging under the bar.

The band was uncapped, so a long thread turned a glance-over-your-work strip
into a second window. It now tops out at the smaller of 9.5rem and 42% of the
HUD.

The frost is native vibrancy — the OS content view, which fills the window
rectangle and cannot be clipped to the sheet from the page — so it is only ever
right when the sheet covers the window. With the band capped that is now
essentially never, and anything looser paints a grey slab across the whole HUD.

The composer's drop target is a full-window dashed sheet sized for the app's
chat column; in a bar there is nowhere to drop anything anyway.

Also insets the band 0.5rem each side so the bar's corner controls sit clear of
the sheet's edge rather than on top of it.
2026-08-08 18:31:52 -05:00
Brooklyn Nicholson dd11b8e8e8 fix(desktop): drag the HUD by holding the composer
`-webkit-app-region: drag` and `useHudClickThrough` cannot share a window, and
every previous attempt at a HUD grab handle had them fighting. The window
manager takes a draggable region's mouse input whole, so the page never sees
the cursor arrive on the handle — and click-through, which decides whether the
window is solid from exactly those moves, has already handed the window to the
desktop by the time you press. The handle was unusable (the press fell through
to the app behind) and, having eaten the moves on the way out, it also left the
HUD solid over its own dead space so clicks meant for the app behind died in
empty window.

So no HUD surface declares a drag region any more, and dragging is a press and
hold on the bar: 140ms to arm, then the renderer moves the window through a new
`hud.moveBy`. Deltas are read in screen coordinates, because client coordinates
are relative to the window being moved and report zero once it keeps up with
the cursor. The pointer is captured and the window pinned solid for the
duration, so a fast drag cannot outrun the bar.

Removes the 2rem invisible drag strip along with it — dead window that only
ever swallowed clicks aimed at whatever was behind the HUD.
2026-08-08 18:31:37 -05:00
Brooklyn Nicholson 0db2788338 fix(desktop): keep the composer focused while reading the HUD band
The band is there to be read over another app, so clicking a line in it is not
leaving the composer. Mousedown on the scrollback blurred the input, which
faded the band and dropped the focus treatment mid-read.
2026-08-08 18:31:29 -05:00
Brooklyn Nicholson 50e0640d1a fix(desktop): drop the "Edit message" tooltip from user bubbles
The bubble IS the edit button, so the native title popped up on hovering any
user message anywhere in the app. The aria-label stays, so the control keeps
its accessible name.
2026-08-08 18:31:29 -05:00
Brooklyn Nicholson 67518a2adf fix(desktop): repaint peer windows when the appearance changes
Skin and mode are per-profile localStorage, and every desktop window is a
separate renderer on the same origin that reads them once at boot. Changing
the theme in the HUD therefore repainted the HUD alone; the app window still
held its startup value and reverted the moment you looked at it.

Listen for `storage`, which fires in the OTHER windows of an origin — exactly
the set that needs to catch up.
2026-08-08 18:31:25 -05:00
hermes-seaeye[bot] 3e6a081d60
fmt(js): `npm run fix` on merge (#82055)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-08 22:51:53 +00:00
teknium1 55982159dd feat(tests): CI-enforce skill authoring standards; clear all remaining debt
New tests/skills/test_authoring_standards.py parametrizes every bundled +
optional SKILL.md (1148 checks) against the mechanically-verifiable subset
of the hardline standards:
- required frontmatter fields (name/description/version/author/license/
  platforms) + tags
- frontmatter name == directory name
- description <= 60 chars, ends with period, no marketing words
- related_skills resolve in-repo
- no machine-local paths
- <= 100k chars
Grandfather dict for legacy debt ships EMPTY — all pre-existing violations
fixed in this PR:

- 13 frontmatter names canonicalized to their directory names (the install
  identifier); all related_skills references updated (comfyui -> stable-
  diffusion). Fixes the class behind PR #42788's report; also fixes
  here.now's invalid dot-name.
- optional-skills/devops/cli -> inference-sh-cli (dir was the generic
  'cli'; fm name was right) incl. docs pages (en + zh-Hans), catalog row,
  sidebar entry.
- pytorch-fsdp: 157k generated 'Quick Reference' dump moved to
  references/common-patterns.md; SKILL.md 159k -> 2.5k with a pointer.
- research-paper-writing: 31.7k Phase 5 drafting section moved to
  references/phase5-paper-drafting.md; SKILL.md 103k -> 71k.

Docs regenerated with scope discipline.
2026-08-08 15:43:00 -07:00
brooklyn! 3898e646e5
Merge pull request #82044 from NousResearch/bb/agent-plugins
Surface agent plugins in the desktop app's Settings → Plugins
2026-08-08 17:41:38 -05:00
Brooklyn Nicholson 44790bc9c8 feat(desktop): plugin descriptions + open the agent plugins folder
HermesPlugin/PluginRecord gain a description one-liner (kanban gets
one) shown in the inventory instead of the raw file path, and the
agent plugins section can open the backend's plugins dir — path from
config.get profile so it's profile-aware, local backends only since
openDir mkdir-creates.
2026-08-08 17:34:20 -05:00
Brooklyn Nicholson ed9eee5dc9 fix(gateway): report bundled auto-loading plugins as enabled
Bundled backends/platforms/providers load without a plugins.enabled
entry ('must just work'), but plugins.manage reported them 'not
enabled' — clients rendered running plugins with an OFF switch.
Surface the truthful default; explicit disable still wins.
2026-08-08 17:34:20 -05:00
teknium1 1c9433897c chore(skills): standards sweep — bring 42 bundled/optional skills up to hardline
Audited all 191 in-repo skills (77 bundled, 114 optional) against the
authoring standards (AGENTS.md hardline + PR #80800). Fixed 42:

- 8 overlong descriptions rewritten to <= 60 chars, one sentence, period
  (one-three-one-rule 525ch, drug-discovery 405ch, web-pentest 358ch,
  fitness-nutrition 352ch, neuroskill-bci 332ch, oss-forensics 320ch,
  computer-use 307ch, memento-flashcards 252ch)
- 24 skills with missing frontmatter fields: author (credited from git
  history: f-trycua, SHL0MS, teyrebaz33, haileymarshall, FurkanL0,
  teknium1), license, version, platforms, tags
- 7 machine-local paths scrubbed (/home/bb, /home/user, /home/ubuntu ->
  portable placeholders)
- 11 marketing-word intros reworded (Comprehensive/state-of-the-art)
- docs catalogs + per-skill pages regenerated, scope-disciplined

Deferred (not in this PR):
- 14 frontmatter/dir name mismatches — open PR #42788 already proposes
  the dir-rename approach for 4 of them; resolve there as one class
- 2 skills over 100k chars (pytorch-fsdp 159k, research-paper-writing
  103k) — need content splits into references/, separate PRs
- comfyui dangling related_skill resolves after the name-mismatch class
2026-08-08 15:27:29 -07:00
Brooklyn Nicholson c86da8397b feat(desktop): agent plugins in Settings → Plugins
Backend plugins — native Hermes plugins and portable Agent Plugins v1
packages — were invisible in the desktop app. Settings → Plugins now
lists them under the desktop (renderer) plugins with source/portable
pills, enable/disable switches keyed by canonical registry key, and a
live-filter search box, backed by a nanostore over plugins.manage.
Categories other surfaces own (dashboard_auth/*, model-providers/*,
platforms/*) are curated out renderer-side.
2026-08-08 17:24:59 -05:00
Brooklyn Nicholson a60b492e07 feat(gateway): key-addressed plugins.manage rows + portable MCP toolset fold-in
plugins.manage list rows now carry the canonical registry key and a
portable flag (Agent Plugins v1 plugin.json packages), and toggles
address the key — bare names collide across category dirs
(image_gen/fal vs video_gen/fal), so name-addressed toggles flipped
both. Portable packages' in-memory MCP servers also fold into
enabled_mcp_server_names(); without that their tools registered with
the MCP runtime but never reached the model's schema.
2026-08-08 17:24:59 -05:00
brooklyn! fe42097865
Merge pull request #81985 from NousResearch/bb/session-titles
Name sessions instantly from the opening message, and make the name stick
2026-08-08 17:17:09 -05:00
Brooklyn Nicholson f726090d48 feat(sessions): name a session the moment it starts
Titling fired on the first response, so a session sat unnamed for the whole
opening turn - p50 151s, p90 1212s across real sessions, because a turn is
tool calls, not one round-trip. A turn that failed or was interrupted never
got a title at all. Four surfaces each carried their own copy of the call.

Move it into the shared turn prologue and split it in two: a deterministic
title derived from the user's opening message, written inline before the
model runs, then one small-model call that upgrades it. The response is
constrained to a JSON object so there is no preamble to strip, and control
wrappers are stripped rather than refused, so a slash command titles as
what the user asked for instead of the command itself.
2026-08-08 17:07:21 -05:00
Brooklyn Nicholson e358eaf44a perf(sessions): resolve the titling model from the provider's live catalog
Titling ran on the user's main chat model, so a five-word title was billed
to a frontier reasoning model and inherited its latency. Pinning a cheap
model id instead just moves the problem: the hardcoded default was already
dead upstream and every call paid a 404 before the retry net caught it.

Match model FAMILIES against the provider's live /v1/models catalog,
preferring rolling '-latest' aliases where a provider publishes them, and
order the families by measured latency. Nothing to bump when a provider
ships a new mini/flash/haiku. Opt-in per task, so compression, vision, and
search keep 'auto means my chat model'.
2026-08-08 17:07:21 -05:00
Brooklyn Nicholson 5566379f57 fix(sessions): give titles provenance so they stop overwriting themselves
A session title had no notion of who set it, so two bugs followed. An
auto-generated title could clobber a name the user typed, and every
compression rotation renumbered the conversation it forked - one piece of
work reaching 'Smallville Map Architecture Plan #10' in the sidebar.

Titles now carry a source (derived < llm < user) enforced by one
compare-and-swap, so an automatic write can only ever replace a title of
strictly lower authority. Compression carries the name across unchanged.
Legacy NULL rows rank as user, so auto-titling only fills genuinely
empty titles on existing data.
2026-08-08 17:07:21 -05:00
brooklyn! 3391836888
Merge pull request #82036 from NousResearch/bb/session-arc-source
perf(desktop): keep session status inside the row's own fiber
2026-08-08 16:58:34 -05:00
brooklyn! 4907e3d900
Merge pull request #81977 from helix4u/fix/desktop-main-window-ready-fallback
fix(desktop): reveal windows after a missed ready-to-show event
2026-08-08 16:58:00 -05:00
Brooklyn Nicholson 41ae3db426 fix(desktop): reveal every window after a missed ready event, not just the main one
Session, instance, HUD, quick-entry and pet-overlay windows all open with
show: false and are revealed only by ready-to-show, so the Electron 40 bug
strands them exactly the way it stranded the primary window — and none of
them have the second-launch workaround that made the main-window case
recoverable.

Generalize the controller to any window and wire all six through one
wireWindowReveal helper. Callers pass their own reveal action (showInactive
for the pet overlay, show + focus for the HUD and quick entry) and their own
post-visible work, so whichever path wins runs them exactly once.

Quick entry now reveals the window the call created rather than whatever
`quickEntryWindow` points at when the event lands.
2026-08-08 16:51:44 -05:00
Brooklyn Nicholson 041cbff5e3 fix(desktop): keep the Playwright reveal path off the production fallback
Desktop E2E is hard-disabled in ci.yml (#76627) because the mock-backend
window never reaches a usable state, so nothing can validate dropping the
TEST_WORKER_INDEX force-show right now — and the suite's lead symptom is
already a window-readiness failure. Restore it, routed through the reveal
controller so the bookkeeping in onRevealed still runs exactly once, and
leave the removal to whoever re-enables the suite.
2026-08-08 16:51:44 -05:00
Brooklyn Nicholson 615a435b7a perf(desktop): stop a settling turn from repainting the whole sidebar
ChatSidebar read $workingSessionIds with useStore purely to notice that a turn
had finished and re-probe worktree lanes. Nothing in its markup used the value,
so every status edge re-rendered the entire sidebar — each section, each row —
to run an effect that touches no DOM.

Listen to the store instead. The rows own their status subscription, so a
session changing color repaints that row's fiber and nothing above it, which a
test now holds in place by counting row renders.
2026-08-08 16:51:25 -05:00