- compareVersions had zero production callers (the gate rides
meetsBaseline alone) and is not re-exported through the package entry;
its spec block duplicated what meetsBaseline's cases already pin
through compareParsed (a major-gap case joins meetsBaseline's block to
keep every arm covered). isNewerVersion keeps its own numeric-only
compare: ignoring prereleases there is the upgrade nudge's deliberate
semantics, and adopting the channel-aware order would change
user-visible behavior.
- pad2 hand-rolled padStart(2, '0') in a function that already used
padStart for the year.
- StoredHeaderTool/StoredHeaderRecord and DecodeCounts/DecodeSpans lost
export keywords nothing inside or outside the repo imported.
- The ConnectionHostFace interface and its bind-at-extraction guard were
copy-pasted across detail/backfill/balance; one host/connection.ts
module now owns the face and fetchRouteRegistrar.
- accumulateCost (timeline fold) and pricedDayOf (activity ledger) were
two hand-written copies of the same clone-along-path cost accumulation,
period rule included; both now call the exported addBilledUsage.
- The cost zod schemas were defined field-for-field in activity.ts and
timeline.ts; activity imports costUsageSchema from the wire owner.
Rejected from the audit after inspection: sharing firstText/firstFullText
(the walks differ in the acceptance predicate — trim-nonempty vs
nonempty — and unifying would change skill-name matching on a
whitespace-only first block of untrusted log data), and unifying the two
host asRecord guards (a one-line stdlib idiom; cross-module imports for
five lines is churn, not simplification).
makeDetailFetcher's read switched to a plain fetch in a82e6a1, leaving a
lone void-statement as the ctx parameter's only use — yet ctx kept being
threaded through detailStoreOf, useTimelineSource, and makeAgentHeads.
All four drop the parameter; the spec's ctxWithCall factory (which only
ever stubbed global fetch and returned a ctx nobody read) collapses to
a plain stubDetailFetch.
- timelineOf's slow path and detailOf carried a byte-identical
requests/events/nodes/droppedNodes/archive/floors/fileOps/spans block;
both now spread one collectionsOf(data) at the same boundary rigor.
- sessionsSnapshotOf/workspacesSnapshotOf were the same guarded-hook read
differing only in the prop key; both delegate to standardSnapshotOf.
Rejected from the audit after inspection: merging billedTokensOf into
usageTotalsOf (their input contracts differ — the former's per-level
re-proving against hostile wire data is pinned by cost.spec and the
compatibility contract), and a shared own() disposer helper (the three
sites have two different shapes; the guard is a one-token idiom).
- BlocksBody's richable prop was declared and passed at all four call
sites but never read.
- RawSection was a single-use Section wrapper; inlined at its one call.
- makeSliceList/makeDonut took a kit they immediately voided; the
factories and all call sites (specs included) drop the parameter.
- seqOfMessageId/flowAnchor lost export keywords nothing imported.
- EVENT_ICONS shed the inject/model entries the icon short-circuit
renders unreachable.
- a.lc-stat(:hover): orphaned since the anchor cost cell was rebuilt as
the Agent Team ledger (3dc892c); every lc-stat is now a div.
- .lc-rich-seg*: byte-identical re-declaration of the shared .lc-gran*
segmented control; RichSwitch now wears the lc-gran tokens, and the
browser spec scopes its queries to the detail body accordingly.
- lc-col-stats: a class token no rule, query, or script ever read.
The ported AttachmentLightbox predates the discovery that the shell seeds
ALL of ui-primitives into the browser module table: ImageLightbox is
exported on both supported lines (0.1.7-rc.2, 0.2.0-rc.2) with the exact
props the call site already builds. Deleting the port also drops the
lc-att-lightbox sheet; the spec keeps only the plugin-owned open/close
wiring (dialog behavior is covered upstream), and upstream adds Escape
stopPropagation plus a Tab trap.
The supported set is now the two V4-generation lines (0.1.7-rc.2+ and
0.2.0-rc.2+): the manifest's dshReleases, the peer floors (>=0.1.7-rc.2,
cordis ^4.0.4 — both lines vendor 4.0.4), the devDependency pins, the
baseline matrix, the release workflow's tag fetch, and the compatibility
docs.
With every supported line on session format V4, the V3-only paths leave:
the fold reads the tool result's first-class message alone (source.callId
pairing, message.isError — the admission validation refuses any row whose
data.error outlives it); the settings.register host face module is gone
(the Config-form entry schema serves the preferences); the client drops
the settingsScope/settings.plugin.item card seat, the retired sessions.open
jump fallback, the legacy icon spellings, and the settings-dialog leg of
the settings jump; the history page's tool/result mapper reads the V4
message (its V3 wrapper reads were dropping result content on supported
lines); the cache probe rides the 2-arg cachedSnapshot face. The baseline
gate stays: a below-floor harness composes the fallback units and the
upgrade prompt, and an undetectable version still fails open.
Verified: pnpm run lint:fix && pnpm run test && pnpm run build green (1918
tests, 100% per-file coverage); the compat matrix passes against the real
dsh sources at both baseline tags; live on dsh 0.2.0-rc.2 the plugin
composes, the Plugins-page preference card renders and round-trips a
write, the insights warm-up fills the heatmap, and the session jump and
Context tab work end to end.
The mask was inset on the left sidebar alone, so with the right
Sidebar open the dialog centered over the main column plus the right
panel and its right half disappeared underneath. measureDock now
parses the frame template's trailing px track too, clamped to the
space the centre track's declared minimum leaves: on the minmax
generations the trailing bound is only the right panel's bid, not the
solved width — the frame lets the grid squeeze the right track before
the centre drops below its minimum (measured live: template
minmax(0px, 495px), solved track 420px at a 1100px viewport), so an
unclamped parse over-insets the mask in exactly that squeeze regime.
The V3 line's fixed trailing track and every degrade-to-full-mask
branch are unchanged.
The card width now resolves against the backdrop (100% instead of
100vw), so when both sidebars bound the mask the card clamps inside
the main column instead of poking underneath either edge. Verified
live at 1280/1100/900/390 px with the right panel open, closed, and
across its track removal at phone width.
Overflowing style alone is not enough: on layouts where an outer
container scrolls (the web app's conversation scroller), the Context
tab's own .lc-root reports overflow-y auto yet grows with its content
and never moves — taking it short-circuited the walk into a silent
no-op, so neither the team card's jump to the Agent network card nor
the chat→context jump's composition reveal ever scrolled. The walk now
requires scrollHeight > clientHeight, so the first ancestor that can
actually move takes the rect delta (verified live: click lands the
foot card at the scrollport top).
The Agent Team ledger becomes the Agent Network card — the same name as
the big card at the tab's foot, sharing the agents.title key so the two
can never drift. Clicking the card anywhere outside the price link (or
pressing Enter/Space on it) scrolls the foot card into view through the
shared revealInScrollParent helper; the price link and the '?' hover tips
keep their own behavior, and the jump is a quiet no-op when the harness
hides the foot card (no sessions service).
The dashboard's row IS the team now: each listed session folds its WHOLE
subagent subtree into itself (all levels, mirroring the Context tab's cost
fold) — subagent-origin sessions never list, so nothing counts twice. The
session card's billed tokens and priced cost (now labeled 团队用量 /
团队费用), the KPI band's volume and spend, the last-7-days usage chart,
and the heatmap all read the family scope: the subtree's cost buckets
merge per row, the daily ledgers merge day by day, and the composition
split proportions each member's OWN composition instead of the root's.
The flow's fourth node becomes a three-row team ledger: the family's
billed-token and estimated-cost totals in the header, then one row per
scope — the current agent and the subagent subtree with its session count
— each pairing its billed tokens with the estimated cost (billedTokensOf:
the token face of the same buckets the estimate prices). The subagent
scope is the WHOLE descendant tree, which the copy now spells out; the
rows render unconditionally (a scope with nothing billed reads 0 / —),
and both tooltips gloss the token figure.
The six-cell grid becomes a left-to-right flow: two source cards (Input /
Output — human inputs plus the whole session's file reads/writes, with
searches and image reads when they happen; Context Events broken down by
kind) feed the highlighted Current Session node (turns / steps / subagents
— the subtree's session count, folded from the session list alongside the
cost usage), which drains into the two effect cards (Tool Calls with the
three most-called tools and an overflow pill; Cost with the family figure
and, when a subagent subtree reports, the this-agent/subagents split).
Measured bezier ribbons — a translucent band plus a traveling dash per
element, not a conservative Sankey — connect the nodes and flip to a
top-to-bottom stack when the card narrows past the container-query fold.
A ctrl+f-style lens scoped to one section, toggled from a magnifier
button beside the copy control: a debounced query (150ms, so rapid
typing or deleting rescans once instead of per keystroke), matches
wrapped as DOM-level <mark> over the rendered body (same lens in raw
and markdown modes, capped at 500 rendered marks with a '+' counter),
Enter/Shift+Enter or the chevrons cycling the active match into view,
Escape folding the bar. The section body remounts on a text change so
React never reconciles over the injected marks; the restore rebuilds
the original text nodes, keeping the copyable source byte-identical.
The card's reading was the ring's integer (`Math.round`), so a 101.6k /
1.0M session printed 10% while the kernel's own occupancy read 10.2%.
The percent now comes from one shared `occupancyPercent` helper that
mirrors dsh's `contextOccupancy` — the composer ring's derivation
(`min(100, round(used / window * 100))`, `used` = `contextPressure`'s
`projectedTokens ?? pressureTokens`) — on a 0.1 grid, and keeps the
kernel's numerator precedence ahead of the plugin's derived anchor. The
kernel symbol is unreachable from a client plugin (no package export, a
self-registering browser bundle), so the formula is mirrored by
necessity, as docs/compatibility.md now records.
The Agent Network rings read the same helper (whole tenths stay compact
there), and the number row wraps so the wider reading folds to its own
line instead of spilling on the narrowest panes.
The token card's corner already carries the session's cache-hit share
(ef72a7b), so the Context Stats board duplicated it. The grid is now six
cells: turns, steps, human inputs, live tool calls, and the two cost cells.
The removal takes its dead plumbing with it — the card drops its `usage`
prop, and `cacheHitPercent` loses the `decimals` override that existed only
for this cell's one-decimal display. The README rows and the card's
screenshot move the figure to the token card.
The token card's title row gains the session's cache-hit share in its
right corner — the harness chat line's own reads-over-billed formula,
reusing the stats board's label and tooltip — hidden until something
was billed. The timing card's throughput chip drops its pill chrome and
bold weight, now reading 输出速度 N tok/s (zh) / TPS N tok/s (en) with
the TPS expansion on the tooltip; both corners share a new plain-text
.lc-card-rate style and keep their native-title tooltips.
The day strip spanned the card's full width while the columns sit inset
between the two 48px tick rails, so every label fanned out from its bar
group; the topmost tick also rode the title's 4px margin. The plot is now
a 3x2 grid — rail | chart | rail, with the strip as row 2 of the chart's
own column, so labels center under their columns by construction — and
the title falls back to the shared 10px margin. Under a 400px container
the seven MM-DD labels thin to every other one instead of crushing.
The donut cards' slice legend auto-fills at a 150px floor (from 160px)
with an 8px column gap (from 20px) and a 30px share column (from 34px),
so two columns start at a 308px legend — the Insights page's ~440px
stats cards ride this — and wider cards take more; the floor is
`min(150px, 100%)` so a narrower legend shrinks the track instead of
clipping the share column (the 390px fold previously cut 11px off the
percentages). Narrow panes keep their single column; the Context tab
keeps the same column count at every width it already wrapped at.
The Context Insights surface moves from a sidebar-foot button opening a
frame-wide overlay to a first-level panel, a sibling of the shipped
Plugins and Automation tasks pages: the page registers on the layout's
keyed `main` slot and the entry on `sidebar.panellist` under one id
(`dsh-context`), so the shell owns the entry row and panel switching —
mounting the page IS opening it (no plugin-side open state, the footer
entry, the overlay seat, and the overviewStore all go away). The
`insightsEntry` preference now mounts/unwinds the registration pair.
Both seats ship on every supported line (0.1.5-rc.1+), pinned per
baseline by the new insightPage seam probe; runtime-verified on a
disposable 0.1.5-rc.1 profile.
The chrome mirrors the shipped first-level pages: one scroll region
with the 960px centered column, the 20px page heading beside the range
group and the balance capsule, and the page element as the `lc-ov`
query container. Inline-size containment makes the page the containing
block for fixed descendants, so the heatmap/usage/balance tooltips can
no longer ride the harness Tooltip (its portal prop is 0.1.7+): they
now carry `data-lc-tip` served by the page's own hover tip — one fixed
bubble portaled to <body>, delegated off the page root (the image
lightbox's precedent), correct on every supported line.
Session card jumps need no close (openSession's own navigation returns
to the conversation); the settings row jump unmounts the page by
selecting the Plugins panel. Verified across 1440-390px in both
locales, and the /context modal is untouched.
The Duration toggle (added in 01b1532) never joined the title-adjacent
zero-margin rule, so its bare .lc-gran kept margin-left: auto and split
the free space with the control cluster — on wide cards it drifted to
the middle of the title row (242px each side at ~930px), and on wrapping
widths it landed wherever the auto margin happened to put it (line 1's
right end in English, line 2's lead at the narrowest).
DNA / Adaptive / Duration now ride the title text's right as ONE
multi-toggle segmented group (.lc-trend-mods), the tips moving from the
group wrappers onto the buttons; the row keeps a single auto-margin push
(the control cluster), so wrapping drops the two semantic groups whole
instead of scattering the modifiers. Also drop the stale "Total/Delta
disables under DNA" note on the disabled-chip rule — no gran button has
been disabled since DNA+Delta shipped.
The Context preferences card gains its eighth row — the trend
duration curve — served across both settings generations: the V3
`settings.register` schema and the V4 Config form (a `.volatile()`
field, so an edit never remounts the entry). The client settings store
parses it fail-open like its neighbours; the trend card's Duration
toggle seeds its mount state from it (default on), and in-chart
toggling stays mount-local and never writes back.
The trend card gains a title-adjacent Duration toggle: every request's
step active time (`activeMs`) drawn as a curve over the bars, read off
a right-hand quartile axis; turn granularity sums the turn's steps, and
a bar without the figure breaks the line instead of faking a point.
The fold stamps it at `step/end` onto the step's committed request
record: the whole step window (the model call plus its tool runs, per
the agent-loop's in-step `executeToolCalls`) minus the step's user
waits — approval decision intervals (`approval/asked` →
`approval/decided`, a crash-dangling pair closing at the step's own
end) and `ask_user_question` answer windows (the tool's body is the
wait itself, in both the blocking and the timed schema's foreground
phase). Overlapping waits price their clamped union, never twice.
The wire field is additive-optional and bumps the timeline projection's
stateVersion (25 → 26): requests already folded can never gain the
figure later, so cached rows refold from the durable log on upgrade.
V4 harness releases log a `developer/message` event whenever the effective tool registry changes mid-session (first shipped in 0.1.7-rc.2): the message nests under data.message with role `developer`, source kind `tool-registry`, and `tool-addition`/`tool-removal` content blocks, lands on the session surface, and the harness's own token meter prices it like any non-system message. The timeline fold had no case for it, so every such message vanished from the plugin's accounting — injection totals, per-request composition, the live surface, and compaction archives under-reported, and the Context tab's numbers could not reconcile with the harness meter or a compaction shadow price.
The fold now treats the event like an injected user message: the nested message is read structurally (the pinned 0.1.5-rc.1 dependency's deriveEventMessage predates the family), the node prices through the shared estimator with the harness's empty-content-is-zero rule, and the injection record takes the producer kind as its label. Developer messages never count as human inputs whatever a hostile source claims; malformed payloads leave the state reference-equal.
stateVersion 24 -> 25: earlier cached rows omitted these nodes and later events cannot recover them, so on upgrade every timeline row refolds once from the durable log (the dashboard warm-up rebuilds idle sessions). The wire shape and durable logs are unchanged. The V4 baselines now carry the event family, and the real-source compat matrix asserts surface tokens, request composition, warm restore, and a v24-row rebuild on both supported V4 lines.
Validation: 102 test files / 1,910 tests with per-file coverage 100%; the real-source matrix passed for 0.1.5-rc.1, 0.1.7-rc.2, and 0.2.0-rc.2; GitHub PR checks green (486069f fixed the workflow's pnpm setup that had failed every external PR at its first step).
Cordis hands out a fresh traced proxy on every ctx.tools property read of a
tracked Service (ToolRuntime is one), each closing over the reader's context.
The hook keyed its WeakSet dedupe on the proxy identity, so every read
re-wrapped and pinned one restore closure per read — patched[] then retained
each caller's whole agent (scope tag -> agent -> session log) past disposal,
and the unload restore guard (instance.register === wrappedRegister) never
held through a traced proxy, leaving the dead wrapper on the shared service.
Peel to the stable underlying instance before dedupe/patch/restore (the
handler still returns the caller's own proxy, keeping scoped registration
this-binding), record the reader as a scalar fiber name instead of the
reader Context, and add a tracked-service regression test: single wrap
across reads, live scoped attribution, and restore on unload. #108
A redacted reasoning block leaves no chunk behind, so the first token
lands after the reasoning window — pricing ttftMs/genMs off the token
charged the marker-tiled decode window to the wait as well, and the
legend's rows summed past 100%. The wait/generation boundary now sits
at the first observable instant (the earlier of the first token and the
first block marker), the same boundary the strip's spans already paint;
the TPS chip keeps the first-token anchor for harness parity.
stateVersion 23 → 24 refolds cached rows with the corrected totals.
The timing card gains a bottom band that reads where the session's ACTIVE
time went: every completed step's slices — the TTFT wait, the decode blocks
in stream order, the tool-run windows, and the in-step residue — packed
gapless in occurrence order, each band's width its share of the cumulative
active time (idle time between steps takes no track), with ticks at zero,
the quartiles, and the full span.
Host: the fold tiles each step at step/end (clamped into the step window,
first-wins de-overlap so parallel runs paint their union, holes filled with
the residue kind) into a new spans collection — bounded to the newest 2,000,
riding the detail channel and the inline view, never the slim head. The
decode window anchors at the first OBSERVABLE instant (the first token, or
an earlier block marker the token packing could not stamp), so a redacted
reasoning block keeps its span instead of being swallowed by the wait.
stateVersion 22 → 23: the per-step collection cannot be backfilled by later
events, so cached rows refold from the durable log.
Client: the strip joins the card's hover link by KIND — a band hover lights
every span of its kind plus the donut arc and the legend row, and the
floating tip answers only the strip's own pointer (the mirror rule).
In DNA+delta combo mode the tooltip's no-band fallback read
tip.total over the delta record, whose total IS the churn —
the label claimed a total while the figure was the change
magnitude (and a zero-churn bar showed 'Total ≈ 0' anywhere).
The band lookup now sits ahead of the mode branches: a hover
with no band under the pointer falls through to the mode's own
row — the shared delta row (fmtSigned of the net) in delta
modes, the total row otherwise. The delta branch reuses
fmtSigned instead of its inline sign formatting.
The DNA on/off state lifts no further than it already sat: the Context
tab's `dna` state now feeds the Context browser through a controlled
pair of props (dna + onDnaChange), so toggling either card's switch
moves the other — the trend bars and the browser's composition bar
become fingerprints together, and drop back together.
The browser stays self-contained where no parent links it: the pair is
controlled only when BOTH props arrive, a lone `dna` prop is ignored,
and the /context modal keeps its mount-local toggle (the established
optional-seam idiom).
DNA and Delta are orthogonal now: the Total/Delta switch stays live under
DNA, and delta diffs each bar's bands against the previous bar's by key
(deltaBandsOf, dna.ts) instead of diffing category totals. Newcomers and
grown items ride the up arm in read order, removed and shrunk items hang
on the down arm in the previous bar's read order, and unchanged items
vanish from both — the first bar carries no change at all, mirroring the
record delta so the axis stays change-driven.
DnaDeltaBar renders each bar as ONE gradient div spanning both arms
around the zero line (down arm reversed bottom-up, then the up arm),
absolutely positioned like the segmented delta arms, with the entrance
rise anchored at the zero line via a per-bar transform-origin. The
cross-bar lifetime highlight becomes a dumb resolved-slice layer both
DNA flavors feed; the tooltip shows the signed per-item change; band
clicks reveal the item in the Context browser through the same bridge
in both flavors. Adaptive rescaling measures the band arms and
re-measures on any mode flip so the axis never rides a stale scale.
A click inside a DNA bar hit-tests the same pointer-to-band mapping the
hover uses and lifts (bar seq, band) to the view, which forwards it
through the browser's one-shot reveal bridge: the browser switches off
the live surface to the bar's step, opens the item's category and
element row, and scrolls it into view. The bar's own click still pins
the step in the detail panel, and both land on the same seq.
The nodeFocus bridge generalizes from message-only (seq) to the band
keys both DNA surfaces share — 'sys' (system prompt), 'tool:<name>'
(a tool schema), 'n<seq>' (a message) — so header bands and message
nodes reveal through one path, and the step-brief / file-activity
locates convert to the same keys. The reveal now also clears the row
lens, so a stale filter can no longer hide the revealed row (the
brief flow had the same latent trap).
DnaBar's hit-test becomes a total function: the first visible band
starts at off 0 (leading zero-token bands advance nothing) and the
pointer fraction clamps into [0, 1], so a hit always exists.
The Context Dashboard entry now renders the document sheet in the
surrounding text colour instead of the six-colour palette, so it sits
quietly beside the harness's own footer rows. The identity seats (tab
chip, command, sidebar registration, panel head) keep the polychrome
default; the package icon.svg is untouched.
Closes#95
Each bar redraws as that request's context fingerprint — ONE gradient
div paints the assembled items in read order (system prompt, tool
schemas, then messages by seq), coalescing adjacent same-color bands,
so thousands of bars cost the same DOM as the segmented stacks.
Hovering a band lights its lifetime slice in every bar at once (the
items' keys are stable across requests), and the tooltip names the
item via the label core shared with the browser's DNA mode
(dnaBaseLabel). The toggle rides the title's right, left of the
adaptive switch; the Total/Delta switch suspends while DNA is on —
DNA plots absolute composition, and a stale 'delta' state degrades to
total semantics instead of emptying the chart. The browser category
focus is ignored (the bands already are the full composition).
Bands are built per displayed record with the same pure client-side
assemble() reconstruction the Context browser uses, memoized on the
timeline so hover re-renders never reassemble. Host-side is untouched
— no wire or protocol change.
A 'Context Browser · Diff baseline' row leads the tool-sort preference:
'prev step' (default) or 'prev turn', reusing the toolbar's vocabulary.
The browser's baseline toggle mounts from the persisted value; in-toolbar
toggling stays mount-local. Served through the settings namespace schema
and the Config-form generation, degrading to the default on stale values.
Replace the fixed 'vs previous turn' hint with a baseline toggle (default:
previous step). A predecessor-less step or live surface diffs against zero,
so the whole makeup reads as change; on live, 'previous turn' measures this
turn's growth so far. The step picker options drop their time suffix.
A session can sit with no pushed contextTimeline value at all — the enable
recomposition raced its open baseline (the projection store clears a key the
baseline omits), or its checkpoint row is missing or version-stale on a
cold-observed session. The old code returned a loading surface in that case
with no request and no failure path, so the Context tab and /context modal
stalled on "Reading the session log…" until a refresh.
The source now opens the detail channel itself (rev 0): the detail route
folds the durable log on demand and serves the slim head beside the
collections, the next pushed head takes the channel back per its own
revision, and a read that settles without data surfaces the retryable
failure note instead of an eternal spinner.
Align the assistant text section head with the Answer filter capsule
term, matching dsh's finalized-answer vocabulary for the non-thinking
reply content.
A tool-name capsule — a row tag (tool results, call breadcrumbs) or a
call head (→ bash, ← bash) — no longer clicks through to the tool's
definition row in the Tool Schemas category: the schema reveal from
5a62b2b is removed whole (the reveal jump, the onSchema threading, the
Section label click affordance, the hover-link styles, and the tooltip
key in both locales). The ×N call-name grouping stays — one capsule per
distinct call name, repeats folded into a multiplier — as plain tags on
the row button, so a tag click bubbles to the row's own toggle.
The assistant search box scanned the tag, preview, and the node's
stamped text — the reasoning blocks live only on the conversation join,
so thinking text was unsearchable. Derive one concatenated reasoning
face per assistant row (both block vocabularies, malformed blocks drop)
and scan it alongside the other faces; the placeholder names the new
face in both locales.
A call breadcrumb no longer renders one capsule per call joined by '›'.
The names group by identity: one capsule per DISTINCT tool in
first-appearance order, repeats folded into a multiplier ('bash ×3'),
each capsule still clickable to reveal the tool's schema. The row's tag
slot becomes a shrinkable capsule group, and the hover affordance moves
from the inner segment to the whole capsule. The tag's scan text keeps
the raw joined names, so the text filter still matches every call.
A tool-name capsule — a row tag (tool results, call breadcrumbs) or a
call head (→ bash, ← bash) — is now clickable and reveals the tool's
definition row in the Tool Schemas category: the category opens
unfiltered, the named row expands, and a one-shot scroll lands on it
(the same reveal the DNA bands use). A breadcrumb renders one clickable
segment per call; its segment click stops propagation so the row toggle
cannot overwrite the reveal. Tags that do not name tools stay inert.
Hover affordances: tag segments lift to the primary label on the shared
interactive hover face; call heads shift to the brand face + underline.
The tag capsules (browser row tags, file-activity op tools, step-brief
tags/facts/chip tags) bordered on border-l1 — 6% white in dark mode,
near-invisible against the row. Step the edge to the harness's next
border tier (border-l2, 12% white / 10% black), the pairing the pick
control already uses, so the capsules read as pills in both locales and
themes.