Commit Graph
17 Commits
Author SHA1 Message Date
bowenliang123 dd05bbda4e feat(client): price a file attachment by the handle the model receives
A file block never reaches a model as bytes: dsh projects every one to
`fileHandleText` at request assembly, and the token meter prices that handle in
place of the block's structural JSON price. An attachment therefore costs a
fixed ~60 tokens whatever its size, so the card now shows that price beside the
size — computed from the three facts a log carries (name, byte size, digest
prefix) and priced with the meter's own text arm, now shared with the fold's
message price instead of restated there.

The handle also names the stored copy's read path, which only the live
environment resolves and no log holds, so the row prices the pathless arm and
reads as a lower bound (`≥`) rather than an `≈`. The fold keeps the meter's
fixed structural price for the same block, exactly as the harness does: one
figure answers the request, the other the surface.
2026-10-13 23:15:39 +08:00
bowenliang123 6487602f1e feat(client): render message attachments in order and preview them in the sidebar
A user message's image and file blocks now render as one ordered attachment
block in the Context browser: images keep their session-authorized thumbnail,
other files show their type glyph, extension, and stored size, and the row
chip counts every attachment. One click IS the preview — the card hands the
attachment's named host path to dsh's own preview column (the File Activity
card's `previewAddressOf` → `openResourceVia` path), which renders markdown,
code, PDF, office, and image viewers under the file's real name. A refused
open reports on that card alone; the image lightbox and the per-type modal it
replaced are gone with `images.tsx`.

The path comes from the plugin's own `/api/dsh-context/attachment` route (the
same authenticated `/api` fence, Connection exact-route registry, and
cold-read gate as the detail and fleet routes). It answers only an id the
session's own log cites — a depth-bounded, total walk over the event payload
(user content, tool-result blocks, spliced inbox deliveries) — and answers
the store's named alias: `attachments.fileHostPath` for verbatim files, and a
published-or-reused named hard link for images, whose objects are digest-named
and suffixless while the preview column dispatches viewers by suffix. The
link shares the immutable object bytes and lives in a disposable cache
(`<home>/cache/dsh-context/previews/`); a store that is not host-file-backed,
or a media type the harness never admits, resolves the typed null the card
reports as an unavailable preview.

`Section` moves to its own module so the attachment block heads itself
without importing the tool card, and the compat matrix probes `FileTypeIcon`
alongside the icons it already pins.
2026-10-13 21:19:47 +08:00
bowenliang123 87a2893190 refactor(comments): clean up the test-suite comments to the guideline
Apply the AGENTS.md comments guideline to every .ts under tests/: remove
what the code already expresses, keep only what it cannot.

