Commit Graph
15 Commits
Author SHA1 Message Date
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
bowenliang123 3acec4c87e feat(ui): instant hover tooltip and turn-aware dimming on the history chart
- replace the delayed native title with an instant custom .lc-chart-tip glued
  to the hovered bar's column (single line: turn/step, time, total, actual)
- turn-aware dimming: while a turn is focused (bar or strip hover), bars and
  strip blocks outside the active turn fade to 35%; the whole current turn
  and its strip block stay fully opaque
2026-08-15 03:38:15 +08:00
bowenliang123 6cb6a96980 refactor: layered module split with first-screen optimizations
- shared/types.ts: single wire contract (type-only) used by both halves
- host/: services (contracts), pricing (token-meter), fold (incremental fold
  + retention), snapshot (build + event turn/step attribution), index (RPC)
- client/: i18n, styles, format, categories, viewkit (shared deps),
  cache (per-session stale-while-revalidate), components/* (one module per
  component), view (composition root), index (plugin entry)
- first-screen: initial render seeds from the session cache, polling pauses
  while the tab is hidden (resume + immediate fetch on visible), and a
  transient fetch error never blanks already-visible data
- build.mjs: bundle:true for both halves (react external in the client
  bundle); the client half is now covered by tsc --noEmit for the first time
2026-08-15 02:19:28 +08:00