Every runtime seam the plugin rides is source-identical from
0.1.7-alpha.2, so no code changes; the supported V4 baseline moves
from 0.1.7-alpha.2 to 0.1.7-rc.1 (the alphas leave the matrix). rc.1
adds the harness-side plugin peer-compatibility admission gate
(evaluatePluginCompatibility); the plugin's >=0.1.2-rc.1 dsh peers
satisfy it under the gate's includePrerelease check, so the bundle
composes with no exemption. The release workflow fetches the rc.1 tag
for the compat matrix and points the plugin checker at the npm next
dist-tag (0.1.7-rc.1).
The en/zh title and description are the canonical Plugins-page copy, not
a restatement of the package.json fields - the manifest values only
back the en fallback when a dictionary entry is absent. The en/manifest
equality assertions drop; the zh spec now pins that it is a real
translation (same fields, different copy) instead of only matching
en's shape.
The Host's package-meta reader serves each plugin's localized
title/description from the dictionaries exported under locale/ - the
same surface the official plugins use, so the Plugins page and the
settings inventory present dsh-context in the active locale instead of
the raw manifest fields. en carries the canonical copy (matching the
manifest name/description it overlays), zh the translation; the
exports pattern and the files list ship both dictionaries. A spec pins
the dictionaries' shape, the exports/files alignment, and the en/manifest
agreement.
The Plugins page's package cards draw each bundle's icon from the Host's
package metadata (manifest-relative SVG/PNG/JPEG/WebP, inlined as a data
URI), which we never declared - the cards fell back to the default
artwork. The Context emblem's single graphic source is now the
package-root icon.svg: the package.json "icon" field points the Host's
reader at it, and the client build inlines the same file through a
"*.svg?raw" channel (the bundled ContextIcon re-renders it at every
requested size), so the code carries no second copy of the paths. A spec
pins the file and the component in lockstep; the files list ships the
asset.
The Config-form jump stops settling for the card list: after the panel
entry's click, a bounded poll (40 ticks at 100ms, riding the same
scheduler contract the tests drive) follows the page's package load and
opens the bundle's page through the card's title control — the one
button inside the shipped data-plugin-package list item that is not the
enable switch, so the match survives every locale. A poll that finds
nothing (still loading, plugin absent, or the page already open on the
detail view, where the list is unmounted) silently degrades to the card
list.
The User preferences delta moves from deferred to shipped: one schemastery
entry schema serves both generations, the browser seat and transport per
line, the deliberate strict-key relaxation, and the settings.yaml legacy
import that carries persisted values into the profile entry. The README's
Settings section names where the card lives on each release.
The settings seam now names the browser half's faces per generation —
the keyed card slot (settings.plugin.item vs plugins.bundle.config) and
the transport service (settingsScope vs configForms) — each asserted
both ways against the tag's sources, like the sidebar seam, so a moved
seat cannot read as unsupported.
The preferences jump prefers the Plugins main-panel entry (matched by
its shipped aria-label) where the generation ships it — one click and
done, the page's internal view state has no external face — and falls
back to the settings-dialog choreography for the older lines. The jump
copy becomes generation-neutral (Open preferences).
The Config-form generation retired the settingsScope service and the
settings.plugin.item slot, which took the preferences card with them.
A second deferred inject — fired only where the configForms service
exists, so each line runs exactly one transport — binds the shared
settings store onto the dsh-context config form and registers the flat
card on the Plugins page's keyed plugins.bundle.config seat, alive only
while the Host serves the namespace (whileServed). Both seats render the
same six preference rows over the same store; the settings-section card
keeps its shape for the older lines.
The Config-form generation (dsh 0.1.7+) derives the Plugins page's live
form from each loader entry's own Config schema, so the entry schema must
be the schemastery one: cordis keeps validating the config: block through
its Standard Schema face, and the same schema now carries the six display
preferences as .volatile() fields (live-editable there, namespace = the
entry id). The fold bounds stay ordinary fields — their edit remounts the
entry, a preference edit commits volatile-only.
The .volatile() modifier ships only with the newer schemastery, so the
builder feature-detects it and the same bundle loads on the older lines
(3.18.2 predates the modifier). The devDependency bumps to 3.18.4 to
exercise the marked path in tests. Deliberate relaxations against the
replaced zod schema: unknown keys merge through, and integers are
expressed as min 1 + step 1.
The version.spec fixtures relied on clean CJS walk-up failure from the
tmpdir scratch tree, but Node consults NODE_PATH after the walk-up
fails - and a pnpm-managed environment (a `pnpm` binary running from a
sibling checkout, as on a dev machine with the harness repo alongside)
injects that checkout's virtual store into NODE_PATH. Every probe
package ABSENT from a fixture home then resolved to the ambient harness
install, and the four fall-through / degradation arms answered with its
real version instead of nothing.
Each scratch home now SHADOWS every probe package its failing probes
could reach with an entry that resolves at the fixture level but
answers no version (exports gate on the manifest subpath, versionless
own manifest on the entry ascend), so resolution never leaves the
fixture regardless of the environment. Two coverage-preserving additions
ride along: an entry-gone home pinning versionViaEntry's unresolvable
branch, and a stamped-slot merge test for the additive-optional blocks
record the timing tallies introduced.
The generation split (thinking / answer / tool args) already tiles its
window off the stream's block-start markers; the same markers now count
into additive-optional per-bucket tallies (reasoningBlocks / textBlocks /
toolArgBlocks), so the card qualifies each decode slice with the number
of blocks actually decoded - the honest per-slice counterpart of TTFT's
call count and tools' run count (one model call emits zero to many
blocks, so a call count there would be a false tally). Overhead stays
unqualified: it is the residue, with nothing behind it to count.
Both log generations carry the markers: the V0 chunk flood counts them
into the step slot alongside the spans, and the V2+ embedded stream
yields them from the same single pass that prices the split
(decodeSpansOfStream -> decodeTallyOfStream). Counts ride the split's
own first-token gate, and rows cached before the fields existed render
the duration alone.
dsh 0.1.7-alpha.2 introduces no breaking change for the plugin: the
vendored cordis 4.0.3 -> 4.0.4 bump is version-metadata only, the
durable log stays at V4 with an unchanged event vocabulary, and every
seam the plugin rides (session-projection registry, right-Sidebar tab
and navigation faces, connection RPC, settings namespace, agent-loop
pre-step, ui-primitives icons and markdown labels, the plugins.bundle
config slot) is source-identical or additively extended. The remaining
deltas are internal to the harness: an optional turnWindow on history
page requests, client-module batch-failure recovery, and host-side
tool-result retention (the already-documented deferred image-offload
area).
Per the one-representative-per-generation convention, replace the
alpha.1 baseline with alpha.2 across the declared releases, the seam
matrix, the release workflow's tag fetch, and the compatibility page.
The compat matrix now boots the real alpha.2 sources on cordis 4.0.4.
Three per-event costs leave the fold: every state-changing event shallow-copied
all seven retained collections, every event re-scanned the retained requests to
count turn runs, and every tool call stringified its arguments even when the
tool can never row a file op. Cloning now follows the branch's own mutation
list, the run count rides an incremental ledger (recomputed only where a trim
replaces the array), and raw arguments are kept only where they are read.
Measured over 279 real sessions (282,783 events): the fold drops 16.8% corpus
wide, ~51% on V3-shaped logs and ~57% on sessions at the retention caps, with
state output field-identical apart from the new optional `turnRuns` field and
the intended `argsRaw` omission. `stateVersion` is unchanged, so cached rows
keep parsing and stay usable.
The Windows check rode along: `path.relative` returns an ABSOLUTE path across
drives, so the old `..`-prefix test read a foreign drive's module as a child of
the plugin's own tree and degraded version detection; `!isAbsolute(rel)` closes
it.
Review follow-up on top of the original six-file change:
- an absent `timing` slot stays absent. The selective clone materialized
`timing: undefined`, which is not losslessly JSON-serializable, so
`snapshotJsonValue` rejected the whole checkpoint: every projection-cache
write for that session was skipped, and a log ending on an unpaired
`tool/result` never got a cache row at all — every reopen then paid the full
cold fold this change set aimed to cut;
- the no-`undefined`-member rule is now pinned on every intermediate fold
state, not only the last one — the gap that let the above ride through a
green suite, since reading the slot cannot tell an absent key from an
`undefined`-valued one;
- the "which tool needs its raw arguments" gate moved beside the file-op
parser (`opBearingTool` / `rawArgsNeeded`), so the fold no longer restates
the tool list.
Verified: `pnpm run lint:fix && pnpm run test && pnpm run build` green (1763
tests, per-file coverage 100%), plus a field-level diff of the fold over
282,783 real events (no divergence beyond the intended deltas), the
incremental ledger cross-checked against a full scan on every event including
both trims, and a frozen-predecessor run proving no in-place write.
Co-authored-by: Nwflower <1679659@qq.com>
npm's 0.1.5-rc closure is broken upstream: dsh-web-app 0.1.5-rc.3 was
published while the dsh-client-ui-sidebar-documentpreview rc.3 it
requests never was, and every ^0.1.5-rc range (including `latest`'s own)
resolves to it — so installing the checker's `@deepseek-ai/dsh@latest`
fails with ETARGET before any check runs. Pin the checker to `alpha`
(0.1.7-alpha.1), whose closure is complete and which is the release this
cut declares compatibility with.
Add the V4 baseline (session format 4, cordis 4.0.3): the fold's switched
event families are unchanged, so its entry rides the V3 seam face with a
per-generation settings surface — the `register` face and the retired
`settings.plugin.item` slot are asserted BOTH ways (present through V3,
absent on V4+), and the modal dock probe now pins the sidebar-leading
grid track the measure actually reads instead of one generation's exact
template. New ICON_SEAMS probe: every icon the client renders must exist
in at least one spelling per tag, so a platform rename fails the matrix
instead of the browser. Declare the release in the manifest, fetch its
tag in the release workflow, and document the V4 generation with its
known deltas (deferred: the preferences card's Plugins-page migration,
image-offload accounting).
dsh 0.1.7 renamed every `Icon*Outline16`/`Icon*Outline14` to
`Icon*OutlineRegular`/`Icon*OutlineMedium`, so the plugin's named imports
arrived as `undefined` at runtime and the Context tab crashed with React
error #130. Each icon now resolves through BOTH spellings at module load —
modern first, legacy fallback — with a render-nothing component and a
bounded property-read catch so any future rename degrades to a missing
glyph instead of a crashed tab.
V4 lifted `isError` from the tool-result content wrapper onto the message
and made the event-level `data.error` identity optional, so a failing
result could lose its error flag. The fold now reads the mark from BOTH
spellings — surface nodes and file-op rows keep it on every generation.
dsh 0.1.7 (session format V4) removed `settings.register` — configuration
forms derive from each loader entry's own Config schema there. The inject
callback now probes the method and stays inert on a generation without it
instead of throwing inside the fiber.
The Plugin / GitHub / Settings labels open with siDeepseek, siGithub,
and the harness's IconSettingsOutline14 gear, riding currentColor at the
label's baseline. simple-icons is a dev-only dependency pinned to the
harness ui-primitives' own 16.31.0 and tree-shakes into lib/client.js as
exactly the two mark paths, so no runtime dependency is added and the
marks are identical to the harness's own SiteGlyph artwork.
The context stats card takes three flex parts and the plugin card one,
with the plugin card's floor lowered to 240px so the ratio shows at
common widths (its rows ellipsize and recover values on hover). Below
the 360 + 240px floors the pair wraps onto full-width lines as before.
A dashboard reopen landing inside the balance read's window joined the
in-flight route read but never registered its landing callback — the
read reported only to the open that started it, so the reopened
dashboard showed the stale remembered figure for its whole session
(issue #82, exactly as reported). The landing callbacks now collect in
a set and all fire when the read lands; the previous behavior was even
pinned by a test asserting the joiner stayed silent, which now asserts
the opposite. The store-level join test and a capsule-level
close-reopen repro pin the regression both ways.
The timing card gains a top-right tok/s chip, computed over the same
pairing as the harness's own session-stats fold: per assistant call,
the provider-reported output tokens paired with the first-token →
message window, summed only over calls carrying BOTH — so the plugin's
figure equals the harness pill's by construction (verified against the
running harness). The formatter mirrors the harness's too: whole tokens
from ten up, one decimal below.
The seat rides TimingTotals as additive-optional fields (speedTokens /
speedMs — a usage-gated subset of genMs), so cached rows written before
it simply render no chip, with no stateVersion bump. Unreadable or
absent pairing renders no chip; the tooltip names the pairing.
The range selector gains 近 24 小时 (Last 24 hours) ahead of 7d — the
finest window the dashboard offered was a week, too coarse for the
hour-scale sessions a working day produces. The window filters by the
rows' last activity exactly like the other ranges; the heatmap keeps
its own fixed 8-week view.
The tooltip repeated the pill's own total, and a zero side (granted or
topped-up) rode the bubble as a meaningless line. The pill keeps
carrying the total; the tooltip now lists only the non-zero parts, and
an all-zero account renders the pill bare — no bubble, nothing to break
down. The unused balance.tip.total key drops from both dictionaries.
The capsule's figure could come from two stale layers, and its total could
disagree with its own breakdown. Both are gone.
The host route remembered a successful read for five minutes, so opening the
console first pinned the capsule to that older snapshot — and the balance
moves with every billed request, so the two could not agree. Nothing outlives
a read now beyond the outbound fetch already in flight.
The client paints the remembered figure (memory, then storage) while it
revalidates in the background, so a reopening panel shows a number at once and
settles on the live one without ever trading it for a blank pill: a failed or
absent read notifies nobody and keeps serving what it remembered.
The total is now the granted and topped-up parts summed rather than the
platform's own `total_balance`, which rounds independently of them and can
land a cent away from what the breakdown beside it adds up to (the viewer's
arithmetic is the authority); an entry no longer needs `total_balance` to
prove its shape at all.
The specs cover the storage and transport failures that must keep the figure,
the per-open revalidation, and the disagreeing total.
Replace the native `title` on the balance pill — browser-throttled to about
a second — with the harness Tooltip primitive, the same zero-delay seam the
heatmap cells and the assistant jump action already use. The multi-line
breakdown keeps its shape through the bubble's `pre-line`, and an
`aria-label` carries the same text for assistive tech.
The spec drives the bubble through real hover events instead of reading
`title`, and covers the leave that drops it.
The Context Dashboard's balance pill becomes a link to
platform.deepseek.com/usage — the console page behind the figure it
shows — opened in a new tab with noreferrer, the same external-link
shape the Plugin Info rows use. Hover underlines the figure as the
click affordance; the read, degradation, and tooltip behavior are
unchanged.
The dashboard header gains a capsule with the DeepSeek open-platform
balance (issue #80). The host mounts a /api/dsh-context/balance route
through the same Connection exact-route registry as the detail and
backfill routes, resolves the connection facts per request the way
llm-deepseek serves its own requests (its llm-deepseek settings section
for the credential ref and endpoint override, the credentials service
for the key), and reads /user/balance behind a short TTL cache — the
key never leaves the host process.
Every face is optional and runtime-proved: a deployment without the
DeepSeek provider, without a resolvable credential, without the route
registry, or whose platform read fails (network, non-2xx, malformed
payload, hostile entries dropped whole) answers a typed null, and the
capsule renders nothing — no pill, no spinner, no error state. The
entry follows the display locale (zh → CNY) with the account's first
currency as fallback, and the tooltip carries the granted/topped-up
breakdown.
The persistence seam translates the format edge's
SessionFormatUnsupportedMigrationError into SessionFormatUnsupportedError
(with the "source vN artifact remains unchanged" suffix) before the error
escapes, so the exact-name check never matched on any supported dsh
baseline: the skip summary never fired and every refused legacy session
kept its own WARN (issue #75). Classify by the SessionFormatUnsupported
prefix instead, and cover both faces of the translation in the fixture.
The models.dev deepseek branch publishes the official OFF-PEAK rates
(deepseek-flash: 0.15/0.6/0.003 USD per 1M = api-docs.deepseek.com's
valley tier), but the estimator read the book as the peak list and then
halved the off bucket — every DeepSeek session under-estimated ~2x in
both periods. Flip the semantics: DeepSeek's peak bucket prices at
twice the book rate and the off bucket at book; the tooltip's peak
column doubles for DeepSeek rows with its structure untouched, so
deepseek-flash now reproduces the official peak|off-peak list exactly.
Fixes#78
The jump's landing used to reset the scrollport to the top, where the
stats cards hide the composition card and the pinned turn's trend chart
below the fold. The landing now reveals the Current Composition card
(`data-lc-current`) so the card and the trend chart share the
scrollport.
The reveal walks from the anchor to the nearest scrollable ancestor —
host-agnostic by need: the conversation tab scrolls the shared
`[data-conversation-scroll]` container while the right Sidebar scrolls
dockkit's pane body, a CSS-module class with no stable attribute of its
own. The rect delta lands the anchor flush regardless of the
container's current position, overriding the scroll ledger's restore;
repeat jumps re-reveal. No scrollable ancestor (or a hostile chain)
degrades to a quiet no-op.
The jump's seq resolution ran inside the click handler, but the
assistant-actions seat's `useChat` is a real React hook — calling it
off-render throws the dispatcher guard (React #321), which the seat
guard swallowed, so no focus was ever recorded and the landing view
stayed on its default selection. The button now resolves the reply's
seq at RENDER time (unconditional seat read, click consumes the
captured value), so the pin finally fires in production.
The relay also wakes mounted views now: the sidebar landing keeps the
Context panel mounted across jumps, so repeat clicks re-pin it instead
of recording a focus nothing consumes (the conversation-tab landing had
the same gap whenever the tab was already active).
The assistant-action jump now opens the Context tab on the right Sidebar
over the guarded `ctx.sidebarRight.openTab` optional seam (the column
expands in the same step, keeping the clicked reply in view) instead of
switching the conversation tab. Every harness or placement the seam
cannot serve — lines older than 0.1.5-rc.1, tab-only placement, no
mounted session surface, a hostile face — keeps the conversation-tab
activation as fallback. The tooltip is reworded neutrally ("Context
view") since the landing follows what the harness serves.
The warm-up paragraphs (the lastUser stateVersion bump and the
contextActivity dashboard key) now describe the trigger route, the
one-pass-per-process arm, the migration-refusal skip accounting, and the
deployment shapes that never arm the pass.
The contextActivity/contextTimeline warm-up used to cold-read the whole
session corpus at every startup, whether the dashboard was ever opened or
not — and a legacy log the running harness refuses to migrate re-attempted
and re-warned per session on every boot (one reporter measured 115 warn
lines, ~60 KB per start; #75). Under that sustained read pressure the host
also stalls its own client handshake, leaving the workspace sidebar to
mount without its session rows until a restart.
The pass now runs on demand, once per host process: the client POSTs the
plugin's /api/dsh-context/backfill fetch route (the same authenticated
/api fence as the detail route) the first time the Context Insights panel
opens, and every later hit is a no-op. The route and the cold-path faces
compose in either order; a deployment without the connection service or
the exact-route registry never arms the pass — such a client could not
open the dashboard to read the rows anyway.
Skips are accounted instead of spammed: a migration-refused log
(permanent, source-side) drops to a per-session debug line and joins one
summary info line per pass; unexpected per-session failures keep their
own warn, now proof against hostile error objects whose toString throws.
The inter-session yield widens to 100 ms so an on-demand pass never
squeezes the host's other services.
The green pill on the sidebar-foot entry read as a notification badge
(#74): a running-task count is not something a footer control should
surface as an attention marker. The entry returns to a plain icon +
label; the count's only consumer was the badge, so the tally helper and
the button's useSessions seat plumbing go with it.
Fixes#74.
The three local until-polling copies let the debounced detail updates
land outside act — React warned 19 times per run. They collapse into
one shared act-per-tick until in the kit: timers and promise chains
only fire while a tick's sleep is awaiting, so the updates stay
act-attributed and commit as each scope exits.
The four browser specs that arm the fetch-on-miss retry with a
deliberately failing fetcher silence the production console.warn the
failure legitimately emits, restored by a file-level afterEach.
A default-reporter run now prints zero stderr.