Removed: spec-header coverage inventories the describe/test tree already
names; comments restating the next line's assertion or its message; helper
JSDoc whose body already says it; within-file duplicates; five stale
claims (trendChart's "six aggregated turns" against agg.length === 8,
agentGraph's "six categories" against CATS' seven, fold-timing's "cheapest
call LAST" against a loop that feeds it first, fold-surface's unreachable
plain-JSON note, contextView's "two turns" for a one-turn fixture).

Kept and condensed: the projection-freeze class, strict wire/state-schema
hazards, retention and pricing windows, pairing rules, hostile-input
rationale, fixture and expectation-array ground truth, jsdom mechanics,
and issue-numbered regressions.

Comment lines 2604 -> 2014 (-22%); 85 module headers kept, 30 coverage
inventories dropped; every surviving comment line is within the 140-column
limit, with 36 wrapped lines joined onto fuller ones.

No logic, string, identifier, import or directive changed: a comment-strip
gate proves each file's comment-free code is byte-identical to HEAD, and a
line-level proof pairs all 845 changed lines with an unchanged code part.
pnpm run lint && pnpm run test (106 files / 2061 tests, per-file 100%
coverage) && pnpm run build all pass.
2026-10-10 00:50:55 +08:00
bowenliang123 7c87ce2b76 refactor(comments): rewrap test comments onto fuller lines
Merge consecutive short comment lines in tests/** up to the 140-col limit
without touching their text: comment-stripped content, comment text (whitespace
normalised) and line-comment runs are all proven identical per file. This is the
line-count half of the cleanup only; the content pass over tests/** is still
outstanding.
2026-10-10 00:26:42 +08:00
bowenliang123 6051e39180 feat(overview): a custom date range beside the range presets
The Insights page's range group gains a calendar picker: one panel that
takes BOTH ends, in either order. A backwards pick — end before start —
is sorted on the second click, so `orderedDayRange` hands every reader
downstream (the scope window, the ledger-day predicate, the chip that
names it) a pair with `from <= to`; `dayRangeWindow` orders it again at
the seam, so a hand-built pair can never filter the page to nothing.

The trigger IS the scope: one pill carrying the start and the end, fed
the LIVE window — a picked range when one owns the scope, the preset's
own window read as days otherwise, so "last 30 days" prints the thirty
days it covers and the two controls can never disagree. Only a picked
range takes the chip's brand voice and grows the way back, and the panel
keeps no second copy of the two dates: the chip above it is already
showing them, live, while the pick runs.

Underneath, the range model moves from a start-only floor to a two-sided
`RangeWindow`, which closes the top of a picked scope: the ledger-day
predicate gains the ceiling it never had, so a day beyond the range no
longer leaks into the skill card, and both picked days count whole
(midnight through the last millisecond). `days.ts` gains the calendar
arithmetic this needs — a day's epoch bounds, its weekday, month
stepping, and one month laid out as Sunday-first weeks — with the
round-trip key validation its siblings already used.

The panel portals to `<body>` and seats itself off its trigger's rect,
the containment escape the page's hover tips already take: the page is
the `lc-ov` query container, so an in-tree popover would be both
captured and clipped by the one scroller. It measures itself before
paint, clamps inside the viewport, flips above the trigger when it would
fall off the bottom, and retracts on any scroll, resize, outside press,
or Escape.

Closes #128
2026-10-09 16:49:07 +08:00
bowenliang123 a19c86c593 chore: drop the dead compareVersions and unexport nothing-imported symbols
- compareVersions had zero production callers (the gate rides
  meetsBaseline alone) and is not re-exported through the package entry;
  its spec block duplicated what meetsBaseline's cases already pin
  through compareParsed (a major-gap case joins meetsBaseline's block to
  keep every arm covered). isNewerVersion keeps its own numeric-only
  compare: ignoring prereleases there is the upgrade nudge's deliberate
  semantics, and adopting the channel-aware order would change
  user-visible behavior.
- pad2 hand-rolled padStart(2, '0') in a function that already used
  padStart for the year.
- StoredHeaderTool/StoredHeaderRecord and DecodeCounts/DecodeSpans lost
  export keywords nothing inside or outside the repo imported.
2026-10-08 01:05:01 +08:00
bowenliang123 a988c9a8fb feat(compat): drop dsh 0.1.5-rc.1, floor the support matrix at 0.1.7-rc.2
The supported set is now the two V4-generation lines (0.1.7-rc.2+ and
0.2.0-rc.2+): the manifest's dshReleases, the peer floors (>=0.1.7-rc.2,
cordis ^4.0.4 — both lines vendor 4.0.4), the devDependency pins, the
baseline matrix, the release workflow's tag fetch, and the compatibility
docs.

With every supported line on session format V4, the V3-only paths leave:
the fold reads the tool result's first-class message alone (source.callId
pairing, message.isError — the admission validation refuses any row whose
data.error outlives it); the settings.register host face module is gone
(the Config-form entry schema serves the preferences); the client drops
the settingsScope/settings.plugin.item card seat, the retired sessions.open
jump fallback, the legacy icon spellings, and the settings-dialog leg of
the settings jump; the history page's tool/result mapper reads the V4
message (its V3 wrapper reads were dropping result content on supported
lines); the cache probe rides the 2-arg cachedSnapshot face. The baseline
gate stays: a below-floor harness composes the fallback units and the
upgrade prompt, and an undetectable version still fails open.

Verified: pnpm run lint:fix && pnpm run test && pnpm run build green (1918
tests, 100% per-file coverage); the compat matrix passes against the real
dsh sources at both baseline tags; live on dsh 0.2.0-rc.2 the plugin
composes, the Plugins-page preference card renders and round-trips a
write, the insights warm-up fills the heatmap, and the session jump and
Context tab work end to end.
2026-10-07 23:37:52 +08:00
bowenliang123 9acbf7eb45 feat: support dsh 0.1.5-rc.1+ and 0.1.7-rc.2+ as the supported lines
The supported range is now two baselines - dsh-v0.1.5-rc.1 (session
format V3) and dsh-v0.1.7-rc.2 (V4) - enforced by the runtime gate at
BASELINE_DSH_VERSION = 0.1.5-rc.1; below it the fallback units and the
upgrade modal take over (0.1.2/0.1.3 hosts were previously served by the
real folds).

The pre-V3 log shapes are dead code on every supported harness: the
running harness migrates historical logs on every read path (or refuses
the session wholesale), and 0.1.5-rc.1 rejects a request/header carrying
header.system outright. The fold therefore drops the V0/V2-only paths -
the header.system envelope system prompt, assistant/chunk floods,
{start, end} replacement endpoints, tool/code-dispatch - and the state
fields only they wrote (systemsFromHeader, the stepStart decode
accumulators). stateVersion stays 20: no supported log folds
differently, and pre-V3-era cached rows are discarded by the projection
cache's identity gate (and the strict stepStart sub-schema) instead of
served. The headers unit folds metadata-only epochs (envelope
systemTokens write dropped; the v1 cached-row read-compat and its pinned
stateVersion 1 stay); replaceRangeOf speaks startSeq/endSeq only; the
browser's epoch fetch maps request/header to tools and system/message to
the prompt text.

tests/baselines.ts re-fits to the two entries (the right Sidebar seam is
required on both), the compat matrix drives both tags' REAL sources
(canonical logs re-shaped to V3, the hostile probe renumbered past them),
and the version fixtures pin the new floor. Manifest: dshReleases
{0.1.5-rc.1, 0.1.7-rc.2}, dsh peers >=0.1.5-rc.1 (satisfies the 0.1.7
admission check under includePrerelease), devDeps pinned to the
0.1.5-rc.1 surface plus the ui-primitives runtime closure the client
lane inlines (test-only), and the release workflow fetches the two tags.
docs/compatibility.md is rewritten around the two-line matrix; README,
AGENTS.md and the issue template follow.

Also riding this move, from the audit pass: the fold re-proves an inject
source's form before it books the event record (the harness validates
`kind` but not `form` - a hostile one previously failed the strict
wire/state schemas on every delivery, the #44 freeze class), the headers
unit guards a data-less envelope (it has no try/catch of its own), the
schemastery schema annotations drop the explicit z<T> wrappers the 3.18.4
Mode generics reject, and the version-history note records why
stateVersion is deliberately not bumped.
2026-09-25 01:08:38 +08:00
bowenliang123 51acdf498a fix(client): anchor the activity heatmap on Sunday-first rows
The weekday gutter drew 一/三/五 on rows 1/3/5 while the grid was
Monday-first, so every caption named the row below its day - today's
Thursday cell read as Wednesday. Rows now start on Sunday (row 0 =
Sunday): days.ts renames mondayOfWeek to sundayOfWeek and anchors on
getDay() directly, the grid builds off that anchor, and the labels key
by the row index they name so no row arithmetic can shift them again.
The month gutter is untouched - the label still marks the column
containing the month's 1st, now read off the column's Saturday. Specs
re-derive every pinned date from the Sunday-first calendar.
2026-09-24 16:04:24 +08:00
bowenliang123 de1c848698 test: let the meta dictionaries diverge from the manifest copy
The en/zh title and description are the canonical Plugins-page copy, not
a restatement of the package.json fields - the manifest values only
back the en fallback when a dictionary entry is absent. The en/manifest
equality assertions drop; the zh spec now pins that it is a real
translation (same fields, different copy) instead of only matching
en's shape.
2026-09-23 17:00:38 +08:00
bowenliang123 aa0c0a5f57 feat: localize the Plugins-page title and description
The Host's package-meta reader serves each plugin's localized
title/description from the dictionaries exported under locale/ - the
same surface the official plugins use, so the Plugins page and the
settings inventory present dsh-context in the active locale instead of
the raw manifest fields. en carries the canonical copy (matching the
manifest name/description it overlays), zh the translation; the
exports pattern and the files list ship both dictionaries. A spec pins
the dictionaries' shape, the exports/files alignment, and the en/manifest
agreement.
2026-09-23 16:52:20 +08:00
Bowen Liang e33deda752 feat(host): add the contextActivity daily-ledger projection with a cold-read backfill
The Context Dashboard's heatmap and session cards read the session list's
projection column, which only serves durable cache rows. The new
contextActivity unit folds each assistant settlement into a per-day ledger
(billed tokens + completed requests, keyed by local day, 400-day retention)
alongside the existing contextTimeline/contextHeaders units.

A session folded before a unit existed — or one that predates the plugin
entirely — has no row until it next goes live, so a one-pass warm-up
(backfill.ts) cold-reads every such session's durable log through the
projection cache's own cold-read ladder (coldSnapshot): each unit seeds
from its cached rows, folds the remainder, and the refreshed checkpoint is
written back. Live sessions are skipped (a throwing liveness probe skips
conservatively too), unreadable logs are logged and skipped per session,
and the whole pass rides a deferred inject across the
sessionQuery/sessionProjectionCache/sessionPersistence/sessions services.
2026-09-16 08:55:49 +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
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 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
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