Commit Graph
52 Commits
Author SHA1 Message Date
bowenliang123 c003916360 feat(client): two equal-width image attachment columns with token badges
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.
2026-08-22 16:31:13 +08:00
bowenliang123 d3cdc263d6 feat(client): read the official contextBreakdown projection for the composition card
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.
2026-08-22 16:31:05 +08:00
bowenliang123 d9c09782fc feat(host): price image blocks by DeepSeek's official image token calculator
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.
2026-08-22 16:30:55 +08:00
bowenliang123 75d1ec8917 test: shift ContextView hook slot indices for the focusTurn state
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.
2026-08-22 04:33:45 +08:00
bowenliang123 ad1e6872ba feat(client): collapsed browser rows lead with an inverted tool breadcrumb + call summary
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.
2026-08-22 04:31:34 +08:00
bowenliang123 654eca8cdb feat(client): tool calls in assistant messages and tool results share one arg card
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.
2026-08-22 04:16:56 +08:00
bowenliang123 c55ab3da3d perf(client): memoize trend-chart bars; fix stale retention comment
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().
2026-08-22 03:49:32 +08:00
bowenliang123 5967200a9b perf(host): consume callNames entries on tool-result fold
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.
2026-08-22 03:45:50 +08:00
bowenliang123 47b7d9eacd feat(client): assistant tool calls render as parsed argument cards
A tool call inside an assistant message's blocks now forms its own card,
mirroring the tool definition's parameter card: the head names the call
target with the parsed argument count, the body lists each argument as a
name/type/value row (values render raw when strings, JSON otherwise).
Arguments that are not a parseable JSON object fall back to the raw
payload inside the same card.

The spec now arms two calls — one parseable, one not — and asserts the
card chrome, the parsed row, the count badge, and the raw fallback. The
view-bed text helpers learned to read number children (React renders
them as text), which the count badge needs.

No new CSS: the card and rows reuse the tool-schema classes.
2026-08-22 03:25:26 +08:00
bowenliang123 680951aed9 test(client): assistant thinking/answer split into cards with independent raw/markdown switches
The assistant-message detail renders each thinking / answer block as its
own card (tool-schema chrome) carrying its own raw/markdown switch, so a
mixed reply's parts toggle independently; tool calls stay raw. The spec
arms an assistant conversation node (reasoning + text + tool call) and
asserts the two cards, their default markdown views, per-card switching
independence, and the raw tool-call row.
2026-08-22 02:41:36 +08:00
bowenliang123 598a2e06fa refactor(client): move styles to a real stylesheet through the official CSS channel
- tsdown.config.ts gains the three CSS virtual loaders mirrored verbatim
  from packages/client/tsdown.client.ts: .module.css (lightningcss-hashed
  class map + tagged injection), .css?inline (compiled text), and plain
  .css (unhashed tagged injection), each registered as a watch dependency
- src/client/styles.ts (223 rules as a string array) becomes
  src/client/styles.css — real stylesheet authoring, minified by
  lightningcss at build (client bundle 134.8 -> 130.3 kB)
- injection now happens at factory materialization with the official tag
  shape (data-plugin + data-plugin-css + dedupe guard), replacing the
  hand-rolled ctx.effect; the HMR receiver owns removal by data-plugin,
  so the hmr spec pins that lifecycle instead of fiber disposal
- Decision: class names keep the flat lc-* namespace (the anti-collision
  discipline, per the research note) instead of per-component hashed
  modules — the stylesheet's rules are interconnected across components
  (parent:hover child, shared state classes) and 241 test selectors read
  literal classes off the built bundle, which a content hash would make
  unknowable; the hashed .module.css channel is mirrored and available
  for future self-contained components
- globals.d.ts declares the *.css / *.module.css module shapes for the
  strict typecheck
2026-08-22 02:39:19 +08:00
bowenliang123 49f1439dae test: migrate to vitest, split the monolithic client spec into modules
- vitest is the runner (matches the official harness); the two bare-node
  scripts and the manual repro become 13 spec files under tests/
