Commit Graph
880 Commits
Author SHA1 Message Date
bowenliang123 537e53a56a refactor(comments): trim comments in the build configs
Drop the restated build narration in tsdown.config.ts and vitest.config.ts and
keep only the decisions a reader cannot recover from the code: the platform
module subset, the pinned native toolchain, the virtual-id suffix rule, the
unminified/typed host half, and the compat lane's preconditions. Stop claiming
to "mirror" the harness lists, which these are subsets of.
2026-10-10 00:26:24 +08:00
bowenliang123 7cb0a9fa46 refactor(comments): trim client comments to the value the code cannot carry
Apply the repo's comments guideline to src/client/**: delete comments that
restate JSX, Tailwind utilities or standard React behaviour, narrate history,
duplicate docs/, or describe what the next line does; keep only container-query
and containment traps, portal requirements, reference-stability rules with a
real re-render consequence, CSS<->TS constant parities, harness seat and payload
contracts. Align the page's name with the shipped UI label ("Context Insights",
not "Context Dashboard"), unify drifted phrasings, drop the last non-English
fragments, and rewrap survivors onto fewer, fuller lines within the 140-col limit.

Comment lines 5,183 -> 1,822. No code change: comment-stripped content is
identical per file; the client bundle differs only by dead Tailwind utilities
that existed because deleted comments contained those words.
2026-10-10 00:26:03 +08:00
bowenliang123 3fe1b7cf77 refactor(comments): trim host/shared comments to the value the code cannot carry
Apply the repo's comments guideline to src/host/** and src/shared/**: delete
comments that restate the adjacent declaration, narrate development history,
duplicate docs/compatibility.md, or walk through a function body; keep only
critical decisions and facts the code cannot self-explain (harness and log-shape
contracts, retention invariants, untrusted-input traps, plain-JSON cache
preconditions). Rewrap the survivors onto fewer, fuller lines within the 140-col
lint limit, and drop the last non-English comment fragments.

Comment lines 4,020 -> 1,026. No code change: every file is proven identical
after comment stripping, and the built lib/index.js is byte-identical to the
previous build (modulo the build-time banner).
2026-10-10 00:25:27 +08:00
bowenliang123 f90bd987fe docs(agents): refine comments guideline for clarity and conciseness 2026-10-10 00:07:00 +08:00
bowenliang123 79fe95e2cf docs(agents): update comments guideline for clarity and conciseness 2026-10-09 23:49:48 +08:00
bowenliang123 def268b7bb perf(trend): rise only the newest columns on mount
Every retained bar carried `animate-lc-bar-in`, so the chart's mount animated
each one — the host retains up to maxRequestSteps (1500) requests — and every
animating bar layerizes on its own. Only the columns a pane can reach are ever
seen: the chart mounts anchored to the newest bars, so the rise is now drawn by
the newest RISE_CAP (200) columns, the stagger counted from the window's edge
so those columns still cascade left to right. Bars appended to a live session
keep rising, and a log shorter than the window is unchanged.

Measured on the 218-step session, the per-bar animations cost 17ms of the worst
frame gap and 16ms of blocking on top of the strip fix. With both changes the
open measures 243 animations, a 133ms worst frame gap (was 250ms) and 98ms of
blocking (was 173ms); with every CSS animation disabled it measures the same
133ms, so what remains is plain mount cost. Three tests pin the window in
total, DNA and DNA+delta modes.
2026-10-09 23:42:35 +08:00
bowenliang123 20826dcaf7 perf(timing): sweep the strip's bands on one wrapper, not one animation each
Opening the Context tab on the 218-step session started 1584 CSS animations at
once, 1324 of them the timing card's strip — one `animate-lc-stacked-in` per
band. Tracing the open showed 1582 animationstart / 1578 animationend
dispatches, 343 Layerize events (392ms of layer churn) and a 250ms worst frame
gap where the shell's own tab switch measures 67-83ms. Disabling just the
strip's per-band animations on the same build already cut that open to a 150ms
gap and 109ms of blocking (from 173ms).

