Commit Graph
162 Commits
Author SHA1 Message Date
Bowen Liang 4d144147a0 refactor(client): import React members directly, dropping the central react module
Every client file reached hooks and element types through the central
require seam (src/client/react.ts) and spelled them React.useState /
ReactNS.ReactElement. Import the members directly instead — named value
imports for hooks, inline type-only imports for element types — and let
the bundler own the single require('react'), which is exactly what the
platform module table answers.

This needs JSX off the classic runtime (it required a React identifier
in every tsx scope): tsconfig moves to jsx: react-jsx, so the bundle
sources jsx from 'react/jsx-runtime', already seeded by every supported
harness (verified against the dsh-v0.1.2-rc.1 platform.ts). The bundle
smoke's fake module table gains the specifier to stay a faithful shell
stand-in. h() survives as a local createElement alias at the slot
render-prop seams and the lightbox portal imports createPortal from
react-dom directly.

Verified: the built client.js requires react, react/jsx-runtime, and
react-dom exactly once each; the compat lane (bundle smoke + the
real-baseline matrix) passes against the fresh artifacts; the full gate
stays green with per-file coverage at 100%.
2026-09-07 08:59:03 +08:00
Bowen Liang 2d371dc72c fix(client): refit the system-open affordance to the session RPC seam
The File Activity card's hover-underline + click-to-open affordance died
with the harness connection refactor: the hostDescription.canOpenPath
fact and the connection.api.host.openPath RPC that the capability probe
and opener read no longer exist on any supported harness line, so the
probe answered false everywhere and every file name rendered inert.

Re-point both at the session controller's workspace-opener remotes
(session/canOpenWorkspacePath / session/openWorkspacePath) over the
generic '/api' channel — the same Connection RPC the split timeline's
detail channel rides — with the wire contract proved against a live
host: `args` must be a plain object keyed by the remote's declared
parameter names (an array is rejected host-side), and the capability is
now an RPC round-trip instead of a synchronous host fact, so it lands
in view state behind the harness's own isLoopback gate; a probe that
settles after unmount drops its answer. The shared connection-RPC
resolution is deduped into rpcCallOf, which makeDetailFetcher reuses.
2026-09-06 23:52:51 +08:00
Bowen Liang b13ad55501 fix(host): serve the timeline detail for cold sessions (viewed, never entered)
The detail endpoint resolved sessions through the live store's
`sessions.get()` only — but a session that is merely VIEWED never enters
it (`SessionStore.prepare` with a persistence seed deliberately does not
register), so every cold session answered the typed null and the Context
tab parked on the retryable failure note. On a gappy durable log this was
unrecoverable by construction.

The handler now follows the harness's own ladder: live sessions still read
the registry's current fold state via `stateOf`; a store miss observes the
session through `ctx.sessionQuery.observeSession()` (the observation
carries the complete immutable log) and folds the detail from init —
cheap, since a cold log is static and the client's per-session store reads
it once per page view. The lease disposes promptly; an absent sessionQuery
face, an unobservable session, or a rejecting read keep the typed-null /
failure-envelope degradation.
2026-09-06 12:03:34 +08:00
Bowen Liang 6faf8f16bf feat: count the searched target alongside the matched files in the op log
A grep/glob with the complete matched-file meta rowed ONLY per hit file —
the searched path (or the workspace-wide pattern) vanished from the File
Activity card entirely. Both now book: the call's target row first (the
searched path, carrying the pattern(+include) detail; a pathless search
rows the pattern itself, marked as a pattern), then one row per matched
file with its hit count. The degenerate single-file search (the target IS
the matched file) keeps one row. Truncated or malformed metas keep the
honest fallback (the call target only).

The fold's op-log semantics change, so stateVersion bumps to 15 and cached
rows refold.

Verified: 1147 tests, per-file 100% coverage; live on a grep-heavy session
the card counts 80 search ops — the searched directories row alongside
their per-file hits (plus a deterministic fold over the real log: 6 target
rows + 107 hit rows).
2026-09-06 12:03:07 +08:00
Bowen Liang 5c99a476f3 feat: fold the file-operation log host-side (full-log File Activity coverage)
The File Activity card derived its ops client-side from the served tool
nodes joined with the conversation WINDOW: a call whose arguments had aged
out of the loaded tail named no target and vanished from the card silently,
so the "up to latest" scope actually covered only the chat window. The join
was also the only meta/subCalls carrier, and the per-call op assembly was
written twice (top-level and Code-Mode nested paths).

The derivation moves to the host fold, which sees every lifecycle event with
its full payload: `tool/call` arms name + raw arguments (the pending-call
map's entries grow `argsRaw`), `tool/result` books the ops off the armed
arguments plus the result's presentation meta (grep's by-file match groups,
glob's path list, read's exact window), and `tool/code-dispatch` buffers
nested Code-Mode ops under their run_code root until the parent result
flushes them with `parent` + the program description. The log is bounded
(`maxFileOps`, default 400) with a `fileOpsFloor` coverage stamp, rides the
detail payload (and the inline value on channel-less hosts), and is plain
JSON end to end. stateVersion bumps to 14; cached rows refold from the log
to rebuild the op log.

The per-call parsing is ONE shared module (shared/fileOps.ts — opsOfCall
plus the kind/path/delta/read-window/search-attribution primitives),
consumed by the host fold and by the client's inline-generation fallback
(the legacy window derivation stays for old hosts, now through the same
parser, closing the duplicated assembly). The client aggregates + filters
(`activityOfOps` joins the archive's `gone` stamps at render time; nested
ops scope by their parent result's seq, matching the legacy node-level
rule); the card, its chips, rows, deltas, and locate bridge render
unchanged.