- the 1637-line client.test.mjs splits into focused modules (chart /
  browser / linkage / composition-link / tool-chip-bridge /
  browser-autoload / images / modal / resilience), each booting a fresh
  view bed from tests/helpers/viewBed.mjs — the stateful fake-React
  harness, fake ctx, fixtures, and tree walkers now live in one place
- tests/hmr-safety.client.spec.mjs (jsdom lane, @vitest-environment):
  fiber dispose removes every registration — locale dicts, slot rows,
  trigger source, provide contribution, and the plugin-owned style tag
- host spec gains the same dispose-removes-registrations pin for the two
  session-projection units
- the real-React jsdom mount (was npm run repro, broken since the tsdown
  switch by its own string surgery) now runs the bundle verbatim through
  __ModuleLoader__ and is part of the suite; fixed two latent harness
  bugs along the way: the slot-register capture was overwritten by the
  later overlay registration, and the granularity query needed scoping
  to the trend row (the events card reuses the pill-button classes)
- bundle evaluation in all specs runs the artifact verbatim via the
  boot handoff — no string surgery, decoupled from bundler layout
- scripts.test is now typecheck + vitest run; scripts.repro is gone
2026-08-22 02:33:28 +08:00
bowenliang123 021db5f2a2 build: migrate tsup to tsdown, mirroring the official client-bundle preset
- banner/intro/footer three-part closure-factory wrap (verbatim from
  packages/client/tsdown.client.ts), intro now owns var module/exports
- deps.neverBundle/alwaysBundle guards: host keeps production deps
  external, client keeps only module-table specifiers external
