The DNA flat feed lays every message out at once, and an uncapped card would
outgrow the page. The browser column now measures the composition and trend
cards stacked beside it — per render, followed by a ResizeObserver for pane
drags and card reflows — and tops out at three times their sum, scrolling
taller content inside; the card head (title, composition bar, notices, and the
DNA feed's lens) stays pinned above the scrolling list. The /context modal
keeps its own single-card scroll and stays uncapped.
In DNA mode the category accordion yields to one flat list of every assembled
item: the model reads the prompt oldest-first, so the list presents that order
bottom-up — the newest message is row one, and the tool schemas (one group row
opening into the category body) plus the system prompt close the list. Each row
carries its #prompt position, a category chip tinted with the category's own
palette color, its introducing-step stamp and token price, and expands into the
same full content.
A text lens pinned in the card head filters the feed's rows and the open tools
group's schemas alike (the served system prompt text included); hovering a row
lights its exact band — or its band group for the header rows — on the DNA bar
without reporting upward; a category jump (catFocus) drops back to the
accordion, which has no row landing in the feed.
Whole-text JSON payloads (a leading { or [ that parses) open on the shell's
JsonTree inspector instead of markdown: the switch offers Raw|JSON there
(markdown is meaningless on structured payloads), the tree opens two levels
deep via the expanders' own click path (focus restored, capped at 100), and
an expanded row drops its inline preview Chrome-DevTools style. The JsonTree
faces are pinned per baseline and probed by the compat matrix.
Archived sessions fold back into every figure the page derives off the
session list: rowsOfSnapshot now tags archived rows instead of dropping
them, and one coveredRowsOf pass scopes the KPI band, charts, heatmap,
skills, and the session grid together. A "Filter sessions" select next to
the list's search box offers dsh's own archived-filter set — All (show
archived) by default, Hide archived, Archived only. The Active Sessions
card follows the picked coverage (Sessions incl. the window's archived
count under All; Active/Archived Sessions over the full-list total under
the narrow scopes), archived cards wear an Archived tag and refuse to
open like dsh does, and kpisOf drops its now-unused listed param.
A bar-right toggle reveals a squarified treemap of the shown step's
context: category regions sized by tokens, with same-kind items folded
into one weighted block (every bash result together, skill loads by
skill, injections by form) and an 'others' tail past the fold cap.
Clicking a singleton block lands on its exact row (the DNA band pick's
path), a folded block filters the category's rows to the group's tag,
and a region header toggles the category like its row does. Block and
region hovers float a figure bubble and, outside DNA mode, report the
category to the shared composition hover; the open category keeps its
region framed.
The pinned step's per-category rows were a second rendering of the same
categories, drifted from the one above: their own grid, dimmed regular-weight
labels, a fixed figure column. The panel now renders the composition card's
own chip legend — same dot, 600-weight label, right-aligned figures with the
secondary share, same hover highlight and 160px grid reflow — in the shared
CATS order, so a category reads identically in both cards.
Delta mode has no counterpart above, so makeLegend gains a delta variant: the
chips print the signed change colored by sign, and both the share and its
"of used" hint drop out. The panel's chips report hovers back exactly as the
composition legend's do, and the reserved lane mirrors the chip grid so the
pending and loaded panels still measure alike.
Code review flagged the two "no mini bar" assertions as a duplicated absent-class
check. One anatomy pin per mode now asserts the child sequence a row is made of —
dot, label, figure, share — and a mutation test confirmed reintroducing
.lc-bar-track anywhere in the row fails both modes.
The pinned step's per-category rows carried a mini bar beside the figures
— the share track in Total mode, a diverging zero-line bar in Delta. Both
go: a row now reads dot, label, figure, share, with the figures pinned to
the row's right edge, and the trend card's reserved lane mirrors that same
anatomy so the pending and loaded panels still measure alike.
The Fleet task board's opened card boards its owner's report-back from a
wire excerpt the route clips at 300 chars, so expansion could never show
the whole text even though DSH itself hands the fold the whole log. The
fleet route now reads a `full: true` body whose prompt/reply skip the
excerpt caps (only a hostile-payload bound stands), and the task board's
unfold asks for it once per session per mount: the excerpt renders
first and swaps in place on landing, the section meta carries
loading/excerpt-only states, and the scan never pays for the full read.
The head keeps a teammate's roster name in bold with their duty in the light
secondary tone (the member cards' desc idiom); the self and lead cards keep
their title. The label box grows to a fixed 3 lines so the duty wraps to 2 —
the card grows uniformly with AGENT_CARD_H, keeping every card's metric row
aligned and the link anchors on the card's edges. The geometry pins derive
from literals so a layout bug cannot mirror its own arithmetic.
The aside rides the shipped catalog's own address and options, addressed by the
node's direct parent; a face that cannot take the resource keeps the whole-view
jump on every supported line. The openers' test suites move to their own file
to hold the 1000-line cap.
The overview's quartile poles and the turn strip's width-quartile labels also mark the
full-span position (the last record / last turn), so the log's end carries its figure like the
quartiles do. The full-span pole insets by its own 1px width (calc(100% - 1px)): at exactly
left 100% the pole falls wholly past the scroller's clip, while the number label keeps reading
leftward inside the pane. The collapsed-at-start pole still drops — at the left edge its
label would clip and the pane edge already answers. Chip tips and the README now say quartiles
and the full span.
One toggle next to Duration flattens every retained step (or turn) into a continuous stacked
ribbon across the visible width — no horizontal scrolling, nothing computed while it is off:
the drawing geometry memoizes on [requests, mode, duration] so hover re-renders never rebuild
the polygons. Flags mark the record quartiles only (interior-only: short logs' collapsed edge
quartiles drop), and the turn strip stays proportional with its labels on the width quartiles.
Interaction keeps the bars': nearest-record hover with the shared tip, click to pin, scissors
markers, the duration-curve overlay on the same svg with its right-hand axis, Total/Delta
(signed up/down bands around the proportional zero line) and category focus. Exclusive with
DNA mode and Adaptive: activating one clears the others.
trendChart.tsx lifts focusOf/deltaOf/trendTipRows and the whole-log maxima to module scope
(preprocessTrend/trendMaxima) so both faces share one preprocessing path and one tip shape.
The file card priced only the pathless branch of the harness handle — the one
dsh emits when the stored copy cannot be read — and disclosed that shortfall
with a `≥` lower-bound marker, which read as a bug beside the image rows' `≈`.
Every other token row reports a heuristic estimate, so this one now does too.
The tooltip claimed the handle named "where the stored copy can be read": the
path-bearing branch, which costs roughly 1.7x more and is never priced. Under
`≥` the marker covered that gap; under `≈` the explanation described a branch
the number does not come from, so the clause is gone from both locales. The
estimate itself is unchanged.
A file block never reaches a model as bytes: dsh projects every one to
`fileHandleText` at request assembly, and the token meter prices that handle in
place of the block's structural JSON price. An attachment therefore costs a
fixed ~60 tokens whatever its size, so the card now shows that price beside the
size — computed from the three facts a log carries (name, byte size, digest
prefix) and priced with the meter's own text arm, now shared with the fold's
message price instead of restated there.
The handle also names the stored copy's read path, which only the live
environment resolves and no log holds, so the row prices the pathless arm and
reads as a lower bound (`≥`) rather than an `≈`. The fold keeps the meter's
fixed structural price for the same block, exactly as the harness does: one
figure answers the request, the other the surface.
Three cards-side-by-side fixes: the hover invite is one pill over the
thumbnail instead of a mask that blacked out the glyph; a file card drops the
type row its extension badge already states (an image keeps it, having no
badge), which leaves name plus size; and the thumbnail read now keys off the
attachment id instead of the reference object, which the fold re-proves on
every render — a row re-render no longer re-reads the bytes through the
harness loader. The in-flight card also reports itself busy to assistive tech.
A user message's image and file blocks now render as one ordered attachment
block in the Context browser: images keep their session-authorized thumbnail,
other files show their type glyph, extension, and stored size, and the row
chip counts every attachment. One click IS the preview — the card hands the
attachment's named host path to dsh's own preview column (the File Activity
card's `previewAddressOf` → `openResourceVia` path), which renders markdown,
code, PDF, office, and image viewers under the file's real name. A refused
open reports on that card alone; the image lightbox and the per-type modal it
replaced are gone with `images.tsx`.
The path comes from the plugin's own `/api/dsh-context/attachment` route (the
same authenticated `/api` fence, Connection exact-route registry, and
cold-read gate as the detail and fleet routes). It answers only an id the
session's own log cites — a depth-bounded, total walk over the event payload
(user content, tool-result blocks, spliced inbox deliveries) — and answers
the store's named alias: `attachments.fileHostPath` for verbatim files, and a
published-or-reused named hard link for images, whose objects are digest-named
and suffixless while the preview column dispatches viewers by suffix. The
link shares the immutable object bytes and lives in a disposable cache
(`<home>/cache/dsh-context/previews/`); a store that is not host-file-backed,
or a media type the harness never admits, resolves the typed null the card
reports as an unavailable preview.
`Section` moves to its own module so the attachment block heads itself
without importing the tool card, and the compat matrix probes `FileTypeIcon`
alongside the icons it already pins.
The trigger percentage is derived from dsh-compaction-basic's mirrored
shipped defaults rather than read from the deployment's configured policy,
so hedge the copy instead of asserting it outright.
The output mask's tooltip said this reply's output is reserved at the
window's end, but the value is the model's configured max output tokens.
Update the zh/en copy and the two tooltip assertions that pin it.
The two tooltip labels named the mechanism, not the thing on screen: "Max
output tokens" reads as a statistic, but the mask draws the window's tail held
back for the routed request's reply, and "Compaction headroom" said 余量 — slack
— where it is in fact reserved space. Say both as the regions they are:
Compaction Headroom 65.5k
LLM Output Reservation 256.0k
本次回复的输出预留在窗口末尾
Both English labels now share one Title Case so the three-row tooltip reads as
a set. The note lines keep their sentence form, so only labels changed.
The bar's tooltip was one nowrap string per hover kind, the only tooltip in
the plugin not yet multi-line. Split it into head / figure / note so all four
kinds share one skeleton and wrap:
Tool Schemas ≈6.4k · 3% of used context
Free Window 733.2k · 73% of window
Compaction headroom 65.5k
Auto-compaction fires at 68% of the window
Max output tokens 256.0k
This reply's output is reserved at the window's end
Every figure now names its denominator. The segments share the USED total and
the free track the WINDOW, which the old single line never said; the two masks
had no share at all and now state where they sit in the window instead, read
off the same clamped trackPct the masks lay out with rather than a second ratio.
Replaces the hardcoded 12/88 left clamp — a guess at half the tooltip width that
only held around a 920px pane, and cut the bubble off at narrower ones. The
percentage anchor stays the source of truth; a layout effect measures the tip and
adds a px shift only when the rows overhang. Rects, not clientWidth, whose whole-
pixel truncation lands the residue straight back on the overhang.
Also adds role="tooltip", and box-sizing so the width cap means what it says.
dsh-compaction-basic REFUSES a target at config time when the output
reservation plus headroom leave no pressure budget (resolveCompactSpec
throws TargetPressureConfigError, compactIfNeeded never runs) — it does
not clamp. The reserve comment claimed a clamp, and the geometry acted
on it: a too-small window collapsed threshold to 0 and painted a
full-width headroom band, claiming a trigger the harness never fires.
The bar now reserves nothing and paints no masks for such a window,
matching the harness. Also removes the redundant reserve comments the
review flagged and restores the dashed-border rationale the mask CSS
had displaced.
The Current Context bar showed a single hatched band mirroring dsh's
default 80% threshold ratio, which reads wrong on any window where the
pressure budget binds instead (small windows, or a large routed output
cap). It now renders the two real reservations the harness spends the
top of the window on, priced by dsh-compaction-basic's own min() rule:
- the compaction headroom mask (mirrored default 65,536 tokens), then
- the routed request's output reservation, read from the durable
request/header's materialized config.maxTokens rather than guessed.
The output cap folds off request/header at fold time, so the timeline
projection's stateVersion bumps 27 → 28 to refold cached rows from the
durable log. Geometry lives in a new client reserve module; the bar
tiles [threshold, window] with the two adjacent masks, each with its
own name-plus-length tooltip.
Reading a day's volume meant hovering every column or interpolating between
the left rail's quarters. Each blue bar now carries its own figure on its
top edge, so the common case reads straight off the chart; a day with no
tokens prints nothing, since there is nothing to say.
The label is a child of its bar rather than positioned against the column,
so it rides the bar's height without a second source of truth for where that
top is. The gridlines move into the bars' own box for the same reason: with
headroom taken above the plot, `bottom: N%` still reads the bar scale, so the
822.1M line keeps marking the 822.1M bar instead of drifting by the headroom.
Measured across ten pane widths: the labels overlap below a ~30px column, so
they fold out under the same 400px container query the day strip already
uses — the hover tip carries the exact figure there, and keeps doing so
everywhere else.
The Context tab's Plugin-info card already hinted at a newer npm release;
the Context Insights page head had no such hint. Add one beside the
balance capsule, in that page's own capsule idiom rather than the card's
inline chip: label plus the version in the warn token, linking the release
notes through the page's portaled hover bubble.
The registry check moves out of pluginInfo into a useNewerVersion hook both
surfaces read, so the second hint costs no new fetch path, state, or
mechanism — one TTL-cached read still answers both. Its own coverage moves
with it into latestVersion.spec, leaving each surface's spec asserting only
what that surface renders.
The two head capsules share one CSS chrome instead of a copy, and the head's
gap drops to 8px so the capsule cluster tightens; the title keeps its wider
16px lead-in, and the fold at narrow widths leaves every wrapped row on the
same left edge with no overflow.
watchPlacement inferred "this side is mounted" from the mount handle
being a function, so a face that seats the seat and hands back no disposer
(a stripped or foreign `slots`) read as never mounted — and every later
preference publish registered a second copy of the same seats. Track the
mounted state directly; the handle is unwound when there is one.
The same package can mount twice into one page's slot ledger — a static
profile bundle plus a later activation of the same package. The
client-plugin runner force-assigns a fresh shadowing priority to every
registration, so the registry's same-id duplicate guard never fires across
the two mounts, and the conversation header — which renders the raw ledger
and matches the active view by entry id — paints both copies of every
list-slot seat.
apply() now reads the raw ledger first (SlotRegistry.entries) and mounts
nothing when this bundle's chat-to-Context jump entry is already seated.
The read fails open: an absent, shape-shifted, or hostile face reports
unseated and the mount proceeds, so an older line never blanks a working
mount. The entries seam is pinned per baseline.
The harness-side defects (the runner forcing a rank on additive list
slots, and the header rendering the raw ledger) are left to
deepseek-ai/deepseek-harness.
- A pick anywhere (graph node, member card) now steers the comms timeline
beside the task board: rows narrow to the traffic the picked agent sent or
received, the dropdown steps aside for the pick chip (label, never a raw
id), and an agent without traffic says so
- The pick governs while it stands: the dropdown's own choice yields on
landing and never resurrects stale once the pick clears; without a pick the
dropdown keeps its free per-participant browsing
- The pick key resolves once in the view (roster name -> session id, a
subagent's own id) — the one key both boards narrow by; zh/en gains the
no-comms note
- A task card opens on a pick (or Enter/Space on the row): the task brief, the
owning agent, and the final report-back each box up in the context tab's own
section chrome, with the raw/Markdown switch on every box head — Markdown the
default, the report-back stamped with its time
- Closed rows keep the compact read, the description clamped to two lines and
rendered through the shared Markdown renderer (memoized per text, so board
hover re-renders never re-parse)
- The row pick no longer pins: pinning stays with the owner chips, the band
heads, and the agent box inside an opened card; Enter on a nested control
keeps activating that control instead of folding the card
- zh/en gains the box labels (task brief / agent / final reply)
- Plain subagents join the board: each delegation reads as a task of its own
(the prompt as the subject, the session state as the status) with the run's
live figures on the row — tokens · duration · steps and the pressure-tinted
occupancy; the row itself is the agent (family-hue icon, session id on hover)
- One vertical, owner-grouped layout replaces the status/owner switch: roster
members first (the lead in brand hue, teammates by stable roster hue), stray
owners the roster never served, then headless delegation rows in spawn order,
the unowned pool last; status lives on the rows
- The toolbar gains search (subject/owner/id) and status filter chips with
counts; the completion bar now counts every row on the board
- A pick anywhere narrows the board to that agent — graph nodes and member
cards included, subagents by their own session id (the pick chip names the
label, never a raw id); the members board reports picks raw and the view owns
the toggle, so a release lets go of the paired narrowing wherever it came from
- Renamed to Tasks (team tasks and subagent delegations); zh/en updated, the
dead view-switch keys removed
- Cards keep a 178px minimum width (slot pitch floor 150 -> 190) with tighter
padding (9x10 -> 7x9) and a 100px height, so dense families read wider and
calmer
- The corner arrow button is gone: in the Fleet seat a card click selects and
the jump stays with the boards' own open buttons, while the sidebar seat
keeps opening on click; the select-mode subtitle no longer names an arrow
- The other-agents section renders the family's plain subagents, ancestors, and
siblings as the same cards the team roster uses — label as the identity (two
clamped lines), node-state dot, status + model line, landed latest reply, and
an open button; the card hue follows the graph's family hue
- Member cards gain the run's activity meta (tokens · duration · steps, with
the pressure-tinted occupancy figure), team cards included
- A compact card layout (8x10 padding, tighter gaps) and a 200px grid floor
keep the two-column read the common case; the fleet-row list is gone
- Overview strip: family stats (agents/running/done/tokens/est. cost) with an
adaptive progress bar — shared tasks when a team serves any, else subagent
completion, and no bar for a solo session
- Agent network: taller link channel (level pitch 144→168), node cap 25→40,
and a card click now selects (ring + the task board's owner filter) instead
of jumping sessions; the jump moves to a corner button on each card
- The graph's cursor-following inspector leaves the Fleet tab (the boards
carry that detail); the sidebar mount keeps it
- Members board absorbs the roster: team cards section plus a compact
other-agents section, one search/status/sort toolbar steering both; the
standalone FleetList card is gone
- Task board: completion bar, status/owner grouping switch, owner pinning and
hover lineage from task rows, transitive prerequisite-chain highlighting,
and a guidance empty state without a team
- Both boards stay mounted with guidance notes in team-less and solo
sessions, so the page never reshapes between session kinds
- Hoist displayCurrencyOf into cost.ts (was duplicated per mount)
On DSH Desktop for macOS the shell moves the window by elements marked
`data-window-drag`, and the shipped pages mark their head, which their
top padding stretches over the hiddenInset title bar band. The insight
page head already takes that band (`--dsh-frame-top-clearance`) but was
not marked, so the window could not be dragged from the top of the page.
Mark the head. The harness keeps buttons, links, fields and anything
outside its root out of the drag area, so the settings chip, the
balance link, the date range and the range group stay clickable, and the
date picker's portaled dialog is unaffected.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Fleet tab's Team Members and Task Board cards stacked vertically no
matter how wide the pane ran. They now share a `.lc-cols` row — the
plugin's own two-column idiom (flex-wrap + the shared
`flex-1 min-w-[min(360px,100%)]` floor the head-band cards ride) — so
they sit side by side as two equal-height columns and fold back to
full-width stacking below two 360px floors plus the row gap.
Gating semantics are unchanged: the task board still serves whenever the
team projection exists, even without a family forest (the duo row then
carries it alone at full width), and the members board still needs the
forest for its roster join. Tests pin both the together case (same
`.lc-cols` parent) and the forest-null case.
Three review passes (standards, contract, over-engineering) over the whole
settings surface left four follow-ups worth taking:
- sync() wraps its snapshot parsing in the compatibility.md parser gate's
bounded catch: a served value that throws on property access keeps the
current state instead of throwing into the mirror's fan-out, with the
mandated hostile fixture beside the happy path.
- the attach disposer now clears the scope, so a set() after disposal never
writes into a dead form; a superseded attach keeps the newer scope bound.
- dead surface trimmed: six type re-exports nothing imported through the
module, a single-caller query helper, and an orphaned export on the
settings-card props interface.
The card's eleven rows had grown into one flat list whose only structure
was "Topic · Field" prefixes baked into the labels. The rows now sit under
three titled groups — Entry & Visibility, View Defaults, System Prompt —
so the card reads as where the plugin appears, what its surfaces show by
default, and what it injects, in that order.
The harness projects no group chrome into the `plugins.bundle.config`
slot, so the card draws its own: group heads mirror the harness's
Plugins-page `.sectionTitle` (14px/500 primary, one weight step above the
pinned-400 row labels), separated by its row-separator token. Each group
carries `role="group"` + `aria-label`; the prompt hint moves inside the
prompt group it describes.
Labels drop the prefixes the group titles now carry ("Trend Chart ·
Default granularity" → "Trend Chart · Granularity", "System Prompt ·
Prefix" → "Prefix"), zh/en in lockstep. The spec test asserts the three
groups, their order, and the hint's seat; row order is unchanged.
Two new per-user preferences, `promptPrefix`/`promptSuffix`, hold free text
joined at the very start / end of the harness's assembled system prompt for
all agents (subagents included) — a per-deployment persona or behavioral rule
riding the harness's own prompt registry instead of session-log surgery:
- src/host/promptInjection.ts registers two global prompt sections through a
deferred `ctx.inject(['systemPrompt'], …)` (a composition without the
registry degrades to a no-op; the registration unwinds with this plugin's
fiber). Orders anchor on the harness's own section-order registry, so a
central renumbering keeps the prefix first and the suffix last. Text is
registered `interpolate: false`, so `{{...}}` in user text stays literal.
- The section text providers read the volatile config references the loader
rewrites in place on preference edits, so an edit applies from the next
turn without remounting the entry; new sessions carry the text from their
first turn through the harness's own system-prompt projection.
- The settings card grows two free-text rows (local draft, committed on blur
and on unmount; a host push never clobbers in-progress typing), zh/en
strings, and the served-value sanitize/degrade path for the string fields.
Verified against a running harness: the committed text persists in the
profile layer, and a new session's system prompt starts with the prefix and
ends with the suffix. docs/compatibility.md documents the seams, and the
compat matrix probes them on both supported lines.
Split the Agent network card into graph + cursor-following inspector
(delegation prompt, latest reply, descriptor, owned tasks, comms), add
the members board and the status-grouped task board off the agentTeam
projection with inline dependency chains, a filterable/sortable roster,
and an on-demand comms timeline over relay/mailbox/settled records.
Data rides a new /api/dsh-context/fleet route (one observeSession read
per session through the shared cold-read gate); hover, pin, owner
filter, and task focus are lifted to the view so every panel steers the
same inspector.
The preferences card gains a "Fleet tab" row (Show/Hide, default Show): the tab is
registered or unwound live, and the placement preference still has the final say —
'sidebar' serves no conversation tabs at all. The ninth preference field rides the
same Config schema and settings transport as the rest, including the fail-open read
(a value the plugin cannot read degrades to Show) and the rollback when a write is
refused.
The three on/off rows (entry visibility, the Fleet tab, the duration curve) now
share one row shape and one set of Show/Hide labels instead of a copy each, and the
mount/unmount/subscribe watcher both gates ride is one helper (toggleGate.ts) —
watchInsightPage and watchFleetTab were the same skeleton twice, down to the
registration-handle guard.
One preference now costs several coordinated edits in the settings store, so the
snapshot comparison is a shallow compare over the state's own keys and the refused-
write rollback is one table lookup, not a chain that has to grow with every field.
The stats board's family cell hands its jump to the caller: statsContext no longer
reaches into the tab bar for the Agent Network view. contextView owns the targets —
the Fleet tab when the preferences serve one, else the right Sidebar panel that
keeps the card inline — and drops the cell's button role when it serves neither, so
a hidden tab can never leave a dead button behind.
The Agent network card leaves the Context tab's foot for a page of its own —
the Fleet / 编队 tab, right of Context (order 30). The card is unchanged,
but both mounts now read one shared `useContextSession` data path and fold
the current agent's figures with one `agentSelfOf`, so the panel and the tab
cannot disagree; the Fleet page keeps the same cold-start retry note and the
same baseline gate. The tab label key splits into `tab.context` / `tab.fleet`.
The right Sidebar's Context panel keeps the card inline — its column has no
room for a tab of its own — so the center Context tab renders it only for the
sidebar host, and nothing else leaves that tab.
The stats board's Agent Network cell used to scroll to the card further down
the same page; it now activates the Fleet tab (activateContextTab is
generalized to activateViewTab). Its miss arm is unreachable for our own
tabs: the board and the Fleet tab ride the same placement gate.
The bundle smoke's fake registry now records `register()` calls instead of an
inject callback's return value, matching the real registry — one inject may
declare several registrations, which the second tab exposed.
An injection row's label is now a link into the Context Browser: hovering
underlines it, and a click selects the step that first carried the item,
opens its category, expands its row, and scrolls the browser card into
view. An injection event's seq IS the seq of the surface node it added
(fold.ts), so the events row and the browser row are the same item; the
node's own `gone` stamp bounds the reveal to the steps before its removal.
The step comes from the existing locateStepOf(), which now sorts the wire
the way the browser's own entry stamps do — a re-ordered log must not move
the reveal past the step that actually carried the item.
The events card's position column spelled the same fact out in full
("第 1 轮 · 第 9 步"), crowding the row's meta tail on narrow panes. It now
uses the notation the browser's own rows already carry (browser.stepAt):
T1 S9, T2 S3→4, T50 S8 → T51 S1.
eventAt() is shared, so the request detail's marker chip and the trend
chart's marker tooltip read the compact form too — one notation for one datum.
Opening the Context tab, and every step landing after it, moved the cards
below: the page grew 343px on a cold open and the trend card jitters by a few
pixels on each step. Seven independent sources, all the same shape — a box
whose height came from its content instead of from its role:
- The brief lane under the chart held a 76px floor but its rows were 24.5px
bare and 26.5px carrying a chip, so the lane measured 79.5px or 81.5px
depending on the step. Every row is now a constant 27px.
- The detail header let its metric pills share a wrapped line with the title,
so its height followed which pill topped each line as the figures changed.
The pills now claim a full row of their own.
- The file card's empty state replaced the controls and the 320px list with
a one-line note, a 265px collapse on the first read of a session. The band
and the list lane now render in every state, and the lane holds the list's
height.
- The file card's filter band rewrapped as its tallies gained digits; it now
reserves its folded height, which depends on the card's width alone.
- The context browser's category rows wrapped as the per-step item counts
changed; they now stay on one line and the name ellipsizes.
- A title carrying only text wrapped when its scope chip changed length
(longest in English), resizing the card mid-scrub. Text-only titles keep
one line; titles carrying toggle groups keep the wrap they need.
- The trend card's pending body rendered as a one-line note and grew by its
whole height when the first step arrived, shoving the page down. It now
stands in the panel's OWN boxes — same classes, same grid — so the detail
fills the card instead of growing it.
Measured across 360px–1440px in both locales: every card's height variance
while scrubbing a 40-step session is 0, and the cold-open fold is 5px.