Restyle the request detail header for a cleaner, more consistent look:
- even 8px gaps and center-aligned items on the single wrapping row
- time pinned right with tabular figures
- provider figures (actual prompt / output / cache) as uniform neutral
pills with tabular numbers, replacing the plain spans and the two
same-green accent colors
- marker chip tinted with the warn color, matching the event list chips
Drop the heuristic estimate from the step header; show the provider-reported
cache-hit share of the billed prompt (cacheRead / prompt, truncated to two
decimals) instead. The host folds each request's cacheReadTokens into its
record (stateVersion 9); the stats board reuses the same formula via a shared
format helper.
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.
Remove assertion restatements, act narrations and decorative section
markers; keep fixture math, harness-gotcha rationale and slot maps.
Fix stale comments: ctxSlots maps (trendMode shift), a dangling fixture
fragment, and the cardRig 'expanded via header' claim.
Apply the full-repo comment audit to the host half and shared wire types:
delete restatements and decorative headers, compress rationale comments to
one line, and fix stale references (agent-loop buildRequest, live-surface
image cell, five->four priced categories). Kept only contract notes,
invariants (plain-JSON precondition, shadow-seqs protocol) and verified
dsh-mirror pins.
The turn strip renders each label inside a nested `lc-turn-label` span
(added with the scroll-centering work), so the label assertions must read
through textOf instead of expecting a bare string. The scroll/layout
handlers also call updateTurnLabels (querySelectorAll over the scroller),
which the fake scroll elements must stub — an empty querySelectorAll keeps
the label-centering a no-op without touching the asserted behaviors.
Markdown cards render soft line breaks (a single newline now breaks the
line via white-space: pre-line — two newlines still separate paragraphs)
and long unbroken tokens wrap in place: the section card gets min-width: 0
so it shrinks below its content, the body adds overflow-wrap: anywhere
with horizontal overflow hidden, and code blocks keep pre-wrap with
overflow-wrap so long code lines wrap instead of scrolling sideways.
A failed tool result now leads its collapsed row with the chat's red
run-state dot (6px core, 10px halo) right after the chevron, so the
failure scans without expanding. The failure state comes from the fold's
`err` stamp OR the conversation snapshot's `isError` — the mismatch
matters: dsh only stamps the event envelope's `error` info
conditionally, while the snapshot block always carries `isError`. The
redundant ⚠ suffix on the tool-name chip is dropped (the dot marks
failures), and per-row conversation lookups are hoisted into one `conv`
for the merged condition.
The call half of an expanded tool result drops its argument-count badge
(the rows below already show the arguments); the head's right edge now
carries a run-state pill — green OK by default, red Failed with the
payload's terminal `exit N` code when the fold's `err` stamp or the
snapshot's `isError` marks a failure (exit recovered from the dsh
`[exit code: N]` marker). The payload card heads its line count before
the raw/markdown switch (switch stays at the far right), and its text now
renders through the same switchable prose anatomy as every other detail
section instead of the fixed dim raw view.
Rework the Settings \u2192 Plugins card onto the framework's own patterns:
collapsed by default like the official PluginCard (header button discloses
the controls in place, chevron rotates), and each preference row is the
official settings-row pattern \u2014 a selector pill opening the shared Menu
dropdown from dsh-client-ui-primitives. Picking an option writes
immediately through the settings scope (display preferences, like the
official Language row).
Add a second preference, defaultTrendMode (total/diff): the Context tab's
trend chart opens cumulative or per-request diff per the stored choice;
in-chart toggling stays mount-local and never writes back. The settings
store's choose() generalizes to set(field, value), with one prefsOf()
validator shared by scope sync and the local echo.
Register the dsh-context settings namespace on the Host (dsh-settings seam,
schemastery schema, optional composition) and claim its Plugin configuration
card in the browser (settings.plugin.item, keyed by namespace). The card
edits the default trend granularity the Context tab opens with; in-chart
toggling stays mount-local and never writes back.
Degrades cleanly: no settings provider -> no registration, no card, step
default; remote browser (memory mode) -> unavailable scope, no card.
The row carried the producer's gross shadow price, while the trend chart
visualizes the net drop. The shadow-price protocol guarantees the
replacement folds in synchronously after the metering event, so rewrite
the event's tokens to removed-minus-replacement at that moment; a
metering event left without a replacement keeps the gross price.
Plan mode includes a guidance section in every model request while
active, so its durable plan/mode event is a real context-composition
change and earns a neutral mode event row (own kind chip + filter).
A request header logged with reason 'resume' carrying a different model
is likewise a real switch made between sessions — record it alongside
the in-session 'change' ones.
A notice-form source already carries the producer's bounded one-line
account; record it as the event's detail and render the row as
title · producer · account, matching the dsh transcript row shape.
The dsh transcript treats a user/message as injected context whenever its
durable source kind is not 'user'; the fold's narrower check (plugin /
skill-invocation / has form) misclassified formless producers — goal
continuation rounds, team relays — as user messages, skewing the token
attribution and hiding their inject events. Keep the form check as a
fallback for foreign sources without a readable kind.
Mirror the dsh transcript's context provenance on inject events:
agent-instructions names its reconciled file paths, a plugin source its
plugin id, and any other producer (skill-catalog, ...) its durable kind,
so the events panel shows '目录更新 · skill-catalog' style rows like the
harness GUI does.
Each image attachment card now carries its own border (border-l1, 8px
radius); hover strengthens the frame to border-l2 and washes the card with
the interactive row hover token — the same affordance as the browser's
clickable category and message rows.
The attachment card's thumbnail click used to open the full image in a new
browser tab. Now the whole card (tile + metadata column) is one click
target, and clicking it opens an in-page preview modal mirroring the chat
history's ImageLightbox recipe: body portal via the module-table react-dom,
blurred mask on the same dsw theme variables, contain-fit image, circular
close control (IconCloseOutline16 from the seeded ui-primitives), Escape/
mask close, and focus restored to the opener. The ui-attachment package
itself is not seeded into the browser module table and cross-plugin value
imports are gated off, so the recipe is ported onto the plugin's lc-*
classes with fallbacks; load failures still retry on click.
The image count moves off the persisted whole-session counter onto a
per-node imgs field (absent when zero), and the view sums it over the
live surface — matching the Tool Calls cell's semantics: images of a
message compacted or pruned out of the context stop counting. The
persisted-state shape changes, so stateVersion bumps to 8 and cached
rows are refolded.
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.