Commit Graph
34 Commits
Author SHA1 Message Date
Bowen Liang 8bfcf65d31 feat(client): count whole-session human inputs on the stats board 2026-09-12 01:24:46 +08:00
bowenliang123 65e5f0efd4 fix(host): probe the running module tree before the profiles mirror
The baseline gate resolved the harness version from the healed
`$DSH_HOME/profiles/node_modules` mirror first, but that mirror is only as
fresh as the last CLI profile boot: it is rewritten by whichever installation
last healed it, and a packaged desktop host never heals it at all. A leftover
global CLI could therefore pin the mirror below the baseline and gate a
healthy desktop harness — an upgrade modal plus fallback (unfolded) projection
units on every launch (issue #59).

Resolve through the module tree the plugin is actually bound to instead: its
own ESM resolution (`import.meta.resolve`, the same pipeline its runtime
imports use, so an embedding shell that redirects plugin imports — the
packaged desktop's ASAR resolver bridge — redirects the probe too) of the
library packages it imports. A witness that lands inside this package's own
tree is the plugin's dependency closure (a `link:`-installed dev checkout's
pinned devDependencies), never the harness, so it is discarded. The home
mirror stays as the fallback for a dev checkout and for a running anchor that
cannot answer; the CLI package is still probed only through it.

Fail-open is unchanged: any unresolvable package, non-file URL, or unreadable
manifest degrades to `undefined`, which never trips the gate.
2026-09-11 16:18:47 +08:00
bowenliang123 a4add07664 feat: add a placement preference (tab / sidebar / both) for the Context view
Closes #58: the Context panel hung off two registrations at once (the
conversation tab and the right-Sidebar guide entry), with no way to keep
just one. A new per-user `defaultPlacement` preference — the first row of
the Plugin configuration card — picks who carries the view: the
conversation tab, the right Sidebar, or both (the default, so existing
deployments see no change).

- shared: `DefaultPlacement` vocabulary (`all` | `tab` | `sidebar`) on the
  settings namespace; the Host schema gains a loose `defaultPlacement`
  field so a stale persisted value degrades to `all`
