The cell sits left of Images and counts the tool calls whose result is
live in the current context: one tool/result folds to exactly one tool
surface node, so the host derives the figure at view time from the live
surface (no persisted counter, no state-version bump). Calls still in
flight and results compacted or pruned out of the surface are excluded.
* feat: add PR checks workflow for automated testing and validation
* feat(client): hide the composer while the Context tab is active
The view ring mounts one view at a time, so the tab's .lc-root existing
inside the shell scrollport is exactly 'Context tab active' — a single
:has() rule hides the sticky composer seat and hands its height to the
dashboard. display:none keeps the seat mounted, so drafts survive the
round trip.
Interactive takeovers (approval / question / plan-review panels) replace
the bar inside the same seat; keep the seat visible while one is present
so a pending decision is never hidden behind the focus mode.
Closes#11
* ci: build before test in PR checks
Client tests mount the built bundle (viewBed.ts reads lib/client.js), so
pnpm test without a prior build fails with ENOENT in a clean checkout.
ContextViewBody returned the loading frame BEFORE its displayRequests/
markers useMemos: when the projection value landed on the same mounted
instance (a pure timing race), the hook count grew between renders and
React threw #310, which the ErrorBoundary masked as a '数据读取失败'
card. EventList returned its empty branch before useRef/useLayoutEffect:
an empty -> non-empty transition in one instance at best corrupts the
hook list (React logs 'Expected static flag was missing') and at worst
throws the same #310 on builds that don't recover.
Both early returns now sit below every hook; data-less renders fall back
to empty collections so the memos stay unconditional.
Regression tests mount the built client bundle on real React in jsdom and
drive both transitions (loading -> loaded, events empty -> non-empty),
failing on a boundary catch or any hook-order console error.
Closes#12
Adds a display-mode pair (全量/增量, Total/Diff) next to the Step/Turn granularity group in the Context History card. Diff mode plots each request's per-category |change| vs the previous request, with the hover tip reporting the signed net change (Δ +1.2k / Δ −535.5k); Total keeps the cumulative composition and stays the default.
Original PR by @xieshuaix (delta toggle); reworked before merge: the on/off delta switch became a two-way mode control (mode: 'total' | 'diff' end to end), the two toggle groups were wrapped so the title's auto-margin push happens once, trend.hint was shortened/localized, and the fiber-slot/button-count tests were updated.
- drop the redundant node.cat === 'tool' guard (categoryOf always
returns 'tool' in the tool/result case)
- fall back to the rendered <skill_content> wrapper when node.tool is
unresolvable (trimmed window, replay), so a skill load never silently
degrades to an ordinary untagged tool row; the wrapper only appears in
genuine skill results
- regression test for the fallback path, and that a plain tool result
without the wrapper stays untagged
A skill load via the `skill` tool returns the loaded skill's instructions
as a tool result. Keep it a tool result (that is what it is), but make it
findable: tag the node with the skill name (so the Context browser labels the
row `Skill · <name>`) and record an inject event so a `Skill 注入(name)`
entry shows in the Context Events card.
- fold: detect tool/result with node.tool === 'skill', parse the skill name
from the rendered <skill_content name="…"> wrapper, set node.skill, emit a
sub:'skill' inject event. Category stays 'tool'.
- browser: tool row shows the Skill · <name> tag when node.skill is set.
- i18n: add node.skillTag (zh/en).
- test: regression for the skill-tool result path.
The row joins the conversation snapshot and counts image blocks via the
same imageRefOf the expanded body uses; the chip (localized 'attach.image',
×N when several) shows only while collapsed — expanded, the image grid
speaks for itself.
vitest (esbuild) and Node 24 type stripping run the .ts files directly;
helper imports go extensionless like src. Typecheck stays scoped to src/
as before.
Every expanded element body is now a uniform stack of Section cards
(labeled head + body): prose carries the Raw/MD switch in the head, tool
calls/params/images/raw JSON share the same frame, and one blocks→sections
pipeline serves user/assistant/tool-result/injection bodies. Replaces the
floating-switch, bare-pre, and per-kind card idioms; drops the dead
lc-br-blk/lc-br-pre/lc-br-md/lc-br-rich-inline styles and the unused
RichCard/inHead/dim parameters. Element-row tool tags lose their inverted
chip and share the subtle injection-form tag style. Test bed: function
components now receive props.children like real React.
Fold counts image blocks (user uploads + nested tool-result images)
cumulatively over the complete session log, rides the wire snapshot as
an optional additive field, and the stats board shows it as a new cell
between prunes and cache hit. stateVersion 7 refolds cached rows.
Follow DeepSeek's peak/off-peak rule adjustment: Saturdays and Sundays
(Beijing Time) no longer distinguish peak windows, so the whole weekend
prices at the half-price off-peak rate. Peak windows are now expressed in
Beijing Time (weekdays 09:00-12:00, 14:00-18:00), equivalent to the old
UTC windows on weekdays. List prices themselves are unchanged.
Each fact gets its own labeled row: Raw (pre-normalization dims), Sent
(normalized dims + stored size, dynamic B/kB/MB units), Token (estimate).
Facts that dsh never recorded (e.g. no originalDimensions, no byte size)
simply leave no row behind.
The attachment grid becomes a two-column equal-width grid
(repeat(2, minmax(0, 1fr))) and every image card gains a token-cost
line (≈N tokens, with an explanatory tooltip) estimated through the
official DeepSeek image-size conversion on the stored dimensions.
The composition legend and tooltips now show the exact system/tools/
messages counts of the chat composer ring's click-open panel (the
token-meter's contextBreakdown projection) instead of the fold's own
sums re-anchored to the provider total: parts carry the heuristic count
(raw) for display beside the provider-anchored bar width (value), and
the message bucket subdivides into the four surface categories by the
fold's per-category ratios, always summing exactly to the official
message figure. Absent the projection (older harness), the fold's own
sums serve unchanged.
The token-meter heuristic prices image blocks through its generic JSON
branch (~40 tokens for the durable ref), while DeepSeek's vision model
actually bills 117-384 tokens per image by pixel dimensions. Port the
official docs image-token calculator (patch grid 14px, downsample x3,
384-token cap, minPixels 384^2) into src/shared/imageTokens.ts and price
image blocks with it in the fold; unknown dimensions keep the JSON
fallback. stateVersion bumps to 6 so cached rows refold.
The differential fold check replays a parallel image-delta surface so
image-bearing logs still match the official contextBreakdown exactly,
and adapts to the 0.1.1 wire.view contract.
The clickable turn strip added a focusTurn useState between granularity
and hoverCat, shifting hoverCat to slot 6 and toolFocus to slot 8; the
composition-link and tool-chip-bridge specs still addressed the old
slots.
Tool-result and call-bearing assistant rows now open with an inverted
mono chip naming the tool(s) (label/surface tokens swapped — black on
white in light mode, white on black in dark), multiple calls joining as
a breadcrumb (bash › write). The preview then carries the call's own
summary line: a bash-style `description`, else the target path of
path-taking tools (edit/read/write via file_path/path/filePath), falling
back to the reply text or the generic label. The redundant 'calls: …'
preview is gone — the chip says it once.
Both render the call as a card whose head names the tool (→ / ←) with
the argument count, the body lists arguments as name/value rows — the
type column is gone, the name sits in a narrow wrapping left column,
the value wraps in a mono right column (height-capped). The tool name
steps up to 13px mono. The tool result's raw lc-br-call block is
replaced by the same card.
The retained request log is bounded by maxKeptTurns/maxRequestSteps
(defaults 300/1500), not 160 as the comment claimed — a long session's
chart is thousands of bar nodes, and every bar hover re-rendered all of
them through ContextViewBody.
Extract the bar as a React.memo component (hover flips flags on at most
two bars; req/marker identities stay stable between projection pushes)
and memoize the parent's turn-aggregation and marker attachment so turn
mode stops allocating fresh bar props on every hover render. Bars carry
data-seq for identification now that the element key lives on the
memoized component; the fake-React test beds learn memo().
The callId→name map is looked up exactly once — when the result event
folds in — but entries were kept for the session's whole lifetime. As
persisted state, the map was serialized on every projection checkpoint
and shallow-copied by every fold step's lazy clone.
Rebuild the map without the consumed ids at lookup time (no dynamic
delete, per repo lint); consume-once holds it at pending-call size, so
the copy is trivial.
One job instead of two: the dsh-plugin-checker step now runs right after
checkout inside publish, dropping the needs: handoff (saves a runner
spin-up per release; the check still gates every later step).