Verified: 1142 tests with per-file 100% coverage (new parser matrix,
fold-ops, aggregation, end-to-end through the real registry + detail
endpoint); the compat vocabulary probe gains tool/code-dispatch (present at
the 0.1.2-rc.1 tag). Live on the real deployment: the big session's card
grew from 4 window-bound files to 37 files / 135 ops spanning every turn,
and folding the real Code-Mode logs books nested ops with parent + program
stamps (e.g. 136 nested reads across one session, errors flagged).

Known upstream boundary (unchanged): `tool/code-dispatch` persists no
result meta, so nested reads keep the limit-estimate footprint and nested
searches the call-target form until the harness carries meta there.
2026-09-06 09:47:26 +08:00
Bowen Liang 62ae0f9c69 perf: split the context timeline into a slim wire head plus an on-demand detail channel
The contextTimeline projection value rode every harness delivery channel
WHOLE: each session.list row, every control baseline, every follow
snapshot, and a full-value broadcast on every state change (with a
full-value schema parse per push). The heavy collections (request records,
context events, the served surface window, the removed-node archive) are
only ever rendered by an OPEN Context tab/modal, so they moved off the
wire value.

Wire head (buildTimelineHead, ~1KB): the composition breakdown, the stats
board's precomputed counters (turns/steps/injects/compactions/prunes), the
newest request's billing summary (the headline's derived anchor), the
timing/cost totals, and `detailRev` — a revision marker that bumps exactly
on detail-mutating folds, the open tab's refetch signal.

Detail channel (host/detail.ts): the pre-v0.9 generic Connection RPC shape
returns as one narrow endpoint, `/dsh-context` `detail`, serving the
collections off the registry's live fold state (`stateOf` — no second
fold, no private copy). A nested `ctx.inject(['connection', 'sessions'])`
arms it load-order-free; a deployment whose faces never compose keeps the
inline wire value unchanged. The baseline-gate fallback path is untouched.

Client (client/timelineSource.ts): one per-session detail store shared by
the tab and the /context modal — fetch on open, then debounced
trailing-edge refetches whenever the pushed detailRev outruns the served
detail (single-flight, latest-wins by revision, refold reset, failure
backoff, absence stops trailing). Detail-driven cards show a pending/retry
note instead of a misleading empty state; the counters and headline render
off the head immediately. Both wire generations are reconciled per value
(new client + old host renders the inline value untouched; old client + new
host degrades to empty charts in the stale-watch window, never a white
screen). The chat-jump relay holds its one-shot pin until the detail lands.

Fold state keeps the full shape: no stateVersion bump, no cached-row
invalidation (the slim view is recomputed per read for cold checkpoints).
Compat matrix gains the detail-channel seam probes (HostConnectionRpc,
the browser caller, the registry stateOf), verified against the
0.1.2-rc.1 tag.

Measured over 263 real session logs from a live deployment, plus live RPC:
- per-session wire value: 84–150KB (top sessions) -> 0.6–0.9KB (~100-165x)
- all sessions aggregate (session.list rows / control baselines):
  4.4MB -> 0.2MB (~22x)
- per-client push traffic over one big session's lifetime:
  157MB -> 1.8MB (~87x), and the per-push full-value zod parse shrinks
  by the same factor
- detail payloads now flow only for the session being viewed, only while
  its tab/modal is open (120KB measured live for the biggest session);
  background sessions cost zero detail traffic

Verified visually against the running harness (big/small/cold sessions,
a live turn refreshing in place, past-step reconstruction, header epoch
and tool schema reads, the /context modal, the Agent network card) with
byte-identical card output. 1110 tests, per-file 100% coverage.
2026-09-06 04:08:20 +08:00
Bowen Liang cb93e4c74c refactor: share the settings vocabulary and dedupe the client fetch/overlay patterns
- The settings preference vocabulary (DefaultGranularity/DefaultTrendMode/
  DefaultFileSort/PluginSettings/SettingsField) was declared in both
  host/settings.ts and client/settings.ts — declared once in
  shared/types.ts now, re-exported at both ends.