The bands now ride one `.lc-tstrip-fill` wrapper carrying the single sweep, so
the strip wipes open on one composited transform over the same track. Packing,
colours, the kind hover link and the tip are untouched; the per-band ripple it
replaces was capped at eight stagger slots, so a packed strip already settled
as a sweep. A regression test pins the single wrapper and the absence of any
per-band animation class.
2026-10-09 23:42:13 +08:00
bowenliang123 0a57e3376c docs(agents): add guidance for reviewing recent issues and PRs in release notes 2026-10-09 23:27:34 +08:00
bowenliang123 6475afc8d6 fix(client): end the price-book retry cycle when its last subscriber leaves
The backoff timer re-armed unconditionally after every failed read, so an
unreachable models.dev registry kept being polled for the rest of the page's
life even after the Insights page that subscribed had gone. Measured with
fake timers: 7 polls in the 20 minutes after the last unsubscribe, at the
capped 240s cadence, forever.

The timer callback now ends the cycle when no subscriber remains; the next
subscribe already kicks a fresh read. Measured after: 0 polls in the same
window, and the first subscribe still fills the book as before.
2026-10-09 22:42:07 +08:00
bowenliang123 115e926765 fix(client): bound the page-lifetime detail payload cache
The per-session DetailStore map is page-lifetime and every store kept its
fetched collections forever, so the page's heap grew with each session ever
opened. Measured over 24 distinct visits with planted weak references: 9, 17
and then 25 live payloads (one per visit), ~194KB of JSON (~300KB of live
heap) each — nothing released them but a reload.

The newest DETAIL_STORES_MAX sessions now keep their payload for an instant
re-open; older idle ones are demoted (payload and ledger dropped) and read
again when opened. Measured after: a flat 8 live payloads at 8, 16 and 24
visits, the two oldest re-opens read once more, the newest re-opens read
nothing. An active store is never demoted, so the view on screen keeps its
data; rendering was byte-identical across the change.
2026-10-09 22:40:20 +08:00
bowenliang123 45e169749f fix(client): park the detail store when its last viewer leaves
The per-session DetailStore map is page-lifetime, so a store whose Context
tab, sidebar panel, or /context modal had gone kept trailing its own read
schedule: a broken detail route was re-POSTed every 2.4s for the rest of the
page's life with nobody reading the answer (measured on a real session: 2
extra POSTs in the 3s after unmount, unbounded).

The last unsubscribe now parks the store: the armed window is cleared and no
read is armed while nobody subscribes. The cached payload is kept, so a
re-open still renders instantly (measured: identical rendering digest and no
refetch on remount); a resume with nothing cached clears the ledger and the
old backoff, so the mount's own request(rev) reads again instead of sticking
on the retry note.
2026-10-09 22:35:31 +08:00
bowenliang123andClaude Opus 4.8 e1a6655496 fix(overview): halve the heatmap legend swatches
The Less→More key reused the grid's own 20px cells, so the key read as a
second row of cells competing with the grid. Its swatches now render at half
size and keep the grid's depth classes, so the key stays a caption while
never drifting from the colors it explains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-09 18:06:35 +08:00
bowenliang123 5856324fc5 i18n(timing): label the decode-text bucket 模型回复 / Answers
Rename the timing-stats text row from 模型输出/Answer to 模型回复/Answers to match the Answer wording already used by the stats card, and update the affected assertions.
2026-10-09 17:58:32 +08:00
bowenliang123 70d2889b4c docs(agents): add guidance for reviewing recent issues and PRs in release notes 2026-10-09 17:46:31 +08:00
bowenliang123 def8b2b74a chore: release v0.66.1 2026-10-09 17:29:50 +08:00
bowenliang123 aa065729f4 [c857d8c] feat(agents): add instruction to load and run code-review skill for changes verification 2026-10-09 17:22:02 +08:00
bowenliang123 d63ccdd576 fix(plugin-info): drop the update notice onto its own line
The notice trailed the version inside the value box, whose ellipsis cuts
the line last-in-first-out, so at the card's 240px floor it was always the
part lost. Measured in the live harness with English labels, the box
overflowed by 29-44px and ~15px of a 52px chip survived, at every version
string tried. A block-level chip drops below instead, leaving the version
its own ellipsis on the first line.
2026-10-09 17:16:03 +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 d3f2399440 feat(overview): scope the first range option to today, not the last 24h
The Insights page's first range option read "Last 24 hours" while the
ledger it scopes is day-grained, so the rolling window and the day floor
underneath it disagreed: past midnight the 24h window still admitted
yesterday's ledger day whole, and before 1am it admitted only part of it.
"Today" is the local calendar day from midnight — the window the day-
granular heatmap beside it already draws.