- bundle purity gate: any non-requested @deepseek-ai/* value import is
  a build error (INLINE_SAFE / vendored-library / generated-remote
  allowances mirrored)
- NODE_ENV + import.meta.env defines for inlined node-idiom deps
- client sourcemap on, matching the official artifact contract
- client smoke test now runs the bundle verbatim through the
  __ModuleLoader__ handoff instead of slicing the factory body by
  exact output strings (decoupled from bundler layout)
2026-08-22 02:14:44 +08:00
bowenliang123 af14769c0f feat(client): card-style message detail with image attachment rendering
Adapt to DeepSeek Harness 0.1.1's multimodal pipeline (and the vision
capability of DeepSeek-V4-Flash-Vision-Exp): a user message carrying
durable image refs expands into a card layout — prose in the text card,
image attachments as thumbnail cards (name, normalized + original
dimensions, stored size), and unrecognized blocks as raw JSON. Image
blocks in assistant messages and tool results (e.g. read_image output)
now render too, instead of being dropped or dumped as JSON.

Thumbnails load through the harness conversation service's resolveImage
(the same session-authorized loader the chat history uses); when the
service is absent the cards degrade to metadata only. The text card
uses an un-padded RichCard variant so all cards align at one indent.
2026-08-22 01:43:37 +08:00
bowenliang123 925c9ccca6 fix(client): no white screen on malformed backend timeline payload
Sanitize the contextTimeline projection into a render-safe shape at the
services boundary (collections -> arrays, non-object entries dropped,
current -> numeric breakdown, wrong-typed scalars dropped) so a corrupt
or failed host payload degrades to empty sections instead of throwing
during render, keeping every usable piece of data visible.

Wrap the Context tab and /context dialog bodies in a React error
boundary (styled error card + Retry) as the backstop for any residual
render error; harden fmtTime against invalid timestamps, headersOf
against malformed epochs, and the cache-hit/cost math against NaN.

Add client regression tests: corrupt payloads render with usable data,
the modal degrades, and a throwing projection reader lands in the error
card.
2026-08-21 23:12:51 +08:00
bowenliang123 1a625fef96 fix(host): dual-contract session-projection units for dsh 0.1.1-rc.1 compatibility
dsh 0.1.1-rc.1 changed the session-projection unit contract: registration
now reads stateSchema (persisted fold state) + wire (client view), and a
unit without a wire block is treated as host-only — its value is never
delivered to the browser. The units still emitted only the pre-0.1.1
fields (schema + top-level view), so on 0.1.1-rc.1 the Context tab stayed
on its loading screen forever: contextTimeline/contextHeaders never
arrived through useProjection.

Emit BOTH contracts on each unit: schema/view for dsh <= 0.1.0-rc.8, and
stateSchema/wire for dsh >= 0.1.1-rc.1 (each registry reads its own
fields off the same definition and ignores the extras). Added a strict
timelineStateSchema for the persisted state (the cold-read rung also
parses checkpoint rows with it) and mirror checks in host.test.mjs.

Verified against the real registries of both versions (snapshot
delivery, change feed, checkpoint, viewCheckpoint, restore) and the real
plugin-install flow on the 0.1.0-rc.7 and 0.1.1-rc.1 CLIs.
2026-08-21 20:41:10 +08:00
bowenliang123 1cc4da0a9f feat(stats): estimate whole-session cost at DeepSeek V4 list prices
Stats board gains an 预估费用/Est. Cost cell pricing the complete
session's billed tokens at hardcoded DeepSeek V4 list prices: the host
folds per-request usage totals (peak/off-peak split, cache hit/miss
input, output incl. reasoning) from the session log, flash and pro on
any provider, displayed in the locale's currency. The cell carries a
"?" marker whose hover bubble explains the whole-session estimate and
lists the per-1M-token price table straight from the pricing table, so
the printed rates can never drift from the math.
2026-08-21 17:01:22 +08:00
4d9ffcd3ca fix(host): keep timeline state plain JSON for the projection cache
The contextTimeline fold persisted `undefined`-valued properties (model/provider/lastModel/contextWindow at init, pendingShadowedSeqs set to undefined on consume, and turn/step written as undefined when absent from assistant/message data). The harness projection cache snapshots the whole session checkpoint through the lossless-JSON snapshotter, which rejects any `undefined` property — so every projection-cache write failed and the `title` projection (session list) degraded after restart. stateVersion 3→4 discards stale rows; a regression test now drives the real snapshotJsonValue.

Fixes #5, #7.

Co-authored-by: kiligzzz <kiligzzz@users.noreply.github.com>
Co-authored-by: Bowen Liang <liang.bowen.123@qq.com>
2026-08-20 22:39:42 +08:00
Bowen Liang 81415caad4 browser: raw/markdown view switch on detail cards
The context browser's detail cards (system prompt, tool description,
user/assistant message and injection bodies) gain a segmented Raw |
Markdown switch at the card's top-right corner — the same pill chrome
as the trend chart's Step/Turn control. Markdown is the default view,
rendered through the harness's shared MarkdownText (GFM, sanitized,
resolved from the platform module table — zero plugin-side markdown
dependency); Raw restores the exact source <pre>. Each card owns its
mode state via the new shared richText kit (explicit render props, no
React context), so two open details switch independently. Reasoning
blocks, tool-call args and tool results stay raw.

Markdown chrome is scoped to the detail cards: base size 13px (a
proportional face matching the raw view's 12px monospace in visual
weight), headings collapse to a weight-only bump, inline code keeps
full size and is pinned under the paragraph line so wrapped chips no
longer stick across lines.
2026-08-20 21:55:27 +08:00
Bowen Liang 6c924251b9 context command: swap the modal's trend for the Context browser
The /context dialog drops the last-10-turn history chart (TrendChart +
RequestDetail + markers) and hosts the shared ContextBrowser card
instead, so step picking, accordion, reconstruction and paging ride the
same component as the tab. The modal now reads the contextHeaders key for
the browser's system/tools sections, its command description is updated,
the dead lc-modal-trend rule is removed, and the modal render test is
rewritten to assert the embedded browser.
2026-08-20 04:52:44 +08:00
Bowen Liang 24a7707872 overview: mark the auto-compaction reserve band
The current-composition bar gains a striped band over the rightmost
(1-ratio) of the window (AUTO_COMPACT_RATIO = 0.8, mirroring DSH's
dsh-compaction-basic default thresholdRatio, which DSH does not publish
to plugins): hovering it explains that this area is auto-compaction
headroom, normally not actually occupied, and clears the segment hover.
Rendered whenever the window is known, on both the overview card and the
/context modal bar, laid out in window units so it stays correct once use
passes the threshold. Mirrored/detail bars stay clean.
2026-08-20 04:30:54 +08:00
Bowen Liang 498cda5acc browser: show per-category deltas vs the previous turn step
Each category row gains two direction-colored pills — an element-count Δ
after the item count and a token Δ hugging the left of the token figure —
each hidden while its figure held. Both baseline against the PREVIOUS
turn's last request regardless of step/turn granularity (live = last
request), with a short caption in the card header stating that baseline.
The count pill's redundant tooltip is dropped now that the token swing is
a visible pill.
2026-08-20 04:06:38 +08:00
Bowen Liang b2104fb745 overview: lay the category legend out as an aligned grid
The Current Composition legend stops being wrapped inline chips and
becomes an auto-flowing grid: each category is a label-left /
numbers-right cell (as many columns as the card fits, fewer as it
narrows) with the name bold at rest and a pointer cursor on hover.

The chip hover-link assertion now compares the label text via textOf,
since the label is its own span.
2026-08-20 03:35:36 +08:00
Bowen Liang 6a3caac925 stats: add cache-hit cell from the official tokenUsage projection
The context stats board gains a right-most 缓存命中 cell that reuses the
token-meter tokenUsage projection read through the standard useProjection
seat — the exact same data and buckets as the chat stats line below the
input box. The share is truncated (not rounded) to two decimal places,
with a float-noise guard; absent the projection it shows a dash. Adds the
bilingual stats.cacheHit label and test coverage (sixth cell, 80.00%
case, and a truncation proof).
2026-08-20 03:20:40 +08:00
Bowen Liang d1e0fbd507 browser: parse tool schema into a parameter table above a collapsed raw JSON
Each tool row in the Context browser now reads as three stacked titled
sections:

  * Description  - the prose, in its own card
  * Parameters   - one row per JSON-Schema property (name, type, required
                   chip, description); type labels cover the common
                   vocabulary (array<inner>, object{N}, enum, anyOf/oneOf
                   unions)
  * Raw JSON     - collapsed behind a slim toggle; the <pre> only mounts
                   when opened, so the default body stays compact and the
                   JSON.stringify pass runs on demand instead of every
                   render

The two top sections share one card chrome (bordered block with a small
title), so a reader can scan them as siblings rather than mixed prose and
UI.
2026-08-20 01:22:33 +08:00
Bowen Liang e90bd7617d browser: rank tool schemas by token price in the tools category
List the HeaderTools largest-first instead of the producer's header
order, mirroring the overview's top-tools chips. Tests: extended the
header fixture with a deliberately out-of-order pair and assert the
rows come back ranked.
2026-08-20 00:12:22 +08:00
Bowen Liang dc306d2d10 overview: link tool chips and label into the context browser
Make the overview '工具定义 Top' label, its '等 N 个' overflow link and
each tool chip clickable (hover underline) and bridge them into the
Context browser: the label/overflow link open the tools category, a chip
also expands that specific tool's row. One-shot focus is applied by the
browser and cleared back so the same chip can be clicked again.
2026-08-20 00:09:42 +08:00
bowenliang123 3d4267ffc6 feat: pin the Context browser to the History chart's locked bar
Clicking a bar in the History chart locks its step; the browser's step
picker now follows that pin, and unpinning returns the browser to the
live surface (with the same accordion reset as a manual pick). Hovering
another bar still previews transiently over the pin.
2026-08-19 13:55:09 +08:00
bowenliang123 69a62c12f7 feat: emphasize used percentage in overview headline, drop split-estimate note 2026-08-19 13:44:52 +08:00
bowenliang123 03fcc04260 feat: ease hover feedback in and out with 120ms transitions
Row hovers and the shared composition-bar hover (segment brighten, dim,
free-track outline, occupied frame, tooltip, legend chip) now glide
instead of snapping. The tooltip and occupied frame stay mounted with an
opacity toggle so they fade out on leave too, not just fade in.
2026-08-19 13:17:05 +08:00
bowenliang123 8f572ab818 feat: link context browser hover with current composition card
While the browser shows the live step, hovering its category rows or its
composition bar lights the matching segment + legend chip on the Current
Composition card, and hovering the overview lights the browser's category
row and bar segment back. The link is gated on the live step: a pinned or
previewed step has a different composition, so its hover never leaks into
the overview (and vice versa). The mirrored browser bar renders no tooltip
of its own, so a cross-card hover floats only one tooltip.
2026-08-19 13:06:50 +08:00
Bowen Liang 3c858dafc7 feat(client): link trend-chart hover to the context browser
Hovering a history bar now transiently previews that step in the Context
browser — picker, meta line, composition bar and category lists all
follow the pointer, with a 'preview' marker in the meta row. Leaving
the chart returns the browser to its own picker selection; a seq trimmed
out of retention is ignored. Accordion state survives the hover, so one
open category can be scrubbed across steps for comparison.
2026-08-18 04:24:50 +08:00
Bowen Liang beaa746409 feat(host): pin live inject nodes into the served surface slice
Injections land on the surface first, so in a long session the plain
maxNodes tail window dropped their identity while their tokens kept
counting (sums cover the full surface) — the browser's inject section
showed a gray '0 items' row with a real token sum and no way to see
what was inside.

The served slice is now the newest maxNodes tail PLUS every live inject
node older than the tail (they are few; the concatenation stays
seq-ordered). droppedNodes and surfaceFloor count only the unserved
non-inject overflow, so the 'N earlier messages' note no longer
double-counts pinned injects, and per-step reconstruction sees them too.
2026-08-18 04:12:45 +08:00
Bowen Liang 7de0cdab0f feat(client): auto-load older history for out-of-window browser elements
Expanding a Context browser element whose seq is outside the loaded
conversation window now pages older history in automatically (one page
in flight, up to 20 pages) until the join hits, instead of leaving the
reader with the 80-char preview plus a static note.

The pagination verb reaches the card through the harness's
sessions.provide channel: the plugin contributes a loadOlderHistory
standard prop resolved to session.loadOlder(), so every session-scope
slot component receives it. Hosts without the channel (or a duplicate
refusal) fail soft to the previous preview-plus-hint degradation.
2026-08-18 03:34:44 +08:00
Bowen Liang 81b9ecd7f0 context browser 1 2026-08-18 02:34:58 +08:00
bowenliang123 7a57ab2dae feat(client): replace events dropdown with an inline kind picker
The events card header now carries four pill buttons (注入/压缩/剪枝/切换)
instead of a select: all picked by default, clicking an unpicked kind adds
it (A -> A+B -> ...), clicking a picked one removes it, and removing the
last one resets to all. Each kind gets a distinct hue shared between the
pills and the row chips (绿/红/紫/蓝); the hint subtitle and the filter
dropdown are gone.
2026-08-17 15:50:55 +08:00
bowenliang123 68b2252e8b feat(client): add kind chips and conditional tooltips to context events
Each event row now carries a kind chip (注入/压缩/剪枝/切换) colored by
impact direction (+ adds, - frees, ⇄ neutral), the events card header
gains a one-line hint about the block's meaning, and the native title
tooltip attaches only while the label actually truncates (measured after
every render and on resize) instead of always.
2026-08-17 15:14:59 +08:00
bowenliang123 39b32211a1 refactor(client): rebuild Plugin info card as two whole-row links with upgrade chip 2026-08-17 14:32:57 +08:00
bowenliang123 d6e89b415c feat(client): add Plugin info card beside Context stats
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.
2026-08-17 11:36:32 +08:00
bowenliang123 c38cae25e8 refactor(client): trim Context stats to turns, steps, injections, compactions, prunes
Drop recycled/switches/est/actual-prompt/output cells and split the
single recycled counter into two — compactions and prunes — so the
counts are distinguishable in the board. Bold the cell labels and
center the values for a cleaner scan.
2026-08-17 11:08:09 +08:00
bowenliang123 d29dcf89fa refactor: drop the mirrored occupancy fold, read the official contextPressure projection
token-meter is a core service that owns estimation and replay (its docs:
'the singleton ctx.tokenMeter directly owns estimation and replay'), and the
published package registers three projections on the shared
SessionProjectionMap — tokenUsage / contextPressure / contextBreakdown.
The host was duplicating the contextPressure fold (pressureTokens +
sampledSurfaceTokens + occupancyWindow, sampleUsage on assistant/message and
usage chunks) and the client was rendering from that mirror.

Host: the occupancy mirror leaves the fold entirely (assistant/chunk is now
structurally ignored — same reference, no change feed). stateVersion 1 -> 2
so cached rows from the old fold are discarded. The strict wire schema no
longer accepts occupancy; the shared type keeps it optional as legacy for
downstream consumers.

Client: the headline reads the official token-meter contextPressure
projection via the useProjection seat (the same key the chat's context ring
reads); absent key/value degrades to the existing derived fallback (last
request prompt + surface movement), so the UI renders identically in every
deployment. UI functionality is unchanged — same headline, same percentages,
same composition anchoring.

diff-fold.mjs is repurposed: the plugin no longer has an occupancy fold to
compare, but its heuristic composition (current.system/tools + surface total)
must stay equal to the official contextBreakdown projection — verified
MATCH on three real session logs.

Tests: host asserts occupancy is absent + chunk reference stability; client
feeds the official projection through the stub and asserts the same UI text.
2026-08-17 01:21:31 +08:00
bowenliang123 9e6675ce1d test(host): pin R5/R6 semantics — usage bucket mapping and model-switch signal
Verified against the harness source before locking in:
- agent-loop appends request/header (reason 'change' when the envelope
  differs) BEFORE request/context per request, so a model switch is always a
  header change — firing the model event only there equals the durable
  record, and route/capacity metadata can never double-fire it.
- dsh-llm TokenUsage buckets are disjoint: billed prompt-side =
  inputTokens + cacheReadTokens + cacheWriteTokens, outputTokens already
  contains reasoningTokens — the fold's prompt/output mapping was already
  correct; comments now cite the official semantics.

No behavior change; new assertions cover the switch event (exactly one, from
model-a to model-b), provider-only changes (route update, not a switch), and
the usage mapping (1000 = 800+150+50; output 30).
2026-08-17 01:17:00 +08:00
bowenliang123 3e74f354e1 feat(host): add schemastery-compatible Config and make retention bounds tunable
The docs' plugin contract expects an exported Config (Standard Schema) so
invalid entry config fails the load loudly, and the harness config principle
is 'anything two deployments may want to set differently is a configuration
field'. The fold previously hardcoded MAX_REQUEST_STEPS / MAX_KEPT_TURNS /
MAX_EVENTS / MAX_NODES in fold.ts.

- config.ts: exported Config (zod, implements Standard Schema v1 — the same
  validator cordis uses via Config['~standard'].validate) with strict,
  defaulted fields; resolveBounds() turns entry config into FoldBounds.
- fold.ts/timeline.ts: bounds thread through applyTimeline/trimState/
  buildTimelineView; the projection unit is now created per instance by
  createContextTimelineDefinition(config), so cordis config edits hot-reload
  (HMR) like any other entry config.
- index.ts: apply(ctx, config) registers the config-bound unit; Config is
  re-exported for patchers and the config catalog.
- Defaults equal the previous constants, so behavior is unchanged; persisted
  state shape is untouched (no stateVersion bump — bounds are retention only).
- tests: host test drives a bounded instance (maxNodes/maxKeptTurns).
2026-08-17 01:15:09 +08:00
bowenliang123 bf4cdd1e58 feat: add /context slash command with a centered context modal
Typing /context in the composer opens a centered dialog with the current
composition (provider-anchored headline, stacked bar, legend) and the
last-10-turn trend chart — the same pushed contextTimeline projection the
tab renders, no RPC, no polling.

The command is the plugin's own '/' trigger source, fully client-side:
no host descriptor, no session log records, nothing model-visible. The
composer token is consumed only when the modal closes (deferred
slash/input-consume-token with span / bare-token guards, fail-soft if the
draft changed meanwhile). Esc, backdrop click, and the close button all
close it and restore composer focus.

Also extracts the provider-anchored headline derivation shared by the tab
and the modal (headline.ts).
2026-08-16 22:26:46 +08:00
bowenliang123 d7b20a2978 feat(ui): frame the occupied region on composition-bar hover
The composition bar lays segments out against the FULL context window (width
= x / window) while legend/tooltip percentages are shares of the OCCUPIED
part (x / used) — so a slice that is half the used context reads ~20% wide
when the window still has headroom, which looks contradictory.

On hover (segment, legend chip, or the free track) a dashed frame now
outlines the occupied region (left edge to the used/window boundary), making
the 'share of used' reference visible; the frame renders only when a free
track exists (otherwise width already equals the percentage) and is
pointer-events:none. Segment tooltips now append 'of used context'
(zh: 占已用上下文) and legend chips carry the same hint as a title.

Adds the .lc-occupied-box style (shared --dsw-alias tokens), one i18n key per
locale, and render-test assertions that the frame appears on hover and
clears when hover leaves.
2026-08-16 04:14:30 +08:00
bowenliang123 38dbd2ad92 refactor(ui): move the context tab onto the harness's React stack
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.
2026-08-16 03:51:00 +08:00
bowenliang123 6bebd495e9 feat: route context timeline through the harness session-projection pipeline
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).
2026-08-16 03:25:01 +08:00
bowenliang123 97a2d9aa6c fix: align context occupancy with the official ring — provider-anchored totals
The context tab always showed less than the official chat context
indicator because the two sides used different rulers: the plugin
displayed its fixed-density heuristic total (4 chars/token, which
systematically undercounts CJK text and JSON schemas by ~10-15%),
while the official ring shows provider-reported billed tokens. The
running host also predated the occupancy feature entirely, so the
client fell back to the raw heuristic headline.

Core change: totals are anchored to the provider sample, heuristics
only supply the composition ratios (the official ContextMeter's own
trick):

- host: fold a contextPressure-equivalent occupancy projection
  (pressureTokens / sampledSurfaceTokens / occupancyWindow) and serve
  it on the snapshot; usage is sampled BEFORE the message joins the
  surface, and streamed assistant/chunk usage samples count too, so
  steps that fail mid-stream no longer fall back to pure heuristics.
- client: headline = occupancy.projectedTokens, with a last-request
  fallback deriving the same figure against older hosts (a page
  refresh fixes the live view before any server restart).
- composition bar, legend and trend bars are re-proportioned to the
  anchored total (ratios stay heuristic, values marked ≈); the
  conflicting 'estimate ≈ n (~p%)' second percentage is gone.

Verified by tests/diff-fold.mjs (kept as a differential regression
tool): the plugin's occupancy equals the official
contextPressure.projectedTokens to the token on all 33 real session
logs, incl. a 17k-event log with 9 compactions and a chunk-only-usage
session.
2026-08-16 00:47:00 +08:00
bowenliang123 3951da0f42 feat(ui): headline the provider-based occupancy, matching the chat ring
The overview's headline is now the provider-reported CURRENT occupancy
(contextPressure formula: newest usage sample + heuristic surface movement)
— the same figure the official chat ring shows — with the heuristic estimate
as the secondary label. On the live session: 475.1k (48%) provider vs 409.0k
(41%) heuristic; the gap is the provider tokenizer vs the 4-chars/token
heuristic (CJK-heavy content undercounts ~15%), not missing data — our
heuristic fold matches dsh's official fold to the token.
2026-08-15 08:42:08 +08:00
bowenliang123 c78345076f fix(host): shadow-price removal follows shadowedSeqs, not the declared range
The producer's compaction shadow price covers the shadowedSeqs — which can
include prune replacement nodes whose own seqs lie beyond the declared range
end. Removing by the range left those nodes in the surface and overcounted
messages by 8 x the replacement price (10,416 tokens on the live session).
Removal now follows the armed shadowedSeqs (per-category bookkeeping kept),
making our heuristic totals exactly match dsh's official contextBreakdown
fold. Also: provider-based actual occupancy in the overview now uses the
official contextPressure formula (sample + surface movement), and the
missing 'overview.actual' dict key is added.
2026-08-15 04:07:03 +08:00