- The Context browser's two fetch-on-miss state machines (row content,
  header epoch) were line-identical and had already drifted (the row
  read's idle note said 'no content') — one useFetchOnMiss hook plus one
  fetchMissNote helper (components/fetchOnMiss.tsx).
- The /context modal and the upgrade gate duplicated the capture-phase
  Escape handling; useEscapeClose (components/escapeClose.ts) is the one
  overlay contract, and the gate gains the modal's focus restore.
- The five/four identical useProjection reads in contextView/contextModal
  collapse into services.ts's projectionOf; agentTree drops its private
  asRecord copy for the exported one.
- agentTree's compact duration formatter is renamed fmtDurationCompact —
  same name as format.ts's locale-aware fmtDuration with different
  behavior was an import trap.
- Dead surface removed: Snapshot.occupancy (unfolded since 0.11 and
  undeliverable under the strict wire schema), CAT_COLORS,
  HistoryEventLike, and three exports nothing consumed (EVENT_ICONS,
  ContextViewProps, the FileSort alias).
- Comment drift fixed: the conversation join has a single useChat seat
  since the matrix narrowed to 0.1.2-rc.1+ (three sites), and
  services.ts's header no longer claims the module is type-only.
2026-09-05 22:19:01 +08:00
Bowen Liang 8205075877 fix(client): re-prove the meter projections at the boundary and catch settings write rejections
- tokenUsageOf/contextPressureOf were blind casts: any record passed.
  tokenUsage now requires the strict four-bucket wire shape (a partial
  value would silently undercount the billed total); contextPressure
  re-proves each of its three independent last-wins fields and no longer
  lets state-internal fields (surfaceTokens) ride along.
- headersOf now validates every tool entry (name string, finite tokens,
  plugin string when present) — a junk row threw on the browser's blind
  tool.name/tokens reads, blanking the card instead of degrading.
- timelineOf's fast path required Array.isArray but not record entries:
  a null/primitive element passed through and threw downstream
  (req.seq on null). Junk entries now take the sanitizing slow path.
- The settings scope write promise rejects on transport failure (dsh
  keeps only its queue tail fulfilled) — catch it and roll the
  optimistic echo back to the scope's truth instead of floating an
  unhandled rejection.
2026-09-05 22:17:54 +08:00
Bowen Liang 3e0673319c test: give the compat matrix driver hook the driver's 60s budget
A cold stage dir (fresh clone, cache miss, dependency re-pin) runs a
real npm install inside the beforeAll, which the 10s default hook
ceiling kills; align the hook with the driver spawn's own 60s timeout.
2026-09-05 00:55:05 +08:00
Bowen Liang 8b434f2dbe feat: gate below-baseline harnesses to fallback data with an upgrade modal
At apply time the host probes the running harness's version (the
dshHomePath-anchored profiles mirror first, the plugin's module anchor
second) and compares it against BASELINE_DSH_VERSION 0.1.2-rc.1 — with
release > rc > beta > alpha ordering at equal X.Y.Z. A harness below the
baseline gets fallback projection units (both registry contract
generations) that fold nothing and serve zeroed data plus an
`unsupported` wire record; the client keeps rendering the blank cards
and pops a per-session-dismissable modal naming both versions and urging
the update. A detection failure or an unparseable version fails open, so
a probe misfire can never blank a working deployment.
2026-09-05 00:23:22 +08:00
bowenliang123 fbf32ecc67 feat(client): open the Context tab from the chat stats line
The harness stats row (the composer dock's own `stats` entry) gains a
hover underline and opens the Context tab on click or Enter/Space. The
plugin mounts an invisible sibling entry on the officially plugin-facing
`conversation.composer.dock` seat; its anchor spots the single stats row
beside it inside the slot anchor, decorates it in place, and re-spots it
on dock churn (the row mounts only after the first stats exist, and
tooltip bubbles come and go beside it). A drag-selection click is
ignored so copying the numbers never switches tabs.

Every shape without a spotless row degrades to a silent feature-off:
a missing dock seat, ambiguous candidates (other plugins' entries,
crash faces, non-HTML markup), and rows that throw mid-wiring — and
unmount strips every mark and listener. The seat is pinned in the
compat matrix (SLOT_SEAMS) and the bundle smoke counts the new entry.
2026-09-04 18:27:01 +08:00
bowenliang123 33068ee1c9 compat: narrow the support matrix to dsh 0.1.2-rc.1+
The install/runtime compatibility floor moves from 0.1.1-rc2+ and
0.1.2-alpha2+ to 0.1.2-rc.1+. The 0.1.1 line and the 0.1.2-alpha previews
leave the support matrix, so every pre-0.1.2 fallback they existed for is
removed outright:

- client history: drop the legacy `connection.api.sessions.history` face,
  the `result`-nested envelope, and the `events` rows field — the gateway
  `remote.session.page` face and its ClientResult envelope are the only
  served shapes.
- client image loader: drop the pre-0.1.2 `conversation.resolveImage` face;
  `uiConversation.imageUrl` only.
- client node join: drop the pre-0.1.2 `useSession.nodes` seat;
  `useChat`'s ChatSnapshot `legacy.nodes` only (UseSessionLike removed).
- tests/baselines.ts: single baseline `v0.1.2-rc.1`; HostSeam dropped (the
  registry generation hard-codes header-passing init and seq-0 eager
  seeding, now mirrored with the exact `init(header, inheritedEventCount)`
  arity); ClientSeam slimmed to the consumed probes.
- deps: devDeps re-pinned to the 0.1.2-rc.1 generation (types compile
  against the real rc.1 surfaces); cordis ^4.0.2 and schemastery ^3.18.2
  per the rc.1 peer requirements; dsh-scope/dsh-invariants declared as dev
  deps; peer floors >=0.1.2-rc.1; dshReleases table trimmed.
- release workflow fetches only the dsh-v0.1.2-rc.1 tag; docs updated
  (AGENTS.md, README, docs/compatibility.md).
- verified against the rc.1 sources: the tool-attribution map (names and
  packages, incl. the documented `subagent_fork` alias), MCP naming, the
  page/imageUrl/legacy.nodes faces, and the registry contract.
2026-09-04 17:23:08 +08:00
Bowen Liang 4580692389 test(compat): pin the issue-44 freeze and recovery on the real registries
The matrix driver gains two probes over each baseline's REAL
SessionProjectionRegistry: hostile gateway usage must keep the served view
schema-valid on every later delivery (billed from the sanitized buckets),
and a poisoned upstream wire unit must reproduce the dsh containment gap -
the schema violation escapes the drive loop per event, every unit
registered after it starves, and snapshot() (every reconnect baseline)
throws wholesale. That seam is the registry's to fix; the probe names it
per generation.
2026-09-04 00:19:25 +08:00
Bowen Liang 5fdf8f6908 fix(host): fold provider usage through a deep billed-count read (#44)
A nonconforming provider figure (fractional token counts, or a gateway
reporting cached_tokens above prompt_tokens driving the disjoint uncached
input below zero) rode the state raw and failed the unit's strict
`.int().nonnegative()` wire/state gates on EVERY later delivery - the
registry drive throws per event, so the contextTimeline push feed dies and
every unit registered after it starves: the Context tab froze mid-session
while the agent kept running (issue #44: stats stuck at round 26 step 5 of
97, cost 3.49 of ~30 CNY).

- fold: every usage bucket passes a deep billed-count read - finite numbers
  and numeric strings accepted, fractions rounded, negatives clamped to 0,
  non-finite/garbage read as absent; the request record and the
  session-cost totals stay complete instead of freezing
- semantics change: any readable bucket is now a billing sample (official
  token-meter parity) - an output-only usage object bills prompt 0 with its
  output split, where the previous shallow guard dropped the sample whole
- stateVersion 13: cached rows refold from the durable log, which recovers
  the steps the frozen feed never showed (cost/timing are running sums and
  were never trimmed); upgrade and rollback both discard mismatched rows
  and refold clean

Closes #44
2026-09-04 00:19:10 +08:00
Bowen Liang 48806646d8 test(compat): extend the seam matrix to the alpha.5 and rc.1 baselines
Add the two heads of the declared 0.1.2 lines as matrix baselines: the
preview window's head (dsh-v0.1.2-alpha.5, where the DSH STORE
latest-three compatibility window currently ends) and the next-channel
release candidate (dsh-v0.1.2-rc.1). Their seam faces verified identical
to alpha.2, so the generation's client faces collapse into one shared
CLIENT_0_1_2 constant.

The new baselines surfaced a real staging drift: since 0.1.2-alpha.3 the
session-projection sources import SessionLogOffset/SessionSeq from
@deepseek-ai/dsh-session as runtime values, so the compat driver now
installs each baseline's own vendored dsh-session release next to cordis
(Baseline.session).

The release workflow fetches the two new baseline tags before testing.

docs/compatibility.md records the per-release verification evidence,
including the 2026-09-03 disposable-profile install/uninstall matrix
across 0.1.1-rc.2, 0.1.2-alpha.3/4/5 and 0.1.2-rc.1 (ref
AI-Scarlett/DSH-Store#439).
2026-09-03 23:12:19 +08:00
bowenliang123 fcd2071786 fix(client): resolve remote.session through the declared inject (#42)
The target-content fetch read the 0.1.2+ gateway history face with
ctx.get('remote.session') and then dereferenced .page on the returned
value. A cordis traced-service proxy read off a fiber that never
declared the service throws on property access ("cannot get property
\"remote.session\" without inject") — the guard around ctx.get alone
cannot catch it, and the exception escapes the Context tab render and
takes the whole web client down.

The remote face is now resolved through the DECLARED inject instead:
apply() registers a ctx.inject(['remote', 'remote.session']) callback
(the dsh idiom — both names together, because the traced remote proxy
resolves .session through the context), binding the page verb into a
slot the fetchers prefer. Both names together stay absent on the
pre-0.1.2 line, where the callback never fires and the legacy
connection face carries the reads. Every service property chain is
additionally re-proven (readKeysOf: remote.session.page and
connection.api.sessions.history degrade at the first throwing getter
instead of ever throwing), matching the no-white-screen contract.

package.json now declares @deepseek-ai/dsh-api-remotes in
dsh.client.inject, so the graph composes the namespace-mounting bundle
before this plugin (skip-safe on 0.1.1, where the row does not exist).
2026-09-03 18:09:07 +08:00
bowenliang123 ffc89ff197 feat(host,client): split the timing model call into TTFT and LLM generation
The step-start → assistant-message slice reads off the stream now: the
model wait (TTFT, step start → first token) and the generation (first
token → message), folded from assistant/chunk — only the open step's
first non-empty text/reasoning/tool-call delta stamps the slot (the
session-stats rule). Calls with no recorded token stay unattributed in
the overhead residue. `lmMs` leaves TimingTotals for `ttftMs` + `genMs`,
the stepStart slot gains the stamp — stateVersion 12, rows refold.
2026-09-02 18:05:27 +08:00
Bowen Liang 914c54da3d feat(client): show the stats legend shares at 0.1% precision
fmtShare rounded to whole percents with a <1% crumb floor, which bunched
everything under 1% together and hid real differences in the two donut
legends. Shares now carry one decimal (73.8%, 26.1%), the crumb floor
tightens to <0.1%, empty slices read 0.0%, and the 100% cap is unchanged.
2026-09-01 23:48:18 +08:00
Bowen Liang 07aae76261 fix(client): count the system prompt on legacy contextHeaders wire values
The context browser read the system row's item count from
`header.systemTokens`, a field only the post-#37 metadata wire shape
carries — but a host still running the pre-#37 view (a stale watch
build, an app not restarted since the upgrade) delivers content-bearing
epochs where the system TEXT rides the value instead. The row then read
0 项 while the ≈tokens figure still rendered, because the two come from
independent projections. Diagnosed against a real session: the cached
projection row was pre-#37 shaped, holding a 6261-char system string
(≈1570 tokens shown, count 0).

`headersOf` now normalizes both generations: unpriced legacy entries get
the meter's own heuristic, priced entries and new-shape values keep the
zero-copy pass-through. The heuristic (ceil(chars/4) + 4) moves to
shared/estimate.ts so the host fold and the client boundary price
identically. Regression tests cover the boundary matrix and the browser
row through the real call path; the #37 slimming itself is untouched.
2026-09-01 23:36:04 +08:00
Bowen Liang b11a1d2151 feat(client): dock the /context modal mask beside the shell sidebar
The fixed backdrop masked the whole viewport; inset it by the sidebar
track parsed off the app frame's inline grid template (the sole inline
gridTemplateColumns in the dsh client, identical on every supported
baseline), so the dialog centers over the main column and the sidebar
stays visible and interactive. An unresolved frame degrades to the
full-viewport mask.

The measure runs in a layout effect (no 0-inset first paint) and, while
open, a MutationObserver on the frame's style attribute follows sidebar
drags, collapse toggles and narrow-viewport re-solves. A per-baseline
compat probe pins the frame seam.
2026-09-01 22:48:26 +08:00
Bowen Liang c44386bb31 perf: project header epochs as metadata and fetch content on demand (#37)
The contextHeaders projection served full prompt/schema content through every harness channel (session.list, control baseline, pushes, checkpoints), so the initial payload grew with sessions x epochs x content size (issue #37). The projection now carries epoch metadata only; content lazy-loads per epoch via a seq-anchored history read; stateVersion stays pinned at 1 with a v1-tolerant state schema and a normalizing view so no cold session is ever orphaned. Headers wire -97.8%, list projections block -81.8%, checkpoint writes -82% (new sessions), host heap -92%, browser projection memory -98%, checkpoint CPU -90%, schema stringification 52x.
2026-09-01 17:28:51 +08:00
bowenliang123 a658dc4fea feat(client): link the stats legend rows to their donut slices on hover
Hovering a legend row or a ring segment now lights both: the shared
hover key dims the other slices to the stacked bars' 0.35 idiom and
tints the active row. A hover with no painted arc (a zero slice) tints
the row but leaves the ring at rest. The legend names bolden, and the
token buckets get clearer names and colors: Cached Input / 缓存输入,
Uncached Input / 未缓存输入 (violet, no longer blue-next-to-blue with
Output), Cache Hit, Cache Write.
2026-09-01 15:02:59 +08:00
bowenliang123 426f07926b feat(client): two-tier the stats legend rows for clearer hierarchy
One-line rows squeezed the label, the quantity, and the bold share
together, so long names wrapped mid-phrase beside right-aligned figures.
Each row now splits into a primary line — the color dot and the name,
with the share closing a fixed right column — and a muted secondary
line that drops the absolute quantity under the name. The timing rows
move the call count out of the label into that secondary line
('4m 0s · 10 calls'), '(incl. reasoning)' becomes the output row's
qualifier, zero-figure rows dim whole, and names stretch across the
primary line with ellipsis only as a last resort.
2026-09-01 14:51:01 +08:00
bowenliang123 a234d0caeb test: fold the bundle smoke and the compat matrix into one pnpm test
Drop the test:bundle / test:compat scripts. The bundle smoke moves into
the `compat` vitest project (tests/compat/bundle.spec.ts): the
__ModuleLoader__ handoff, the CSS channel, slot/trigger registrations,
and HMR safety over the BUILT lib/client.js — so `pnpm test` runs the
whole verification (host, client, built artifacts, real-code compat
matrix) in one pass with the per-file 100% coverage gate. The
built-artifact specs skip cleanly when lib/ is absent; the release
workflow builds before Test, so they always run there.

Also drop the standalone Bundle smoke step from the release workflow
and refresh the docs/comments that referenced the removed scripts.
2026-08-31 15:20:59 +08:00
bowenliang123 986d45a2bb test: add the cross-baseline compatibility matrix
tests/baselines.ts is the single source of truth for the supported dsh
baselines and their seam faces. Three layers verify them:

- always-on host registry-contract drivers over the REAL projection
  definitions (plain-JSON cache-write gate, wire delivery, checkpoint
  restore, init-header tolerance) — the recurring host incidents,
- always-on client-face matrix (nodes seat, image loader service,
  history face + envelope, markdown chrome, full tab render) — the
  recurring client incidents,
- the `compat` vitest project: stages the REAL harness sources at each
  baseline tag, boots the tag's registry on the cordis release that line
  vendors, applies the BUILT plugin, and probes slots/faces/vocabulary/
  platform table from the tag's sources. The release workflow fetches
  the baseline tags before `pnpm test` so the matrix always runs there.

Adding a future baseline = one tests/baselines.ts entry + one fetched
tag; a failing probe names the seam to re-fit.
2026-08-31 14:58:08 +08:00
bowenliang123 7d2a1e7e90 refactor(host): converge on the 0.1.1-rc2+ / 0.1.2-alpha2+ baselines
Drop the pre-0.1.1 session-projection adaptation: the units emit the
single supported contract (stateSchema + a REQUIRED wire block) instead
of the dual schema/view + stateSchema/wire shape, and the peer ranges
move from >=0.1.0-rc.7 to >=0.1.1-rc.2. The 0.1.2 init(header) widening
stays satisfied by the zero-argument init. Spec helpers and call sites
follow the V2-only shape.
2026-08-31 14:57:46 +08:00
Bowen Liang 88aeccfe30 fix(host): restore the patched tools.register when the plugin unloads
createToolAttribution permanently replaced the global tools service's
register method and never undid it: after the plugin was disabled,
removed, or reloaded, later registrations kept running through the dead
hook's wrapper, which retained the live map and recorded into it.

The wrap now registers a fiber effect whose disposer peels every wrapper
this incarnation installed back to the true original, so the global
instance is left exactly as it was found. A guard keeps the restore
order-independent: a closure that finds a newer incarnation's wrapper
installed does nothing, letting that incarnation's own effect own the
cleanup. ctx.effect exists in every supported dsh version (cordis 4.0.1
since 0.1.0-rc.7).

Fixes #36
2026-08-31 01:18:16 +08:00
Bowen Liang 09aabe01d3 test(host): inline the lossless-JSON probe dropped from dsh-session
`snapshotJsonValue` left @deepseek-ai/dsh-session in 0.1.2-alpha.2 (moved
to dsh-util-values). Inline the same validate-and-detach semantics in the
fold driver helper so the specs compile and run against every supported
dsh face.
2026-08-31 00:13:37 +08:00
Bowen Liang 0a9dc869fb fix(host): register the settings namespace without the removed brand helper
dsh 0.1.2-alpha.2 removed the `settingsNamespace()` export from
@deepseek-ai/dsh-settings, so the named import failed at plugin load time
on that host. Register the raw literal (valid under every version's
namespace pattern) and keep a branded cast for the pre-0.1.2 type face,
mirroring the migration dsh applied to its own packages.
2026-08-31 00:13:27 +08:00
bowenliang123 eba64c99a6 fix(client): keep trend strip labels whole and hold the chart hover tip inside the card
Turn-strip labels render at natural width, overflowing narrow blocks instead
of ellipsizing to "…" on two-digit turns; updateTurnLabels now also thins
colliding labels (one reaching the previously kept label's box drops to
visibility:hidden), so consecutive 14px turn bars stay readable while
scrolling.

The hover tip caps at the chart wrapper's width and wraps instead of hanging
past the card into neighbouring panes or off-screen, and is restructured into
two rows: the bar's identity and its anchor total — the SAME actual value the
bar height and the axis graduation scale against (provider-reported prompt
when available, heuristic total otherwise); delta mode keeps the signed net
row. Identity phrasing follows the granularity: a single-step turn aggregate
reads "第 1 轮 · 共 1 步 / Turn 1 · 1 step" (new tip.turn1 key) instead of the
last step's index, and a record missing stepCount/turn degrades defensively.

The tip no longer shows the request time or the reply preview: the replyTips
prop, brief.replyTipsOf, the contextView wiring, and the tip.actual i18n keys
are removed.

Closes #34
2026-08-30 17:54:18 +08:00
bowenliang123 d765f0a31b feat(client): graduate the trend chart axis with quartile ticks, guides, and a zero baseline
Both total and delta modes now read off a full 0 / ¼ / ½ / ¾ / 1 graduation:
the axis gains the missing ¾/¼ labels (delta marks carry the value at each
fixed height on the uniform px-per-token scale), dashed guides align the bars
with every mark (darkened to --dsw-alias-border-l2), and a solid zero baseline
anchors the '0' label in both modes. A delta quarter mark whose label box would
overlap the zero label yields entirely — label and guide together.
2026-08-30 12:32:59 +08:00
bowenliang123 d0a935e634 i18n(client): capitalize the timing card's center label, fix the en call-count form
'en' only: the donut center sub reads 'Active Time', and the per-row call
count moves the multiplication mark before the figure with a space
('Model calls × 5' over 'Model calls 5×'). The zh forms (总活跃时长,
'71次') are unchanged.
2026-08-29 22:38:34 +08:00
bowenliang123 a8cd10199e test(host): pin the plain-JSON fold-state invariant on the contextHeaders unit
The headers fold helper now asserts every folded state is lossless JSON via
the framework's own snapshotJsonValue, matching the timeline specs; a new
hostile case covers the wrong-shaped plain JSON a committed log can hold and
pins verbatim survival of empty-object tool schemas. A comment on the
verbatim schema entry documents its session.append purification dependency.
2026-08-29 18:57:54 +08:00
bowenliang123 2e54440297 refactor(client): dedupe tooltip chrome into .lc-tip; adopt DSH tokens
The four styled DOM bubbles (stat cost tip, brief tag tip, file-card
delta tip, chart hover tip) each carried the same hand-rolled chrome
(layer-2 fill, hairline frame, 6px radius, float shadow, hover reveal).
A shared .lc-tip rule in base.css now owns that chrome — tips pair it
with their own class and keep only local positioning, width caps and
whitespace. The chart tip keeps z-index 5 and nowrap locally.

Token audit against dsh's ui-theme catalog:
- modal backdrop: hardcoded rgba(0,0,0,0.45) now rides
  --dsw-alias-bg-mask-1 (the harness Modal and our own lightbox mask),
  fallback keeps the old value on older dsh builds.
- monospace stacks (error message, call name/state, param type, arg
  value) now ride --ds-font-family-code so code text matches the
  harness code font; the fallback stack stays for older builds.
- tip shadows ride --dsw-shadow-lv3, the floating-layer shadow the
  harness primitives use, fallback keeps the old tight shadow.

Kept as-is: the neutral translucent grays (128,128,128/x) for tracks
and zero lines and the violet category accents (#a855f7/#8b5cf6) —
the catalog has no tokens with those semantics.
2026-08-29 14:11:32 +08:00
bowenliang123 040b56d926 feat(client): hide zero-value buckets on the Token Stats card
Zero buckets stay hidden once any usage is reported — the always-zero
cache write no longer shows on DeepSeek sessions — while the empty
state keeps the four-row fallback. Billing and cache-hit accounting
are unchanged; writes still render when providers report them.
2026-08-29 13:52:51 +08:00
bowenliang123 59cd1be501 feat(client): expand the plugin settings card on the Open-in-Settings jump
The settings shell keeps its open/section state in private component
state and offers no plugin-facing open face, but the card registered on
settings.plugin.item is ours. After the jump's settings-trigger click
survives, it raises a short-lived in-bundle expand request; the card
consumes it on mount and lands open, scrolled into view. Degraded jumps
raise no request, and the request expires in 5s, so no stale signal
surfaces on a later manual visit.
2026-08-29 13:32:09 +08:00
bowenliang123 0090fa3a57 feat(plugin-info): add a settings entry below the GitHub row
The harness settings panel keeps its open state inside the shell
component and exposes no plugin-facing open face, so the entry drives
the real chrome best-effort: click the sidebar's dialog trigger, then
the Plugins section nav row (matched by its shipped en/zh labels).
Every step is individually guarded - a shell without a matching
trigger or section degrades the whole jump to a silent no-op, and no
path can throw into the render.
2026-08-29 09:54:24 +08:00
bowenliang123 5f9bc0d2dc feat(context): split the stats board into context/timing/token cards
The host fold derives whole-session timing totals from the durable
step/tool lifecycle (step/start -> assistant/message -> step/end;
tool/call -> tool/result by callId): step wall time, the model-call
slice, and per-call tool durations with a bounded per-name tally
(stateVersion 11; cached rows refold from the log).

The head row becomes four equal cards: an eight-cell context grid
(shape stats, event tally, cost estimate with its rate tooltip), a
token ring around the cache-hit share, and a timing ring splitting
model calls vs tool runs. Legend rows pair each donut slice with
its label, quantity, and share - no bars; the ring center type
scales with the ring so it never touches the stroke. Wire guards,
i18n, styles, and hostile-input test fixtures included.
2026-08-29 09:22:09 +08:00
bowenliang123 6654954236 feat(browser): open the lone row with a single-item category
Every one-item category (not just system) now auto-expands its only row
when the category opens, so one click lands on the content directly:
the lone tool schema and the lone surface node join the system prompt.
Multi-item categories keep opening collapsed.
2026-08-29 02:12:27 +08:00
bowenliang123 0c8312763e feat(client): jump from a chat reply to its turn in the Context tab
Add an icon to each finalized reply's assistant-actions row that opens
the Context tab pinned to that reply's turn: the click resolves the
reply's request seq off the served node seat, records it in a per-session
relay, and activates the tab; the view consumes the relay once its
projection data is in, flips the trend chart to turn bars, pins and
centers the turn's aggregate, and resets the shared scroller. Every hop
degrades quietly (unknown seat, unresolvable seq, missing tab, hostile
nodes, aged-out turn).
2026-08-29 01:44:51 +08:00
bowenliang123 86333a80d5 feat(client): link the trend card highlights to the shared category hover
Hovering a category in the Context browser (or the overview legend/bar)
now lights that category across the trend card: the matching segment in
EVERY chart bar (total and delta modes), the detail panel's composition
bar, and its per-category rows.

The chart highlight rides a plain container attribute (`data-catdim`) +
static per-segment `data-cat` keys, so the dim/brighten pairing is pure
CSS and the memoized bars never re-render on a cross-card hover. The
detail bar mirrors the key one-way with its tooltip off (a cross-card
hover must not float a second tip), and the detail rows reuse the
legend-chip idiom: tinted background, bold label, ring on the color dot.
The overview's 'free' key stays filtered, as in the browser's live link.
2026-08-29 00:53:48 +08:00
bowenliang123 7b61d5ddc7 feat(browser): dual-face compatibility across the dsh 0.1.1/0.1.2 seams (#26)
0.1.2 rewrote the client composition and the history transport; every seam
this plugin touches now resolves by feature detection and degrades visibly
instead of erroring:

- history reads: `remote.session.page` (0.1.2; the inclusive cut
  `throughSeq` pinned to the target seq) over `connection.api.sessions.history`
  (0.1.1); both served envelopes unwrap ({ok,value} and {result:{ok,value}});
  packed chunk-row records skip (final events always ride verbatim); a
  malformed payload rejects into the retryable state instead of claiming
  absence.
- conversation join: `useChat` (ChatSnapshot.legacy.nodes) over the session
  snapshot's top-level `nodes`, both seats invoked in a stable hook order —
  fixes the empty File Activity card on 0.1.2 (#26).
- image loading: `uiConversation.imageUrl` (0.1.2) over
  `conversation.resolveImage` (0.1.1); absent faces degrade the cards to
  metadata-only.
- `/context` command: waits for the `inputTriggers` service (ctx.inject)
  instead of racing module arrival order, which 0.1.2's finer modules broke;
  the modal resolves `sessions` at close time for the same reason.

Absent faces drop features (static out-of-window hints, no command) — the
tab and the modal never render an error page.
2026-08-28 22:32:19 +08:00
bowenliang123 d25d44aff1 fix(host): keep undefined-valued scalars off the wire view (#29, #30)
model/provider/contextWindow now join the served snapshot by conditional
spread: an unknown value leaves the key ABSENT instead of materializing an
`undefined`-valued property. The harness's lossless-JSON pipeline rejects
such a view whole — on 0.1.2 that fails every projection push for the
session (the fresh-blank-session breakage reported in #29/#30; the same
property discipline the persisted state has held since stateVersion 10).
Guarded by plain-JSON view assertions over both the unknown and the known
model shapes.
2026-08-28 22:32:01 +08:00
bowenliang123 83bc071338 feat(browser): number the lines of the raw text view
Each source line becomes a block span whose counter-fed ::before sits
in a narrow left gutter; pseudo content never reaches the clipboard and
user-select none keeps it that way, so copying the body still yields
the exact source. Interior empty lines keep their height, a trailing
newline adds no row, and soft-wrapped continuations align at the
content column.
2026-08-28 12:15:40 +08:00
bowenliang123 8a5f1e0b02 style(i18n): per-category placeholders for the row filter
Each category's filter box now names what it scans — message text
(user), injected context (inject), reply or calls (assistant), tool
or arguments (tool results) — instead of one generic "filter by
text". The tools category keeps its existing placeholder.
2026-08-28 10:08:56 +08:00
bowenliang123 093dd9f69e refactor(browser): a shared row filter across every category
The tools' text filter generalizes into the browser-wide row lens: a
shared RowToolbar (input plus an optional control group) now filters
the four message categories too — user, inject, assistant, and tool
results — matching each row's tag and preview text exactly as shown
(call breadcrumbs and call-args summaries included; full bodies stay
out of scope, they load on demand).

The node-list branch splits into a derive pass (each row's tag +
preview facts, computed once) and a filtered render pass, and the
query is a lens on the OPEN category: opening a different one resets
it while step picks keep it. System stays searchless (one row) and
sort stays tools-only (reading order is meaningful for messages).
2026-08-28 10:07:21 +08:00
bowenliang123 d6cd02acad feat(browser): filter and sort the tool-schema rows
The tools category gains a local view state: a text filter scanning
each row's name, producer description, plugin chip, and raw parameter
JSON (a hostile schema degrades to no matchable text), plus a
size/name sort toggle — size (token price, mirroring the overview's
Top chips) stays the default, name gives lookup. The toolbar stays
mounted on an empty match so the filter can always be cleared, and
both survive step picks so one lens compares epochs.

Reuse the FileCard's control idiom: the search input and toolbar
share the lc-fa-search/lc-fa-ctl rules under browser-scoped names.
2026-08-28 09:58:13 +08:00
3244ff29a9 feat: tool plugin source attribution in the Context browser (#25)
Tool-schema rows in the Context browser carry a best-effort source tag
for the registering plugin, recovered without a harness change:

- a harness-logged `plugin` field passes through verbatim (future-proof);
- MCP-proxied tools resolve `mcp:<server>` from their public names;
- shipped first-party tools resolve from a pinned package map;
- a live `tools.register()` hook (cordis `internal/get` reader slot,
  with a caller-package stack fallback for local-link installs) tags
  third-party, agent-scoped, and dynamic registrations, and peels a
  prior incarnation's wrapper on reload;
- tools already registered when the plugin mounts render a localized
  "unknown plugin" tag explaining the boot-order gap.

Priority: `mcp:<server>` > live record > pinned map > unknown. The
chip rides the shared tag frame tinted with the brand badge tokens.

Rebased onto v0.35.0 with maintainer review fixes; the attribution
priority and reload-safety rework and the chip theming are described
in the PR review comment.

Co-authored-by: Nwflower <1679659@qq.com>
Co-authored-by: bowenliang123 <liang.bowen.123@qq.com>
2026-08-28 09:50:10 +08:00
bowenliang123 15ae3b96c3 feat(files): open files on the system; display workspace paths relative
The File Activity card gains two affordances, both resolved from the
client ctx and degraded per-host:

- Workspace-relative display: paths inside the session's cwd (read off
  the session-list row) shorten to a './'-relative form; absolute paths
  outside the workspace, Windows drive paths, and already-'.'-relative
  paths keep their verbatim form. Path search matches the display form.
- System open: when the host description advertises canOpenPath and the
  connection api exposes host.openPath, a file name opens on the user's
  system (fire-and-forget, failures stay silent); otherwise it renders
  inert. A pathless search books its pattern as a display-only pattern
  row — never relativized, never openable — including nested Code-Mode
  searches, which previously leaked the pattern into a resolvable path.

workspaceOf and canOpenPathsOf re-prove every field and bound hostile
snapshots (a throw on call or property access degrades to undefined).
Component specs for the card are restored and extended for the new
props; the fold gains a pattern-row case for nested searches.
2026-08-28 03:11:23 +08:00
bowenliang123 059c85a54c feat(files): show each read's line footprint; op times on the right
Reads now carry their line footprint in the op log, in a third color
next to the +/- deltas: `>>30` for the exact window read off the
result's presentation meta (hover names the line range), degrading to
`≈80` estimated from the limit argument when no meta survived — and
staying silent for an unbounded read, which names no footprint. Nested
PTC reads estimate the same way.

The op timestamp moved from the leading column to the right edge,
mirroring the file row above it.
2026-08-28 01:27:42 +08:00