The Context Stats card's inject/compaction/prune cells duplicated what the
events card already tracks. Drop the three cells (the grid falls to five:
turns, steps, tool calls, cache hit, cost) and show each tally inside the
matching kind-filter button instead, sourced from the same precomputed
counts the stats card used.
The rates themselves check out against the official pricing pages (both
currencies, both families, and the Beijing peak windows), but the flash
row's label named deepseek-v4.1-flash — the model VERSION, not an API ID.
The page's current flash ID is deepseek-flash, with deepseek-v4-flash as
the accepted legacy ID, so the label and the tip's applies-to list now
cite those; the tip also drops the colloquial hardcoded phrasing for a
plain for-reference-only note.
The images cell said little about the session; the eighth cell now shows
the whole-session prompt-side cache-hit rate off the official tokenUsage
projection — the same source and formula as the chat stats line under the
composer, truncated to one decimal (cacheHitPercent gains a precision
parameter; the two-decimal default keeps the request detail's figure).
The donut center now carries the whole-session billed total — the same
figure the harness chat stats line shows under the composer (uncached
input + cache read + cache write + output off the official tokenUsage
projection) — instead of the cache-hit share.
The slices answer WHAT the tokens are rather than how the provider
cached them: the six composition categories share the provider-reported
prompt-side total by the composition card's own estimated ratios (their
counts carry the ≈ marker), and the provider-exact output closes the
ring, so the parts equal the chat line's figure by construction.
The cache-read/cache-write/uncached bucket view and its four locale
keys are gone; tokens.total is new.
A turn aggregate IS its last step's record (the same seq), so with a
plain seq key a step ↔ turn switch reused those bars' DOM nodes — the
entrance animation had already finished on them and CSS never replays an
animation on a reused node, leaving every turn's last step bar popping
in unanimated. Keying ChartBar by seq + granularity forces a remount;
mode (Total/Delta) switches already remount through the stack ↔ arms
structural change.
The donut slices sweep in clockwise from 12 o'clock, the composition
stacked bar grows open segment by segment, and the trend chart bars rise
from their baseline — all staggered in layout order, transform-only (no
layout thrash), running once per mount and disabled under
prefers-reduced-motion (the harness's own idiom).
- the stagger caps (trend bars 20, stacked pieces 8) so long logs and
long DNA band lists still settle in well under a second
- the auto-compaction reserve band stays unanimated: as an overlay its
growth would smear the stripes, and it reads as chrome, not data
- covers the tab, the /context modal and the right Sidebar, which all
render the same chart components
Clicking a file name in the File Activity card now opens the file's preview
tab in the right Sidebar — the shipped document-preview viewer, reached the
same way the built-in Files sidebar reaches it — on harnesses that serve that
column (dsh 0.1.5-rc.1+). The address is built with the harness's own
browser-safe `fileAddressFor`, inlined by the client bundle exactly as the
Sidebar's file types inline it, so a path under the workspace collapses to the
session-relative spelling and every route to one file settles on one tab.
The capability is OPTIONAL by contract, like the tab registration itself: the
opener reads `ctx.sidebarRight.openResource` through the runtime boundary and
re-proves it per call, and reports whether the column took the address. An
absent column, a face that refuses the address, or an unencodable log path
falls through to the pre-existing system-open behavior, so an older line is
fully functional and nothing becomes an inert click. Pattern rows and
directory targets are never previewable.
Adds the `@deepseek-ai/dsh-util-workspace-path` dev dependency (itself
browser-safe), allows it in the client bundle purity gate, pins the
resource-navigation seam in the compat matrix, and covers the address builder,
the card's preview-then-fallback click, and the optional opener per baseline.
Closes#58: the Context panel hung off two registrations at once (the
conversation tab and the right-Sidebar guide entry), with no way to keep
just one. A new per-user `defaultPlacement` preference — the first row of
the Plugin configuration card — picks who carries the view: the
conversation tab, the right Sidebar, or both (the default, so existing
deployments see no change).
- shared: `DefaultPlacement` vocabulary (`all` | `tab` | `sidebar`) on the
settings namespace; the Host schema gains a loose `defaultPlacement`
field so a stale persisted value degrades to `all`
- client: `placement.ts` mounts each registration through its own factory
and, on a preference flip, disposes exactly what the new value drops
(the harness's own dynamic-mount idiom); `watchSidebarContextTab`
returns its deferred-inject disposer so the sidebar mount can be taken
down and put back; the settings card leads with the placement row
A ring with a neighbour on each side now gives half a 0.5-unit gap off
both ends of every arc (1 unit ≈ 1% of the circumference, so ~1px on the
96px card ring), letting the card's own background draw the line between
neighbouring colours. A lone painted slice keeps its full ring — a gap
there reads as a nick — and the cut never takes more than a quarter per
end, so a sliver like the 0.2% overhead wedge never vanishes whole.
Every rich detail section (system prompt, tool schema descriptions,
messages, tool results, compaction summaries, and the same cards inside
the /context modal) gains an icon-only copy control beside the Raw/MD
switch. It copies the section's exact SOURCE text, never the rendered
markdown, via the harness's own writeClipboard primitive; success flips
the glyph to a check for a beat, a rejected write claims no success, and
a repeat click inside the window is a no-op — the shipped code-block
control's behavior. Both icons and the writer are platform-module
exports present on all three supported baselines.
The added control tipped the head's trailing group out of the clipped
section card at the narrowest pane, so rich-text heads now fold that
group onto a second line under width pressure. The wrap is scoped to
those heads: the call-name head must stay one line (long MCP names
ellipsize instead).
A slice that never happened still rendered as a row — "0.0%" plus a count
borrowed from elsewhere on the card — which reads as "this ran 323 times in
no time". The ring has always skipped zero arcs; now the legend matches it,
so the card shrinks to the slices that actually carry time.
Zero slices are common: across 307 local sessions, reasoning is zero in
23.5%, answer text in 14.3%, tool args in 13.7%, and tools in 14.3% (a
one-turn "hi" session collapses from six rows to two). Overhead is the
residual and effectively never zero, so the change bites the decode and
tool rows. Every slice is a share of the wall total, so dropping rows never
distorts the remaining percentages.
The Timing card's model slice expands into Thinking / Answer / Tool args
whenever the host served the split; an older cached row (or a log whose
stream carried no block markers) keeps the single LLM Gen row, so nothing
degrades. The ring shares the generation window across the buckets in card
order, clamped exactly as tools already were.
The decode rows carry no call-count qualifier: they count decode BLOCKS,
and one model call emits zero to many of them (a single call routinely
requests several tools; most calls emit no reasoning at all), so the
session's call count was a false per-slice tally. Measured on a real
236-call session: 236 calls produced 302 tool-call blocks, and 1078 of
3037 calls emitted no reasoning. TTFT and tools keep their counts — those
happen exactly once per call/run.
`timingOf` narrows the new optional scalars without materializing absent
ones, and the fast path applies the same non-negative gate as the slow
path so a hostile bucket cannot slip through.
The right Sidebar's Context panel split its three main cards into three
columns, so a pane too narrow for three folded into a ragged 2+1: a hole
under the composition card and the trend card alone on its own row. The
conversation tab read differently, from the same component.
Drop the per-host arrangement: both hosts now use one two-column row
(composition over trend in the left column, the browser beside it and
stretched to the pair's height). The panel's row simply folds to a single
column at the shared 360px floor instead of splitting across two rows.
The head row is untouched, so the panel still shows its two donut cards.
DeepSeek reprices the Flash series effective 2026-09-10 12:00 BJT
(off-peak per 1M tokens: hit ¥0.02/$0.003, miss ¥1/$0.15, out ¥4/$0.6;
peak at 2x). Between the V4.1 Flash launch and the V4.1 Pro release
every Pro request is routed to V4.1 Flash and billed at the Flash
rates, so pro shares the flash triple until V4.1 Pro is priced — swap
that one reference when it ships. The stats-board tooltip names the
flash row deepseek-v4.1-flash, and both locales note the interim
routing. Fold logic is untouched: the substring family match already
lands v4.1 spellings (new fixture), and the peak windows are
unchanged. Logs spanning the cutover estimate entirely at the new
table — the cell is a reference estimate by contract.
The supported range (dsh 0.1.2-rc.1+) spans three durable-log generations, and the fold only read the V0 spellings — so on 0.1.5-alpha.1 sessions the system prompt priced 0, TTFT/generation stayed 0, and startSeq/endSeq replacements appended instead of replacing.
Read SHAPES, never a detected version (the probe can be wrong: a healed profile mirror may name another release than the running harness):
- system/message surface nodes feed systemTokens plus a bounded systems list (the harness's "last nonempty surviving system" rule), replacing the V0/V2 request/header.header.system envelope figure; the browser resolves the prompt in force per step and fetches its text from either event shape.
- The first token comes from the embedded assistant stream (assistant/message.data.stream / assistant/attempt.data.stream) when the log carries no V0 assistant/chunk deltas, mirroring dsh-llm's assistantStreamFirstTokenTime.
- Replacement endpoints accept both startSeq/endSeq and start/end.
- tool/ptc-dispatch books nested file ops alongside tool/code-dispatch.
Statistics were differentially verified against the harness's OWN projection values on real V0, V3-native, and V3-migrated logs: system/tools/message tokens, request and turn counts, TTFT, generation, tool time, and every billed cost bucket match exactly.
The new state fields are additive-optional, so stateVersion stays 15: a bump would orphan the key for idle sessions (the #37 regression).
The hover-underline/click jump the plugin wired onto the harness chat
stats row conflicts with the reworked stats-line detail view shipping
in dsh 0.1.5. Drop the composer-dock entry, its styles, i18n keys, and
tests wholesale - the stats line renders untouched again.
Closes#53.
A trackpad swipe past either edge of the trend chart's horizontal scroller
chained into the page and the browser read it as its history gesture, so a
few scrolls in the Context tab could leave the whole app for another page in
the tab's history (back at the left edge, forward at the right).
Two layers, matching what the harness already does for its own horizontal
scrollers: `overscroll-behavior-x: contain` on the chart scroller and the
agent graph's stage stops the chaining (Chromium/Firefox), and overscroll.ts
cancels the horizontal-dominant wheel the scroller cannot consume at either
boundary for WebKit, which still navigates on horizontal overscroll with the
property set (WebKit bug 240183). Vertical-dominant gestures are untouched,
so a wheel over the chart still scrolls the page.
A title-row Adaptive switch redraws the chart against the bars currently
visible in the scroller instead of the whole retained log: the axis follows
the scroll and pane resizes, so the first step's system prompt + tool schemas
(a spike far off screen) no longer flattens every bar the reader is looking
at. Per-bar totals scale total mode, the diverging arms scale delta mode, and
the window reads the exact 14px column + 2px gap grid, so a column parked in
the gap never joins it.
The switch reuses the Context Browser's DNA-toggle chrome and stays
mount-local (no settings write); off keeps the whole-log scale. A window
carrying no figure of its own — an empty history, a zero-width pane, or an
all-zero delta window — falls back to that scale, and unchanged maxima
re-render nothing.
The "✂ marks compaction/prune, Step/Turn switches granularity" caption
restated controls that are already visible in the card's own title row,
so it only crowded the header. The subtitle span now renders solely for
the transient focus state ("Showing {cat} only"), and the unused
trend.hint key leaves both dictionaries.
A snapshot push can drop the band under a resting pointer (compaction,
tail slide out of the served window) without a mouseleave, and the dead
key exact-matched nothing: the whole strip sat dimmed with nothing lit
— the very state the open-category pin's breakdown gate rules out.
Honor a band hover only while its key names a rendered band, so the
mirror/pin fallbacks resume underneath.
Also correct the hover-link comment: DNA hovers stay on the bar and
never report back to the overview (the link is one-way in DNA mode,
as the existing test already pins).
The browser's accordion already focuses the Context trend chart on the
open category; the composition bar now holds the same highlight as a
resting state — category mode lights the category's segment, DNA mode
lights the category's bands as a group (the parts' group key). A
pointer hover overrides the pin and the pin resumes on leave; the pin
drops while a previewed/picked step holds nothing for the category, so
the bar never reads all-dimmed with nothing lit. No tooltip floats for
the pin: category mode mounts no tip slot, and the group-keyed pin
exact-matches no DNA band.
A title-row toggle redraws the composition bar as one band per context
item — the system prompt, every tool schema, every surface message — in
prompt order (the header prefix first, then the message flow by seq),
the order the model reads the context and the order items joined it.
The header epoch's own seq is bookkeeping (dsh logs request/header at
dispatch of the first request using it), never an ordering key: a
mid-session refresh must not park the schemas behind the tool results
they describe.
Bands floor at 0.35% of the occupied region so a tiny item stays a
hoverable filament (tooltips still report true shares); hovering names
the item with its time, a mirrored category hover lights the category's
bands as a group, and clicking a band opens its category + element row
below, scrolling it into view.
The trend bars multiplied every category by a per-request anchor factor
(provider prompt / heuristic total). That factor drifts with the heuristic's
ratio error as the context composition changes, so categories that never
change — the system prompt, tool schemas — plotted as steadily growing
(1668→1789 on a real log whose fold value is a constant 1851), and the delta
mode invented non-zero changes for them. The same screen already shows the
raw figures everywhere else (step detail, browser, overview), and dsh's own
token-meter breakdown projection serves un-rescaled heuristics per request,
so drop the anchor: segments, axis, tooltip and the focused view all plot the
fold's figures directly. Provider usage stays visible in the step detail's
Actual Prompt / Output / Cache chips.
A mount could race the declared inject (a watch rebuild remounts the
Context tab and the /context modal before the injected fiber re-fires),
and the fetchers snapshot the absent face once — the browser then stuck
on the static degradation for its whole lifetime, with no details
renderable in any category. The face now lives behind a
subscribe+useSyncExternalStore seat (useHistoryFace), and both
components rebuild their fetchers when it lands or is revoked, so an
open section auto-fetches without any user action. The /context modal
also gains the session image loader the tab already wires, so
attachment cards stop degrading to metadata-only there.
Expanding a category in the Context browser now plots every trend bar on
that category alone: the axis rescales to the category's own max (provider
anchor preserved) in both total and delta modes, the hover tip names the
category, and the card subtitle reads "Showing {cat} only" until the
category collapses. The /context modal keeps its unfocused chart.
The strip label is now the bare turn number (the block's hover title
keeps "T{n}"). Without the prefix, one/two-digit turns fit the 10px
base font at the fixed 16px column pitch with no shrink; the uniform
shrink now kicks in from three-digit turns (7px) and floors at 6px for
four-digit ones.
At the fixed 16px column pitch even a single-digit "T1" at the 10px base
font grazed its neighbour, so the collision chain hid every other label
(the blank T2/T4 blocks in the turn strip). Keep every label on the
single line: the render pass now picks one font size for the whole
strip — the largest at which the tightest adjacent label pair still
clears the gap (analytic glyph-width estimate, no measurement, 6px
floor) — and the measured chain in updateTurnLabels stays only as the
last-resort guard past the floor.
Every client file reached hooks and element types through the central
require seam (src/client/react.ts) and spelled them React.useState /
ReactNS.ReactElement. Import the members directly instead — named value
imports for hooks, inline type-only imports for element types — and let
the bundler own the single require('react'), which is exactly what the
platform module table answers.
This needs JSX off the classic runtime (it required a React identifier
in every tsx scope): tsconfig moves to jsx: react-jsx, so the bundle
sources jsx from 'react/jsx-runtime', already seeded by every supported
harness (verified against the dsh-v0.1.2-rc.1 platform.ts). The bundle
smoke's fake module table gains the specifier to stay a faithful shell
stand-in. h() survives as a local createElement alias at the slot
render-prop seams and the lightbox portal imports createPortal from
react-dom directly.
Verified: the built client.js requires react, react/jsx-runtime, and
react-dom exactly once each; the compat lane (bundle smoke + the
real-baseline matrix) passes against the fresh artifacts; the full gate
stays green with per-file coverage at 100%.
The File Activity card's hover-underline + click-to-open affordance died
with the harness connection refactor: the hostDescription.canOpenPath
fact and the connection.api.host.openPath RPC that the capability probe
and opener read no longer exist on any supported harness line, so the
probe answered false everywhere and every file name rendered inert.
Re-point both at the session controller's workspace-opener remotes
(session/canOpenWorkspacePath / session/openWorkspacePath) over the
generic '/api' channel — the same Connection RPC the split timeline's
detail channel rides — with the wire contract proved against a live
host: `args` must be a plain object keyed by the remote's declared
parameter names (an array is rejected host-side), and the capability is
now an RPC round-trip instead of a synchronous host fact, so it lands
in view state behind the harness's own isLoopback gate; a probe that
settles after unmount drops its answer. The shared connection-RPC
resolution is deduped into rpcCallOf, which makeDetailFetcher reuses.
The File Activity card derived its ops client-side from the served tool
nodes joined with the conversation WINDOW: a call whose arguments had aged
out of the loaded tail named no target and vanished from the card silently,
so the "up to latest" scope actually covered only the chat window. The join
was also the only meta/subCalls carrier, and the per-call op assembly was
written twice (top-level and Code-Mode nested paths).
The derivation moves to the host fold, which sees every lifecycle event with
its full payload: `tool/call` arms name + raw arguments (the pending-call
map's entries grow `argsRaw`), `tool/result` books the ops off the armed
arguments plus the result's presentation meta (grep's by-file match groups,
glob's path list, read's exact window), and `tool/code-dispatch` buffers
nested Code-Mode ops under their run_code root until the parent result
flushes them with `parent` + the program description. The log is bounded
(`maxFileOps`, default 400) with a `fileOpsFloor` coverage stamp, rides the
detail payload (and the inline value on channel-less hosts), and is plain
JSON end to end. stateVersion bumps to 14; cached rows refold from the log
to rebuild the op log.
The per-call parsing is ONE shared module (shared/fileOps.ts — opsOfCall
plus the kind/path/delta/read-window/search-attribution primitives),
consumed by the host fold and by the client's inline-generation fallback
(the legacy window derivation stays for old hosts, now through the same
parser, closing the duplicated assembly). The client aggregates + filters
(`activityOfOps` joins the archive's `gone` stamps at render time; nested
ops scope by their parent result's seq, matching the legacy node-level
rule); the card, its chips, rows, deltas, and locate bridge render
unchanged.
Verified: 1142 tests with per-file 100% coverage (new parser matrix,
fold-ops, aggregation, end-to-end through the real registry + detail
endpoint); the compat vocabulary probe gains tool/code-dispatch (present at
the 0.1.2-rc.1 tag). Live on the real deployment: the big session's card
grew from 4 window-bound files to 37 files / 135 ops spanning every turn,
and folding the real Code-Mode logs books nested ops with parent + program
stamps (e.g. 136 nested reads across one session, errors flagged).
Known upstream boundary (unchanged): `tool/code-dispatch` persists no
result meta, so nested reads keep the limit-estimate footprint and nested
searches the call-target form until the harness carries meta there.
The contextTimeline projection value rode every harness delivery channel
WHOLE: each session.list row, every control baseline, every follow
snapshot, and a full-value broadcast on every state change (with a
full-value schema parse per push). The heavy collections (request records,
context events, the served surface window, the removed-node archive) are
only ever rendered by an OPEN Context tab/modal, so they moved off the
wire value.
Wire head (buildTimelineHead, ~1KB): the composition breakdown, the stats
board's precomputed counters (turns/steps/injects/compactions/prunes), the
newest request's billing summary (the headline's derived anchor), the
timing/cost totals, and `detailRev` — a revision marker that bumps exactly
on detail-mutating folds, the open tab's refetch signal.
Detail channel (host/detail.ts): the pre-v0.9 generic Connection RPC shape
returns as one narrow endpoint, `/dsh-context` `detail`, serving the
collections off the registry's live fold state (`stateOf` — no second
fold, no private copy). A nested `ctx.inject(['connection', 'sessions'])`
arms it load-order-free; a deployment whose faces never compose keeps the
inline wire value unchanged. The baseline-gate fallback path is untouched.
Client (client/timelineSource.ts): one per-session detail store shared by
the tab and the /context modal — fetch on open, then debounced
trailing-edge refetches whenever the pushed detailRev outruns the served
detail (single-flight, latest-wins by revision, refold reset, failure
backoff, absence stops trailing). Detail-driven cards show a pending/retry
note instead of a misleading empty state; the counters and headline render
off the head immediately. Both wire generations are reconciled per value
(new client + old host renders the inline value untouched; old client + new
host degrades to empty charts in the stale-watch window, never a white
screen). The chat-jump relay holds its one-shot pin until the detail lands.
Fold state keeps the full shape: no stateVersion bump, no cached-row
invalidation (the slim view is recomputed per read for cold checkpoints).
Compat matrix gains the detail-channel seam probes (HostConnectionRpc,
the browser caller, the registry stateOf), verified against the
0.1.2-rc.1 tag.
Measured over 263 real session logs from a live deployment, plus live RPC:
- per-session wire value: 84–150KB (top sessions) -> 0.6–0.9KB (~100-165x)
- all sessions aggregate (session.list rows / control baselines):
4.4MB -> 0.2MB (~22x)
- per-client push traffic over one big session's lifetime:
157MB -> 1.8MB (~87x), and the per-push full-value zod parse shrinks
by the same factor
- detail payloads now flow only for the session being viewed, only while
its tab/modal is open (120KB measured live for the biggest session);
background sessions cost zero detail traffic
Verified visually against the running harness (big/small/cold sessions,
a live turn refreshing in place, past-step reconstruction, header epoch
and tool schema reads, the /context modal, the Agent network card) with
byte-identical card output. 1110 tests, per-file 100% coverage.
At apply time the host probes the running harness's version (the
dshHomePath-anchored profiles mirror first, the plugin's module anchor
second) and compares it against BASELINE_DSH_VERSION 0.1.2-rc.1 — with
release > rc > beta > alpha ordering at equal X.Y.Z. A harness below the
baseline gets fallback projection units (both registry contract
generations) that fold nothing and serve zeroed data plus an
`unsupported` wire record; the client keeps rendering the blank cards
and pops a per-session-dismissable modal naming both versions and urging
the update. A detection failure or an unparseable version fails open, so
a probe misfire can never blank a working deployment.
The harness stats row (the composer dock's own `stats` entry) gains a
hover underline and opens the Context tab on click or Enter/Space. The
plugin mounts an invisible sibling entry on the officially plugin-facing
`conversation.composer.dock` seat; its anchor spots the single stats row
beside it inside the slot anchor, decorates it in place, and re-spots it
on dock churn (the row mounts only after the first stats exist, and
tooltip bubbles come and go beside it). A drag-selection click is
ignored so copying the numbers never switches tabs.
Every shape without a spotless row degrades to a silent feature-off:
a missing dock seat, ambiguous candidates (other plugins' entries,
crash faces, non-HTML markup), and rows that throw mid-wiring — and
unmount strips every mark and listener. The seat is pinned in the
compat matrix (SLOT_SEAMS) and the bundle smoke counts the new entry.
The install/runtime compatibility floor moves from 0.1.1-rc2+ and
0.1.2-alpha2+ to 0.1.2-rc.1+. The 0.1.1 line and the 0.1.2-alpha previews
leave the support matrix, so every pre-0.1.2 fallback they existed for is
removed outright:
- client history: drop the legacy `connection.api.sessions.history` face,
the `result`-nested envelope, and the `events` rows field — the gateway
`remote.session.page` face and its ClientResult envelope are the only
served shapes.
- client image loader: drop the pre-0.1.2 `conversation.resolveImage` face;
`uiConversation.imageUrl` only.
- client node join: drop the pre-0.1.2 `useSession.nodes` seat;
`useChat`'s ChatSnapshot `legacy.nodes` only (UseSessionLike removed).
- tests/baselines.ts: single baseline `v0.1.2-rc.1`; HostSeam dropped (the
registry generation hard-codes header-passing init and seq-0 eager
seeding, now mirrored with the exact `init(header, inheritedEventCount)`
arity); ClientSeam slimmed to the consumed probes.
- deps: devDeps re-pinned to the 0.1.2-rc.1 generation (types compile
against the real rc.1 surfaces); cordis ^4.0.2 and schemastery ^3.18.2
per the rc.1 peer requirements; dsh-scope/dsh-invariants declared as dev
deps; peer floors >=0.1.2-rc.1; dshReleases table trimmed.
- release workflow fetches only the dsh-v0.1.2-rc.1 tag; docs updated
(AGENTS.md, README, docs/compatibility.md).
- verified against the rc.1 sources: the tool-attribution map (names and
packages, incl. the documented `subagent_fork` alias), MCP naming, the
page/imageUrl/legacy.nodes faces, and the registry contract.
The step-start → assistant-message slice reads off the stream now: the
model wait (TTFT, step start → first token) and the generation (first
token → message), folded from assistant/chunk — only the open step's
first non-empty text/reasoning/tool-call delta stamps the slot (the
session-stats rule). Calls with no recorded token stay unattributed in
the overhead residue. `lmMs` leaves TimingTotals for `ttftMs` + `genMs`,
the stepStart slot gains the stamp — stateVersion 12, rows refold.
fmtShare rounded to whole percents with a <1% crumb floor, which bunched
everything under 1% together and hid real differences in the two donut
legends. Shares now carry one decimal (73.8%, 26.1%), the crumb floor
tightens to <0.1%, empty slices read 0.0%, and the 100% cap is unchanged.
The context browser read the system row's item count from
`header.systemTokens`, a field only the post-#37 metadata wire shape
carries — but a host still running the pre-#37 view (a stale watch
build, an app not restarted since the upgrade) delivers content-bearing
epochs where the system TEXT rides the value instead. The row then read
0 项 while the ≈tokens figure still rendered, because the two come from
independent projections. Diagnosed against a real session: the cached
projection row was pre-#37 shaped, holding a 6261-char system string
(≈1570 tokens shown, count 0).
`headersOf` now normalizes both generations: unpriced legacy entries get
the meter's own heuristic, priced entries and new-shape values keep the
zero-copy pass-through. The heuristic (ceil(chars/4) + 4) moves to
shared/estimate.ts so the host fold and the client boundary price
identically. Regression tests cover the boundary matrix and the browser
row through the real call path; the #37 slimming itself is untouched.
The fixed backdrop masked the whole viewport; inset it by the sidebar
track parsed off the app frame's inline grid template (the sole inline
gridTemplateColumns in the dsh client, identical on every supported
baseline), so the dialog centers over the main column and the sidebar
stays visible and interactive. An unresolved frame degrades to the
full-viewport mask.
The measure runs in a layout effect (no 0-inset first paint) and, while
open, a MutationObserver on the frame's style attribute follows sidebar
drags, collapse toggles and narrow-viewport re-solves. A per-baseline
compat probe pins the frame seam.
The contextHeaders projection served full prompt/schema content through every harness channel (session.list, control baseline, pushes, checkpoints), so the initial payload grew with sessions x epochs x content size (issue #37). The projection now carries epoch metadata only; content lazy-loads per epoch via a seq-anchored history read; stateVersion stays pinned at 1 with a v1-tolerant state schema and a normalizing view so no cold session is ever orphaned. Headers wire -97.8%, list projections block -81.8%, checkpoint writes -82% (new sessions), host heap -92%, browser projection memory -98%, checkpoint CPU -90%, schema stringification 52x.
Hovering a legend row or a ring segment now lights both: the shared
hover key dims the other slices to the stacked bars' 0.35 idiom and
tints the active row. A hover with no painted arc (a zero slice) tints
the row but leaves the ring at rest. The legend names bolden, and the
token buckets get clearer names and colors: Cached Input / 缓存输入,
Uncached Input / 未缓存输入 (violet, no longer blue-next-to-blue with
Output), Cache Hit, Cache Write.
One-line rows squeezed the label, the quantity, and the bold share
together, so long names wrapped mid-phrase beside right-aligned figures.
Each row now splits into a primary line — the color dot and the name,
with the share closing a fixed right column — and a muted secondary
line that drops the absolute quantity under the name. The timing rows
move the call count out of the label into that secondary line
('4m 0s · 10 calls'), '(incl. reasoning)' becomes the output row's
qualifier, zero-figure rows dim whole, and names stretch across the
primary line with ellipsis only as a last resort.