`rangeStartOf` opens the window on the local midnight by date-field
arithmetic (the harness's own `setHours(0, 0, 0, 0)` idiom, as in
`startOfLocalDay`), which stays exact across DST-short and DST-long days
where an epoch-ms subtract drifts into the neighbouring day. The harness
helper itself is only reachable through a deep
`@deepseek-ai/dsh-client-ui-chat/src/*` path — a package this plugin does
not depend on — so the idiom is mirrored at its one call site instead of
taking the dependency.

Both locales carry the shorter 今天 / Today; the deprecated
`ov.range.24h` key is gone rather than aliased.

The specs that anchored time on raw epoch offsets silently assumed UTC
(`11 * 86_400_000`), and the panel fixture at `NOW - 1h` would have
failed whenever the suite runs in the first hour after midnight; every
boundary is now anchored on a local `Date`, so the day floor is proved
identically in every timezone.
2026-10-09 15:06:22 +08:00
bowenliang123 9d2c812638 fix(tsdown): trim whitespace in SVG markup loading 2026-10-09 11:34:12 +08:00
bowenliang123 00df001055 feat(overview): filter the skill card by name
A case-insensitive substring filter rides the skill card's totals row,
narrowing the rows, the summary counts, and the overflow line; the
input stays mounted on an empty match, next to a zeroed summary and a
dedicated no-match note. The bars keep the scope's heaviest tally as
their denominator so they hold still while the filter narrows.

A pin the filter hides now falls back to the detached detail block with
its head row — previously the head rendered only for pins outside the
scope, leaving a filter-hidden (or render-bound-hidden) pin unnamed.
2026-10-09 10:53:45 +08:00
bowenliang123 f60e1a5643 build(deps): carry zustand and immer, the rest of the phantom set
@deepseek-ai/dsh-client-store declares both under devDependencies
while its published lib/index.js imports them, so the client lane's
inlined harness bundle needs them hoisted at the root exactly like
`diff` in the previous commit. A clean-room install of the lockfile now
resolves the whole runtime import graph from the checkout alone:
zustand, immer, diff, shiki, katex, anser, clsx, simple-icons and the
micromark/mdast set.
2026-10-09 01:41:58 +08:00
bowenliang123 95d36a3013 build(deps): carry diff, the harness bundle's phantom dependency
@deepseek-ai/dsh-client-ui-primitives declares `diff` under
devDependencies while its published lib/index.js imports it, so the bare
import only resolves when the consumer's workspace hoists it — exactly
how shiki, katex, anser, clsx and the micromark/mdast set are already
carried here. `diff` was the one name missing, which failed the release
workflow's `pnpm test` with "Failed to resolve import \"diff\"" across 47
client suites while passing on any tree with a hoisted copy above it.
2026-10-09 01:37:43 +08:00
bowenliang123 4ae8e2338b chore: release v0.66.0 2026-10-09 01:33:18 +08:00
bowenliang123 95d7ef3830 build(tsdown): leave the host half unminified
The host half runs in Node straight off disk, where parse time is irrelevant
and no sourcemap ships beside it, so minifying only cost stack-trace
readability. The browser half stays minified: it is the artifact the browser
downloads and parses.

