The TrendChart layout effect has no deps and dispatched setEdges on every
render. Steady state survived via React's same-value eager bailout, but a
granularity switch genuinely flips the fades (narrow turn chart has no
overflow), and the programmatic scrollLeft assignment fires an async scroll
event whose dispatch leaves the fiber with pending lanes — the eager
bailout is skipped, every commit's effect enqueues yet another update, and
the queue grows without bound until React #185 (maximum update depth)
throws and the slot's error boundary blanks the tab.
Mirror the fades in a ref and never dispatch when the values are unchanged,
killing the cascade at the source instead of relying on bailout internals.
The real-React repro now emulates layout well enough to catch this class:
scrollWidth follows the bar count and programmatic scrolls dispatch an
async scroll event, so the old code crashes the test and the fix passes.
Tooltips and detail headers now read 'Turn 44 · Step 5' (step mode) and
'Turn 44 · 5 steps' (turn mode) instead of the terse 'T44 · step 5';
Chinese uses the natural '第 44 轮 · 第 5 步'. The compact turn strip
blocks keep the short 'T44' label (14px columns cannot fit 'Turn').
Returning from turn to step mode kept the stale left edge (the narrow
turn chart had clamped the scroll position), so the newest bars were off
screen. The layout effect now detects a granularity change and re-runs the
first-mount anchor — both directions land on the right edge. Plain
re-renders (polls) still respect a scrolled-away position.
Mounts the built client bundle with actual react/react-dom inside jsdom,
feeds a 460-request snapshot through the RPC, and toggles step -> turn ->
step — the flow reported to black-screen the tab. Renders cleanly, so the
black screen is not in the render path (layout-effect suspicion remains,
needs the real console error to pin down).
The tooltip is now derived from the shared hover key instead of internal
pointer state: hovering either a segment or its legend chip brightens the
same segment, highlights the chip, and shows the same tooltip centered on
the segment (percentage positioning, no measuring). Moving the pointer
within a segment no longer repositions the tooltip — the centered
placement is consistent for both directions.
The TS port passed the event ENVELOPE (data) instead of the model-visible
message (data.message) for assistant/message and tool/result — every such
node was priced at a flat 4 tokens (role framing only), so tool-heavy and
assistant-rich sessions were massively undercounted in the Current
composition. This is why the overview (20.4k) disagreed with the real
usage. Mirrors dsh's deriveEventMessage exactly now; empty-content
assistant messages price 0; tools total uses dsh's whole-array price; the
overview additionally shows the last provider-reported actual prompt
alongside the estimate.
The fold capped the timeline at a step count, so long sessions lost
older turns entirely in turn granularity. When the step bound is
exceeded, the timeline is now trimmed to the newest 300 WHOLE turn runs
(1500-step backstop for pathological many-step turns) — a turn is never
cut in half, and turn mode always shows the full recent turn range.
The harness session log itself has no event cap, so this was the only
limiter.
The Current composition stacked bar and the legend below share one hover
state: hovering a segment brightens it and highlights its legend chip
(bold text + ring on the color dot), and hovering a chip highlights the
matching segment. The mini bar in the request detail is unaffected (no
legend there); the tooltip keeps working.
Turn aggregates no longer stretch to their step span — every bar keeps
the fixed 14px column in both granularities, and the turn strip blocks
match (uniform in turn mode, step-spanning in step mode), so bars and
blocks stay aligned 1:1.
The Messages list renders newest-first by reversing the seq-ordered
surface; that ordering is now pinned by a test. Each node row additionally
shows the source event's timestamp when the host provides one — the host
now stamps every surface node with ev.time (no new data source, just
passing the existing event time through).
A turn aggregate in the detail panel is now labeled 'T{t} · N steps'
with a 'last step' tag instead of a bare step number, making it clear the
breakdown shown is the turn's final step (the record the bar carries).
Tooltips on turn bars use the same step-count wording; step records are
unchanged.
The strip's min-width clamp silently widened single-bar turn blocks
(14px -> 16px), shifting every following block out of column. Removed the
clamp, and turn-mode bars now span their steps' columns: bars and strip
blocks share the same span math, so they align by construction in both
granularities (step records carry stepCount, which also feeds the strip).
On first mount (and on session switch, via a sessionId key remount) the
chart scrolls to the right edge so the latest steps are visible
immediately; afterwards it only sticks to the end while the user is near
it, so scrolling back into earlier turns/steps is never fought. The edge
fades follow the scroll position as before. The render test's fake React
now keeps useRef stable across renders (like React), which is what the
effect needs to see the container.
The chart sliced to the newest 80 requests, so anything older was never
rendered. All requests the host provides (capped at 160) are now rendered
and reachable by horizontal scrolling; the scrollbar is thicker and
higher-contrast, edge fades appear whenever more history sits beyond the
viewport, and the hint mentions the sideways scroll.
A Step/Turn segmented control in the trend card header switches the chart
granularity. Turn mode collapses each turn into a single bar showing its
LAST step's record (the context state when the turn finished); the turn
strip, hover linking, and detail panel all follow the aggregated bars.
Each turn is now a colored block whose width spans exactly its bars'
columns (same grid math), so the partition lines up with the steps above.
Consecutive turns cycle a palette; the T-label sits on the block with a
tooltip fallback for narrow single-bar turns. Hover highlighting
(bidirectional with the bars) is unchanged.
The turn tick row is now an interactive strip: hovering a turn block
highlights that turn's bars in the chart (tinted columns), and hovering a
bar highlights its turn block — both follow one shared hover-only state,
so nothing glows by default. Leaving the strip or the chart clears it.
The Current composition stacked bar (and the request-detail mini bar)
now shows a theme-native tooltip on hover per segment — category, token
count, and share of the total — positioned at the pointer and clamped
inside the bar. Replaces the native title attribute; the wrapper keeps
the tooltip outside the rounded/clipped stack.
Hovering a trend bar links the detail panel to that request immediately
(hover wins over the pinned selection, which takes over again when the
pointer leaves; both fall back to the newest request). The hovered bar
gets a dashed outline, and the chart clears the preview on mouse leave.
- src/host.ts: typed fold model (Snapshot/RequestRecord/SurfaceNode/
ContextEventRecord), local drift-free service contracts for
connection/sessions/sessionQuery via declare module augmentation
- src/client.cts: typed client bundle (React types via @types/react,
locale/slots/connection contracts, typed component props); CJS-flavored
so esbuild keeps require/module.exports despite type:module
- tsconfig.json: strict, noEmit, verbatimModuleSyntax (runtime-free of
imports in the client bundle is enforced by the type system)
- scripts/build.mjs: esbuild transpile for both halves, loader wrap and
smoke checks unchanged; npm run typecheck added to the test suite
- note: @deepseek-ai/* service type packages publish broken dependency
chains on npm (dsh-paths 404), so local contracts replace them
- bars use a fixed 14px column width, so sparse histories no longer
stretch and dense histories scroll horizontally instead of compressing
- the chart sticks to the newest bars unless the user scrolls away
- a turn tick row under the bars groups consecutive requests of the same
T into one labeled range (T1 spans its bars with a connector line)
- test-client now renders the view tree with a stateful fake React and
asserts bar count, scroll container, and turn-range geometry