- client: `placement.ts` mounts each registration through its own factory
  and, on a preference flip, disposes exactly what the new value drops
  (the harness's own dynamic-mount idiom); `watchSidebarContextTab`
  returns its deferred-inject disposer so the sidebar mount can be taken
  down and put back; the settings card leads with the placement row
2026-09-11 13:44:21 +08:00
bowenliang123 489864d63a feat(cost): price the deepseek-flash model name at the Flash rates
The family matcher required a `v4` marker, so `deepseek-flash` requests
went unpriced. Accept `deepseek` as an alternative marker — proxy
spellings like `deepseek/deepseek-flash` land too, while foreign
flash-named models (gemini-2.0-flash) still match no family. The stats
board's price-list label and both locales' cost tooltip name the new
model beside deepseek-v4.1-flash.
2026-09-10 11:22:42 +08:00
Bowen Liang 635afceb83 feat(host): split the model generation window by decode block
The timing totals priced a model call as TTFT + one opaque generation
window, so thinking, answer text, and tool-argument decoding were
indistinguishable. Fold the stream's `block-start` framing into three new
additive-optional buckets — `reasoningMs`, `textMs`, `toolArgMs` — that
tile the window's marker span.

Both log generations feed the same split: a V0 log's `assistant/chunk`
markers open and close spans in the step slot; a V2+ log carries no chunk
events, so `decodeSpansOfStream` reads them off the embedded settlement
stream. The split is priced only when the window was (an unstamped call's
model time stays unattributed wholesale), and a zero span stays absent so
pre-split-shaped state gains no dead properties.

The fields are additive-optional, so `stateVersion` stays at 15 and
cached rows keep parsing — a bump would discard them and orphan the key
for idle sessions (the #37 regression).

Measured over 305 local sessions: reasoning 31.3% of active time, tool
args 15.3%, answer text 3.5% — the answer is 1/9th of what the model
spends decoding.
2026-09-10 02:21:45 +08:00
Bowen Liang 50e89301cd fix(client): reprice the session-cost estimate for the 2026-09-10 Flash rates
DeepSeek reprices the Flash series effective 2026-09-10 12:00 BJT
(off-peak per 1M tokens: hit ¥0.02/$0.003, miss ¥1/$0.15, out ¥4/$0.6;
peak at 2x). Between the V4.1 Flash launch and the V4.1 Pro release
every Pro request is routed to V4.1 Flash and billed at the Flash
rates, so pro shares the flash triple until V4.1 Pro is priced — swap
that one reference when it ships. The stats-board tooltip names the
flash row deepseek-v4.1-flash, and both locales note the interim
routing. Fold logic is untouched: the substring family match already
lands v4.1 spellings (new fixture), and the peak windows are
unchanged. Logs spanning the cutover estimate entirely at the new
table — the cell is a reference estimate by contract.
2026-09-09 23:41:31 +08:00
Bowen Liang a4bd0d40f9 compat: move the V3 baseline to dsh 0.1.5-alpha.2
The supported V3 line follows the newest release: the baseline becomes
v0.1.5-alpha.2 (tag, vendored session package) and the declared release
map names 0.1.5-alpha.2. The release workflow fetches the new tag.

The alpha.2 client seams are pinned in the matrix: the conversation
surface moved under the keyed `main` panel (`main.conversation`) while
the plugin's `conversation.view` seat keeps its name, and
`SidebarRightGuideEntry` dropped its description line. tests/baselines.ts
therefore gains a guideEntry probe (the fields present plus the dropped
copy field asserted absent).

Every support/capability label states the 0.1.5-alpha.2+ floor;
`0.1.5-alpha.x+` remains only as the V3 LOG-generation label. The docs
record the verified matrix, the two moved seams, and the install row
that still waits on the CLI's npm publication (the `alpha` dist-tag
lags the release).
2026-09-09 23:03:10 +08:00
Bowen Liang ade57b2544 test(host): pin the envelope system list between folds 2026-09-09 02:43:35 +08:00
Bowen Liang 6819fa7fe6 feat(compat): fold the V2 and V3 session-log generations alongside V0
The supported range (dsh 0.1.2-rc.1+) spans three durable-log generations, and the fold only read the V0 spellings — so on 0.1.5-alpha.1 sessions the system prompt priced 0, TTFT/generation stayed 0, and startSeq/endSeq replacements appended instead of replacing.

Read SHAPES, never a detected version (the probe can be wrong: a healed profile mirror may name another release than the running harness):

- system/message surface nodes feed systemTokens plus a bounded systems list (the harness's "last nonempty surviving system" rule), replacing the V0/V2 request/header.header.system envelope figure; the browser resolves the prompt in force per step and fetches its text from either event shape.
- The first token comes from the embedded assistant stream (assistant/message.data.stream / assistant/attempt.data.stream) when the log carries no V0 assistant/chunk deltas, mirroring dsh-llm's assistantStreamFirstTokenTime.
- Replacement endpoints accept both startSeq/endSeq and start/end.
- tool/ptc-dispatch books nested file ops alongside tool/code-dispatch.

Statistics were differentially verified against the harness's OWN projection values on real V0, V3-native, and V3-migrated logs: system/tools/message tokens, request and turn counts, TTFT, generation, tool time, and every billed cost bucket match exactly.

The new state fields are additive-optional, so stateVersion stays 15: a bump would orphan the key for idle sessions (the #37 regression).
2026-09-09 02:09:38 +08:00
bowenliang123 ba074e078e fix(host): backfill pre-step message ids so stripped messages cannot brick sessions
A durable `user/message` event lacking a non-empty string `data.id` passes
the runtime append unchecked but makes the harness refuse the whole session
at its next load (`... lacks an identified message`, issue #51). The archived
wrap-up notice carried a rebuild signature (role-first key order, `id` and
`summary` stripped) that no shipped harness producer emits, so the strip
happens upstream of the append, outside both audited codebases.

Guard the durability boundary from the plugin side: a prepended
`agent/pre-step` listener runs last in the waterfall and mints a `dshctx-`
prefixed id for any decision message that would persist unidentified,
leaving everything else — including the decision's reference identity —
verbatim, and failing open on hostile entries. The seam is pinned per
baseline (the event dispatch, the user/message append that consumes it,
and the prepend option) in the real-code matrix.
2026-09-07 15:12:33 +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 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 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 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
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 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 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 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 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 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 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
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 6275ab00d7 fix(host): keep the projection folds total over untrusted session-log events
The projection registry drives apply straight off the session/event bus
with no error boundary: one throwing fold stalls the unit's cells and
the contextTimeline push feed — the browser waits on "loading" forever.
Three real throw paths existed, and one of them also broke the
plain-JSON checkpoint precondition (one undefined-valued property fails
EVERY projection-cache write for the session):

- applyTimeline now isolates each event: a malformed one is dropped
  whole (all-or-nothing — the state reference is untouched), so no
  event shape, present or future, can take the fold down.
- A tool/result with no paired tool/call (aged-out call event, foreign
  producer, duplicate callId) no longer stamps an undefined-valued
  node.tool onto the surface node.
- pricing and header-epoch folds tolerate null or primitive elements
  inside content-block and tool arrays instead of throwing on them.

Hostile-fixture tests lock each path: a log missing data.message, null
blocks nesting inside tool-result content, unpaired results, and junk
tool entries — every fold state still losslessly JSON-serializable.
2026-08-28 00:06:02 +08:00
bowenliang123 1a046f63c5 feat(settings): add a default sort preference for File Activity 2026-08-26 11:28:49 +08:00
Bowen Liang da394d7e0a refactor(context): drop the 'Top Tool Schemas' chip row from the overview
The chip row lived in CurrentComposition, was fed the fold's per-tool
toolList summary, and the chips focused the Context browser on a tool
via a one-shot toolFocus callback. The browser already exposes the
tool schemas through its headers section, so the chip row was a
duplicate entry point that no longer pulled its weight.

Removes the lc-tools DOM block, the toolFocus/onToolFocusHandled
prop wiring across ContextView/ContextModal/Browser, the
'tools.top'/'tools.more' translations, the .lc-tools* CSS, and the
now-unused toolList summary across the wire + state schemas
(stateVersion 9 -> 10). estimateToolSchema stays because headers.ts
still prices individual tools for the browser's tool-schema cards.
2026-08-26 03:45:04 +08:00
Bowen Liang f106b720ac test: rebuild the suite at source level with 100% per-file coverage gates
The old suite exercised the built lib/ bundle, so v8 remapped almost
nothing (8% src coverage) and every run needed a build first. The new
suite imports src/ directly under two vitest projects (node for
host/shared, jsdom for client), rendering real React 18 trees against
the real dictionaries and ui-primitives, with faithful in-memory
harness faces (cordis inject/effect, locale chain, slots, sessions
provide) instead of plugin mocks.

- 43 fine-grained, stateless, single-module spec files (606 tests)
  forked across workers: ~2.5s wall, no build step
- v8 thresholds at 100/100/100/100 per file over src/**
- host specs drive the real projection definitions (init/apply/view)
  plus one full cordis + SessionStore + SessionProjectionRegistry
  integration; the framework's snapshotJsonValue pins the
  projection-cache precondition on every intermediate fold state
- oxlint's src ruleset now covers tests/** as well
- typecheck covers the tests via tsconfig.tests.json
2026-08-26 00:58:39 +08:00