v0.65.0: lib/index.js 56.87 -> 99.11 kB (gzip 16.85 -> 24.21), comments
already stripped.
2026-10-09 01:28:53 +08:00
bowenliang123 a5a82cf213 build(tsdown): strip comments from the published artifacts
Render both halves with `comments: { legal: true, annotation: false, jsdoc:
false }` (rolldown's OutputOptions). The `/* @__PURE__ */` and `/* v8 ignore
*/` markers that survive the minifier are build-time input whose only
remaining effect in the output is bytes, and plain source comments are noise
in a shipped artifact; legal comments stay for licenses, while `index.d.ts`
keeps the JSDoc that documents the integration contract.

v0.65.0: lib/client.js 355.18 -> 336.46 kB (gzip 94.09 -> 91.32), lib/index.js
57.33 -> 56.87 kB.
2026-10-09 01:28:13 +08:00
bowenliang123 3775107a07 build(tsdown): pin the client browser target and gate bundled deps
The client bundle is a classic script the shell loads into its module
table, never a Node module, so its target must not fall back to
engines.node — that fallback also tripped tsdown's legacy-CJS warning on
every build. deps.onlyBundle turns the "everything else inlines" rule
into an enforced allowlist: any other node_modules package inlined into
the client bundle fails the build.
2026-10-09 01:05:49 +08:00
bowenliang123 44e8e18a06 build(tsdown): minify both published artifacts 2026-10-09 01:00:26 +08:00
bowenliang123 48302d1962 feat(overview): the activity column's skill-load card
The Context Insights page's second insight card lists every skill loaded
within the page's scope (the range floor, or the heatmap's pinned day):
a color-dotted row per name with its load tally, a proportional share
bar scaled to the heaviest row, and the last load's relative time,
ordered by the title's loads / last-used toggle. A row click pins the
session list to that skill's loaders; the pinned row wraps with its
detail into one ringed unit — the description first, then a fact grid
(origin, usage, exact last load, the full path as a click-to-copy
value). A structured hover card answers the same in a floating register.
The catalog read joins description, origin, and path onto the tallies by
name and degrades to bare tallies when the route cannot serve.
2026-10-09 00:57:42 +08:00
bowenliang123 b424e59a5b feat(host): tally daily skill loads and serve the skill catalog
The activity ledger's day entry gains a per-name skill table, booked from
the two durable load gestures the timeline fold tags sub: 'skill' — a
/name invocation's skill-invocation source, and a skill-tool result's
<skill_content> wrapper trusted only when its callId pairs with a pending
skill-tool call (a read of source text quoting the wrapper books
nothing). Tables are bounded per day and the pairing list per session;
the state shape change bumps the unit's stateVersion (3 → 4) so cached
rows refold on upgrade.

The plugin-owned /api/dsh-context/skills route serves what a loaded skill
IS — registry description, origin bucket, on-disk path — resolving the
session's cwd and preset scope the way the harness's own skill catalog
does (web compositions mount discovery on presets, so the global registry
alone lists nothing), every face re-proved per request.
2026-10-09 00:57:09 +08:00
bowenliang123 540e067f99 refactor(i18n): name the settings entry "Plugin Settings"
The bare "Settings" / "设置" label read as the shell's own settings next
to the plugin's; both the page-heading chip and the plugin-info row now
name the plugin's own page.
2026-10-08 18:16:37 +08:00
bowenliang123 4f5b266ba8 refactor(overview): move the plugin settings entry into the page heading
The Context Insights page's preferences entry sat as a full-width row
under the activity heatmap, at the bottom of the insight column and easy
to miss. It now rides the page heading as a quiet chip directly right of
the title, cut like the balance capsule beside it, with the long
"open plugin settings" hint carried on the hover title.
2026-10-08 18:16:11 +08:00
bowenliang123 f9d60414fd docs(readme): add recommendation from DeepSeek Harness staff 2026-10-08 17:52:54 +08:00
bowenliang123 7ec8ff251f fix(sidebar): ink the panel glyph at the harness line weight (#124)
The shell's own panel rows draw their glyphs with the icon set's one-unit
stroke in a 16-unit box — 6.25% of the icon edge — while the sheet's bars
measure 98-101 units of its 1024 artboard (9.6-9.9%), so the row read half
again as heavy as the Plugins and Automation tasks rows beside it; at that
weight antialiasing also filled the strokes enough to look darker than
neighbours that ride the very same `--dsw-alias-label-primary` currentColor
in both themes.

The mono seat now derives the thin glyph from the SAME single artwork
instead of shipping a second SVG: the sheet masks down to the harness
weight by 17 units a side (98 -> 64) as the intersection of the drawing
with its own copies shifted along each axis — a white flood minus one
black shift per direction, every shift painted through the drawing's
complement mask, which stays the only copy of the artwork on the page.
Compositing, not a filter: `feMorphology` rounds its radius to whole
device pixels, so it measured as a no-op at 1x and an over-thin at 2x.
The polychrome identity seats (tab chip, /context entry, guide capsule)
are untouched.

Verified: live strokes measure 17px against the clock's 17px and the
pinwheel's 19px at 256px; screenshots in light and dark; lint:fix + test
(100% per-file coverage) + build.
2026-10-08 17:41:04 +08:00
bowenliang123 d4d5f52734 feat(stats): figure cells jump into the context browser
The session node's second-row cells (Skill Loads / Answers) become
keyboard-accessible jumps (role=button + Enter/Space, the team card's
idiom): clicking scrolls the Context Browser card into view and opens
the matching section on the live surface — skill loads expand Skill
Injections with the row lens cleared; answers expand Assistant Messages
with the Answer kind chip picked, so tool-call-only steps filter out.
Hover/focus carry the team's affordance (soft wash, brightened label,
brand ring).

The browser gains a one-shot `catFocus` prop mirroring `nodeFocus`:
select live, open the named category, pick the optional kind chip,
clear the lens, report handled so the same cell can fire again. The
cells stay inert where no browser mounts (no handler, no affordance);
tooltips name the destination in both locales.

Verified end to end: lint:fix + test (100% per-file coverage) + build.
2026-10-08 16:22:41 +08:00
bowenliang123 16e3a2083e feat(stats): second session-node row — skill loads & answers figures
The Current Session node now reads two figure rows: turns/steps above
skill loads/answers, each row's cells splitting the width evenly and
centering (one column pair whatever a label's length).

- Skill Loads (counts.skills): the distinct skill names among the
  retained skill-tagged inject events — `/name` invocations and
  `skill`-tool loads; the `<available_skills>` catalog digest rides an
  untagged inject event and never counts. Derived at view time, so no
  stateVersion bump: cached rows recompute the exact tally on serve.
- Answers (state.answers, head scalar): one per assistant message
  carrying a non-blank text block — tool-call-only steps are work, not
  replies. A running total over the complete log (the humanInputs
  precedent), so the timeline stateVersion bumps 26 → 27 and the v20
  warm-up rebuilds idle sessions' rows on the first dashboard open.
- The subagent count leaves the session node (the Agent Team ledger
  row still carries it); docs/compatibility.md records the bump.

Verified end to end: lint:fix + test (100% per-file coverage) + build.
2026-10-08 16:20:54 +08:00
bowenliang123 36a1cc1031 docs(readme): update context insights dashboard snapshot image 2026-10-08 13:39:08 +08:00
bowenliang123 eceabd1f1e docs(readme): update context stats snapshot image 2026-10-08 13:35:58 +08:00
bowenliang123 7c3d1f20c6 docs(readme): update context trend snapshot image 2026-10-08 13:34:38 +08:00
bowenliang123 4199d9ea2d style(trend): quiet the duration curve — tertiary gray with a knockout halo
The overlay rode the brand token (near-black in light, near-white in
dark), making the secondary curve the loudest element on the chart and
colliding with the bar hover outline and the DNA highlight slices that
share the token. It now paints in label-tertiary gray — the right-hand
axis labels' own family — at a thinner 1.25px, over a 3px card-bg
underlay that knocks the bar colors out behind the line so it lifts
clear instead of fighting them.
2026-10-08 11:37:09 +08:00
bowenliang123 f6b59f3041 feat(browser): predict the pending step stamp for the live tail
Rows the log holds no request for yet — the newest assistant reply, the
tool results still waiting to be sent — now wear a PENDING step stamp
instead of none. The prediction follows the harness's step loop
(agent.ts): a newest user message opens a new turn, a text-only final
reply means the turn ended, and pending tool results or an in-flight
injection continue the same turn, so the next step is lastStep + 1.
dsh names that step at step/start before the request exists — the
session log's own tail confirms the prediction (step/start Turn 1 Step
42 over exactly the rows that lacked a stamp).

The estimate reads like the confirmed stamp but with a quiet dotted
underline and a 'not yet sent' tooltip in both locales; the confirmed
stamp replaces it the moment the request logs. Past-step views never
speculate: they reconstruct only what their request already carried.
2026-10-08 09:06:07 +08:00
bowenliang123 f1befe8f32 feat(browser): stamp each context row with its introducing turn/step
Every context-browser row (system prompt, tool schemas, user/assistant/
tool/skill/inject messages) now names the turn/step of the request that
first carried the item. The stamp derives client-side from the first
request logged after the item's seq — assemble's inclusion rule is
n.seq < R.seq — the same next-request attachment the events card uses,
so no wire change and graceful degradation on older rows.

The stamp merges with the row clock into ONE muted meta run
('T1 S41 · 22:58:10'), so the meta tail reads as a single unit instead
of three separate clusters; the token figure stays separate. Hover
explains the stamp in both locales. The live tail (items no request
carries yet) and turn-less introducing requests (older hosts) carry no
stamp rather than a mislabeled one.
2026-10-08 08:44:49 +08:00
bowenliang123 2d474b0def build: prepend a package/git header banner to built artifacts
Both tsdown configs (build and --watch) now stamp every built JS file with
a comment header carrying the package name, version, description, author,
license, homepage, build time, and the current git commit, branch, commit
date, and dirty state. The banner goes through tsdown's top-level option,
which rolldown applies post-minification so the comment keeps its
formatting, and the { js } scope keeps it off the published index.d.ts.
Git state is read per build, so watch rebuilds pick up new commits, and
the git lines degrade to omission outside a git checkout.
2026-10-08 08:12:36 +08:00
bowenliang123 b784fd8c32 build(test): enable tsc incremental for the typecheck
tsc --noEmit supports --incremental in TS >=5.6: it emits a .tsbuildinfo
(ignored via *.tsbuildinfo) and reuses it across runs, halving the
per-check cost (0.74s -> ~0.35s) on every pnpm test since vitest runs the
typecheck first. Still catches injected src errors (verified).

Vitest's experimental.fsModuleCache was measured and rejected: cold runs
pay a ~23MB write with no gain, warm runs sit inside the run-to-run noise
band, and caching transformed modules conflicts with the suite's stated
contract that specs import src/ directly so coverage reflects what ships.
2026-10-08 01:39:30 +08:00
bowenliang123 5e6a5b862c build(test): collapse the typecheck to a single program
The src-only tsc pass was redundant with the tests pass: tsconfig.tests.json
extends the base config, so its program loads every src file (96/96, both
passes identical) plus the specs, and tsc type-checks the whole program —
injected src errors surface from the tests pass alone. Verified zero src-only
diagnostics on the clean tree. One pass now covers src + tests; drop the
duplicated full-program compile.
2026-10-08 01:30:08 +08:00
bowenliang123 8364917ea7 build(test): run the two typecheck phases concurrently
The src and tests tsc --noEmit passes are independent (different program
scopes), so they fork instead of running back-to-back: ~0.9s -> ~0.65s.
The vitest body is CPU-bound jsdom startup (workers already saturated:
sequential client is 62s vs 14s in parallel), so no test-file changes are
warranted — the spec sleeps are deliberate (act-attributed state via real
timers, host pacing, real debounce), not redundancy.
2026-10-08 01:26:14 +08:00
bowenliang123 9f52f17a5c test: single-source the baseline entries and the node fixtures
- The two BASELINES entries differed in exactly three fields (id, tag,
  session) over ~95 verbatim lines; one baseline() factory now spells the
  shared V4 seams once, with the per-line comments kept above each call
  (a future line that diverges on a needle outgrows the factory and
  inlines its entry instead).
- The identical node fixture factory lived in five specs under two
  shapes; both moved to the shared test kit as surfaceNode (cards'
  tokens-5 shape) and surfaceNodeAt (assemble/brief's minimal shape).
2026-10-08 01:11:41 +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 6a8f7a2ae1 refactor(host): single-source the connection seam and the cost walk
- The ConnectionHostFace interface and its bind-at-extraction guard were
  copy-pasted across detail/backfill/balance; one host/connection.ts
  module now owns the face and fetchRouteRegistrar.
- accumulateCost (timeline fold) and pricedDayOf (activity ledger) were
  two hand-written copies of the same clone-along-path cost accumulation,
  period rule included; both now call the exported addBilledUsage.
- The cost zod schemas were defined field-for-field in activity.ts and
  timeline.ts; activity imports costUsageSchema from the wire owner.

Rejected from the audit after inspection: sharing firstText/firstFullText
(the walks differ in the acceptance predicate — trim-nonempty vs
nonempty — and unifying would change skill-name matching on a
whitespace-only first block of untrusted log data), and unifying the two
host asRecord guards (a one-line stdlib idiom; cross-module imports for
five lines is churn, not simplification).
2026-10-08 00:59:03 +08:00
bowenliang123 afa3376923 refactor(client): unthread the ctx the detail chain never reads
makeDetailFetcher's read switched to a plain fetch in a82e6a1, leaving a
lone void-statement as the ctx parameter's only use — yet ctx kept being
threaded through detailStoreOf, useTimelineSource, and makeAgentHeads.
All four drop the parameter; the spec's ctxWithCall factory (which only
ever stubbed global fetch and returned a ctx nobody read) collapses to
a plain stubDetailFetch.
2026-10-08 00:48:54 +08:00