The single 576-line global sheet grew with every card (trend chart,
browser, file activity, agent graph...), so every component change
opened the same shared file. Cut it mechanically into 14 cohesive
sheets under src/client/styles/ — one per component surface plus a
base sheet (root/card chrome, the lc-gran toggle, list anatomy,
error states, the composer-hiding rule).
Rules are byte-identical to the previous single sheet; index.ts
imports them in the original section order so the cascade is
unchanged (base first — later sheets re-tint the shared gran chrome
at equal specificity). bundle-smoke.mjs now asserts one style tag
per sheet instead of the single dsh-context/styles.css tag.
Drop the deleted `@deepseek-ai/dsh-client-runtime` from dsh.client.inject
and remove the preloaded-client-externals channel that died with it; mirror
0.1.2's platform module table (+ dsh-client-store) in the client bundle's
purity gate; widen the dsh peer ranges to >=0.1.0-rc.7 so 0.1.2 prereleases
resolve; pin the devDependency types to the newest published surface
(0.1.1-rc.2); the bundle smoke's fake ctx now honors inject callbacks.
The session-log divergence check runs under bun against src/ directly,
tracking the working tree instead of the last build. Also add
scripts/bundle-smoke.mjs (pnpm run test:bundle): the one check that
must exercise the built bundle — module-loader handoff, CSS channel
tagging, registration disposal and HMR style-tag semantics in jsdom
with real React.
Compress the CSS-channel note in tsdown.config.ts, the vitest lane
explainer and the release workflow header; fix the install-hooks header
(git only consults .git/hooks when core.hooksPath is unset); slim the
social-preview generator and bundle-patch comments.
Replace the hand-rolled esbuild script (scripts/build.mjs) with a single
tsup config that already existed but never ran: tsup re-bundles the config
into a temp .mjs, so import.meta.url pointed at the temp dir and the
package.json read failed, silently leaving no entries. Read the manifest
from process.cwd() instead. The host half now keeps @deepseek-ai/dsh-session
external (resolving to the harness's single runtime instance) instead of
inlining it; the client half keeps the __ModuleLoader__.load wrap.
Align deps with the official dsh convention: runtime harness packages
(cordis, dsh-session, client-ui-primitives, react) move to peerDependencies
with dev mirrors (client-only ones optional); zod stays a dependency;
exact rc pins become caret ranges (^0.1.0-rc.8) so rc9/0.2 releases don't
break installs. Drop the esbuild devDependency (tsup brings its own) and
add tsup itself.
The client entry's `module.exports = { name, inject, apply }` is the
closure-factory contract every dsh.client package's `./client` export
must use — the harness's __ModuleLoader__ wraps the bundled CJS in a
factory that provides `var module = { exports: {} }` as a closure local
(see scripts/build.mjs:73-86), so the reference is legitimate. esbuild's
parser nonetheless flags it because the .ts source lives in a
"type": "module" package.
`logOverride: { 'commonjs-variable-in-esm': 'silent' }` suppresses that
one specific warning without touching any other esbuild signal — a future
maintainer adding a real CJS leak (e.g. require() inside the entry) would
still see a different warning. No source, test, or runtime behavior
changes; the bundled client.js still ends with the documented factory
shape that the harness's loader consumes.
Side-by-side head row (3:2 flex, wraps when tight): plugin name, version,
and GitHub repo as whole-cell links (name/version -> GitHub/npm), with the
version/repo baked in from package.json via esbuild define. A once-per-page
npm registry check adds an 'Update!' badge + update-command tooltip on the
version cell whenever the registry release differs; failures stay silent.
Bilingual strings, cell-hover underline + brand border affordance.
All view components are now tsx JSX function components instead of
hand-rolled h() factories; the bundle externalizes the framework seed words
(react, @deepseek-ai/dsh-client-ui-primitives) so the loader module table
resolves the single shared framework instance at runtime.
Reuses shared ui-primitives where a clean fit exists (injection and
model-switch event glyphs -> IconPlusOutline16/IconBranchOutline16); the
bespoke data-viz chrome (charts, shared-hover tooltip) intentionally stays
custom but continues to draw the shared --dsw-alias-* tokens.
esbuild uses the classic JSX transform, so the emitted createElement trees
are isomorphic to the previous h() calls and the element-parsing tests, the
chart-render assertions, and the real-React repro all stay green unchanged.
Adds @deepseek-ai/dsh-client-ui-primitives as a type devDep and documents the
framework-reuse findings in docs/redesign.md §8.
Replace the custom /dsh-context RPC channel + 2s client polling with a native
contextTimeline session-projection unit: the host registers one pure
init/apply/view unit on ctx.sessionProjections (framework-driven, persisted by
the projection cache, streamed via session/projection push frames), and the
client reads the finished value through the framework standard
useProjection seat. Deletes the client stale-while-revalidate cache and the
local service type stubs; grows the modern shelf registration (locale) and
adds the published @deepseek-ai/* types as devDeps plus zod as the single
runtime dependency (wire schema, kept external in the host bundle).
Fixes tool-result node attribution to read the durable source.callId /
content toolCallId instead of the never-present block.callId (tool labels had
silently never rendered).
Retention is tightened to trim by whole turn-runs deterministically (bounded
state, no 1200<->1500 oscillation under incremental folding).
Tests now drive the projection unit directly (host) and feed the view through
a useProjection stub (client); all chart/render assertions preserved. Adds
docs/redesign.md (review + target architecture, phases 0-3 landed) and
docs/CLIENT_SURFACE.md (client-side audit).
- shared/types.ts: single wire contract (type-only) used by both halves
- host/: services (contracts), pricing (token-meter), fold (incremental fold
+ retention), snapshot (build + event turn/step attribution), index (RPC)
- client/: i18n, styles, format, categories, viewkit (shared deps),
cache (per-session stale-while-revalidate), components/* (one module per
component), view (composition root), index (plugin entry)
- first-screen: initial render seeds from the session cache, polling pauses
while the tab is hidden (resume + immediate fetch on visible), and a
transient fetch error never blanks already-visible data
- build.mjs: bundle:true for both halves (react external in the client
bundle); the client half is now covered by tsc --noEmit for the first time
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