1042 Commits
Author SHA1 Message Date
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
bowenliang123 4da78036fe refactor(client): share the timeline collections block and snapshot read
- timelineOf's slow path and detailOf carried a byte-identical
  requests/events/nodes/droppedNodes/archive/floors/fileOps/spans block;
  both now spread one collectionsOf(data) at the same boundary rigor.
- sessionsSnapshotOf/workspacesSnapshotOf were the same guarded-hook read
  differing only in the prop key; both delegate to standardSnapshotOf.

Rejected from the audit after inspection: merging billedTokensOf into
usageTotalsOf (their input contracts differ — the former's per-level
re-proving against hostile wire data is pinned by cost.spec and the
compatibility contract), and a shared own() disposer helper (the three
sites have two different shapes; the guard is a one-token idiom).
2026-10-08 00:41:34 +08:00
bowenliang123 95ddd40bf1 refactor(client): trim unread component surface
- BlocksBody's richable prop was declared and passed at all four call
  sites but never read.
- RawSection was a single-use Section wrapper; inlined at its one call.
- makeSliceList/makeDonut took a kit they immediately voided; the
  factories and all call sites (specs included) drop the parameter.
- seqOfMessageId/flowAnchor lost export keywords nothing imported.
- EVENT_ICONS shed the inject/model entries the icon short-circuit
  renders unreachable.
2026-10-08 00:34:27 +08:00
bowenliang123 bb8547907a chore(styles): drop dead and duplicate CSS
- a.lc-stat(:hover): orphaned since the anchor cost cell was rebuilt as
  the Agent Team ledger (3dc892c); every lc-stat is now a div.
- .lc-rich-seg*: byte-identical re-declaration of the shared .lc-gran*
  segmented control; RichSwitch now wears the lc-gran tokens, and the
  browser spec scopes its queries to the detail body accordingly.
- lc-col-stats: a class token no rule, query, or script ever read.
2026-10-08 00:29:01 +08:00
bowenliang123 525b9af5e7 refactor(client): ride the platform ImageLightbox for attachment previews
The ported AttachmentLightbox predates the discovery that the shell seeds
ALL of ui-primitives into the browser module table: ImageLightbox is
exported on both supported lines (0.1.7-rc.2, 0.2.0-rc.2) with the exact
props the call site already builds. Deleting the port also drops the
lc-att-lightbox sheet; the spec keeps only the plugin-owned open/close
wiring (dialog behavior is covered upstream), and upstream adds Escape
stopPropagation plus a Tab trap.
2026-10-08 00:24:31 +08:00
bowenliang123 3f7be66f2d chore(repo): drop unreferenced img.png files and ignore .DS_Store
Neither img.png (root nor docs/) is referenced by the README's raw URLs
or anywhere else; the Finder metadata files were untracked junk.
2026-10-08 00:19:15 +08:00
bowenliang123 c93a2498b1 chore(deps): drop unused playwright-core
The other suspects from the audit (shiki, katex, anser, clsx, the
mdast/micromark set) stay: the published ui-primitives lib is ESM whose
bare imports are undeclared, and vitest resolves that file for real, so
they are load-bearing devDependencies of this workspace.
2026-10-08 00:18:09 +08:00
bowenliang123 fc981a7a48 chore(styles): drop the orphaned .lc-stat-head rule
Zero consumers since the stats-board split; found in the compatibility
change's leftover sweep.
2026-10-07 23:38:22 +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 65d1c77ac9 chore: release v0.65.0 2026-10-07 17:38:18 +08:00
bowenliang123 a28116e5d0 fix(client): dock the /context modal between both shell sidebars
The mask was inset on the left sidebar alone, so with the right
Sidebar open the dialog centered over the main column plus the right
panel and its right half disappeared underneath. measureDock now
parses the frame template's trailing px track too, clamped to the
space the centre track's declared minimum leaves: on the minmax
generations the trailing bound is only the right panel's bid, not the
solved width — the frame lets the grid squeeze the right track before
the centre drops below its minimum (measured live: template
minmax(0px, 495px), solved track 420px at a 1100px viewport), so an
unclamped parse over-insets the mask in exactly that squeeze regime.
The V3 line's fixed trailing track and every degrade-to-full-mask
branch are unchanged.

The card width now resolves against the backdrop (100% instead of
100vw), so when both sidebars bound the mask the card clamps inside
the main column instead of poking underneath either edge. Verified
live at 1280/1100/900/390 px with the right panel open, closed, and
across its track removal at phone width.
2026-10-07 17:30:50 +08:00
bowenliang123 fb0592f7cf fix(client): reveal through the ancestor that actually scrolls
Overflowing style alone is not enough: on layouts where an outer
container scrolls (the web app's conversation scroller), the Context
tab's own .lc-root reports overflow-y auto yet grows with its content
and never moves — taking it short-circuited the walk into a silent
no-op, so neither the team card's jump to the Agent network card nor
the chat→context jump's composition reveal ever scrolled. The walk now
requires scrollHeight > clientHeight, so the first ancestor that can
actually move takes the rect delta (verified live: click lands the
foot card at the scrollport top).
2026-10-07 16:41:54 +08:00
bowenliang123 39dc891932 feat(context): name the team card after its foot namesake and jump to it
The Agent Team ledger becomes the Agent Network card — the same name as
the big card at the tab's foot, sharing the agents.title key so the two
can never drift. Clicking the card anywhere outside the price link (or
pressing Enter/Space on it) scrolls the foot card into view through the
shared revealInScrollParent helper; the price link and the '?' hover tips
keep their own behavior, and the jump is a quiet no-op when the harness
hides the foot card (no sessions service).
2026-10-07 16:34:34 +08:00
bowenliang123 dcd7fff4da fix(host): gate every cold read through one shared FIFO with a heap high-water mark (#121) 2026-10-07 15:42:21 +08:00
bowenliang123 a4bbcf4f2b feat(insights): rename the card stats to Token 总用量 / 总预估费用 2026-10-07 13:20:13 +08:00
bowenliang123 ef8e1d24dd feat(insights): read every card and aggregate at the team scope
The dashboard's row IS the team now: each listed session folds its WHOLE
subagent subtree into itself (all levels, mirroring the Context tab's cost
fold) — subagent-origin sessions never list, so nothing counts twice. The
session card's billed tokens and priced cost (now labeled 团队用量 /
团队费用), the KPI band's volume and spend, the last-7-days usage chart,
and the heatmap all read the family scope: the subtree's cost buckets
merge per row, the daily ledgers merge day by day, and the composition
split proportions each member's OWN composition instead of the root's.
2026-10-07 13:12:52 +08:00
bowenliang123 3dc892c221 feat(context): rebuild the cost card as the Agent Team ledger
The flow's fourth node becomes a three-row team ledger: the family's
billed-token and estimated-cost totals in the header, then one row per
scope — the current agent and the subagent subtree with its session count
— each pairing its billed tokens with the estimated cost (billedTokensOf:
the token face of the same buckets the estimate prices). The subagent
scope is the WHOLE descendant tree, which the copy now spells out; the
rows render unconditionally (a scope with nothing billed reads 0 / —),
and both tooltips gloss the token figure.
2026-10-07 11:53:43 +08:00
bowenliang123 c4ad2e8a9c feat(context): rebuild the Context Stats card as a session flow diagram
The six-cell grid becomes a left-to-right flow: two source cards (Input /
Output — human inputs plus the whole session's file reads/writes, with
searches and image reads when they happen; Context Events broken down by
kind) feed the highlighted Current Session node (turns / steps / subagents
— the subtree's session count, folded from the session list alongside the
cost usage), which drains into the two effect cards (Tool Calls with the
three most-called tools and an overflow pill; Cost with the family figure
and, when a subagent subtree reports, the this-agent/subagents split).
Measured bezier ribbons — a translucent band plus a traveling dash per
element, not a conservative Sankey — connect the nodes and flip to a
top-to-bottom stack when the card narrows past the container-query fold.
2026-10-07 04:23:36 +08:00
bowenliang123 168051de00 docs: enhance AGENTS.md with detailed coverage report example 2026-10-07 04:23:36 +08:00
bowenliang123 3aed1bd6a2 feat(browser): find-in-text bar for the rich text detail cards
A ctrl+f-style lens scoped to one section, toggled from a magnifier
button beside the copy control: a debounced query (150ms, so rapid
typing or deleting rescans once instead of per keystroke), matches
wrapped as DOM-level <mark> over the rendered body (same lens in raw
and markdown modes, capped at 500 rendered marks with a '+' counter),
Enter/Shift+Enter or the chevrons cycling the active match into view,
Escape folding the bar. The section body remounts on a text change so
React never reconciles over the injected marks; the restore rebuilds
the original text nodes, keeping the copyable source byte-identical.
2026-10-07 02:52:52 +08:00
bowenliang123 199bdbc499 feat(insights): drop the 960px content cap, span the pane's full width 2026-10-07 01:17:55 +08:00
bowenliang123 fbb00303d9 feat(context): read the occupancy percent at one decimal, off the kernel's own value
The card's reading was the ring's integer (`Math.round`), so a 101.6k /
1.0M session printed 10% while the kernel's own occupancy read 10.2%.

The percent now comes from one shared `occupancyPercent` helper that
mirrors dsh's `contextOccupancy` — the composer ring's derivation
(`min(100, round(used / window * 100))`, `used` = `contextPressure`'s
`projectedTokens ?? pressureTokens`) — on a 0.1 grid, and keeps the
kernel's numerator precedence ahead of the plugin's derived anchor. The
kernel symbol is unreachable from a client plugin (no package export, a
self-registering browser bundle), so the formula is mirrored by
necessity, as docs/compatibility.md now records.

The Agent Network rings read the same helper (whole tenths stay compact
there), and the number row wraps so the wider reading folds to its own
line instead of spilling on the narrowest panes.
2026-10-07 01:00:02 +08:00
bowenliang123 eb22c55208 docs: streamline AGENTS.md, merge one-liner sections and cut redundancy 2026-10-06 22:22:37 +08:00
bowenliang123 6b455791b1 docs: update AGENTS.md with improved coding guidelines and pre-commit checks 2026-10-06 21:56:02 +08:00
bowenliang123 984f31c954 refactor(stats): drop the cache-hit cell from the Context Stats card
The token card's corner already carries the session's cache-hit share
(ef72a7b), so the Context Stats board duplicated it. The grid is now six
cells: turns, steps, human inputs, live tool calls, and the two cost cells.

The removal takes its dead plumbing with it — the card drops its `usage`
prop, and `cacheHitPercent` loses the `decimals` override that existed only
for this cell's one-decimal display. The README rows and the card's
screenshot move the figure to the token card.
2026-10-06 21:36:06 +08:00
bowenliang123 ef72a7bbb2 feat(stats): session cache-hit on the token card, plain rate figure on the timing card
The token card's title row gains the session's cache-hit share in its
right corner — the harness chat line's own reads-over-billed formula,
reusing the stats board's label and tooltip — hidden until something
was billed. The timing card's throughput chip drops its pill chrome and
bold weight, now reading 输出速度 N tok/s (zh) / TPS N tok/s (en) with
the TPS expansion on the tooltip; both corners share a new plain-text
.lc-card-rate style and keep their native-title tooltips.
2026-10-06 21:21:57 +08:00
bowenliang123 33f61453d7 fix(insights): align the usage chart's day labels and uncramp the title
The day strip spanned the card's full width while the columns sit inset
between the two 48px tick rails, so every label fanned out from its bar
group; the topmost tick also rode the title's 4px margin. The plot is now
a 3x2 grid — rail | chart | rail, with the strip as row 2 of the chart's
own column, so labels center under their columns by construction — and
the title falls back to the shared 10px margin. Under a 400px container
the seven MM-DD labels thin to every other one instead of crushing.
2026-10-06 17:49:29 +08:00
bowenliang123 a4c2b67e72 ci: auto-close issues whose title matches a blocked pattern
Fires on issue creation; currently the only blocked pattern is
'dsh-plugin.org' (case-insensitive). Matching issues are closed as
not planned with an explanatory comment. Add further conditions by
appending to the patterns array.
2026-10-06 09:13:11 +08:00
bowenliang123 bcb73fca8c chore: release v0.64.0 2026-10-05 15:24:08 +08:00
bowenliang123 8a861c0cfd fix(perf): stop the context page hover from stalling the main thread (#116) 2026-10-05 14:09:33 +08:00
bowenliang123 e5925ee49f feat(stats): flow the slice legend into as many columns as the card offers
The donut cards' slice legend auto-fills at a 150px floor (from 160px)
with an 8px column gap (from 20px) and a 30px share column (from 34px),
so two columns start at a 308px legend — the Insights page's ~440px
stats cards ride this — and wider cards take more; the floor is
`min(150px, 100%)` so a narrower legend shrinks the track instead of
clipping the share column (the 390px fold previously cut 11px off the
percentages). Narrow panes keep their single column; the Context tab
keeps the same column count at every width it already wrapped at.
2026-10-05 11:21:24 +08:00
bowenliang123 96c8b92caa feat(insights): promote the dashboard to a first-level sidebar panel page
The Context Insights surface moves from a sidebar-foot button opening a
frame-wide overlay to a first-level panel, a sibling of the shipped
Plugins and Automation tasks pages: the page registers on the layout's
keyed `main` slot and the entry on `sidebar.panellist` under one id
(`dsh-context`), so the shell owns the entry row and panel switching —
mounting the page IS opening it (no plugin-side open state, the footer
entry, the overlay seat, and the overviewStore all go away). The
`insightsEntry` preference now mounts/unwinds the registration pair.
Both seats ship on every supported line (0.1.5-rc.1+), pinned per
baseline by the new insightPage seam probe; runtime-verified on a
disposable 0.1.5-rc.1 profile.

The chrome mirrors the shipped first-level pages: one scroll region
with the 960px centered column, the 20px page heading beside the range
group and the balance capsule, and the page element as the `lc-ov`
query container. Inline-size containment makes the page the containing
block for fixed descendants, so the heatmap/usage/balance tooltips can
no longer ride the harness Tooltip (its portal prop is 0.1.7+): they
now carry `data-lc-tip` served by the page's own hover tip — one fixed
bubble portaled to <body>, delegated off the page root (the image
lightbox's precedent), correct on every supported line.

Session card jumps need no close (openSession's own navigation returns
to the conversation); the settings row jump unmounts the page by
selecting the Plugins panel. Verified across 1440-390px in both
locales, and the /context modal is untouched.
2026-10-05 11:20:59 +08:00
bowenliang123 84fe1d7038 fix(trend): merge the title's three modifier toggles into one segmented group
The Duration toggle (added in 01b1532) never joined the title-adjacent
zero-margin rule, so its bare .lc-gran kept margin-left: auto and split
the free space with the control cluster — on wide cards it drifted to
the middle of the title row (242px each side at ~930px), and on wrapping
widths it landed wherever the auto margin happened to put it (line 1's
right end in English, line 2's lead at the narrowest).

DNA / Adaptive / Duration now ride the title text's right as ONE
multi-toggle segmented group (.lc-trend-mods), the tips moving from the
group wrappers onto the buttons; the row keeps a single auto-margin push
(the control cluster), so wrapping drops the two semantic groups whole
instead of scattering the modifiers. Also drop the stale "Total/Delta
disables under DNA" note on the disabled-chip rule — no gran button has
been disabled since DNA+Delta shipped.
2026-10-05 07:14:16 +08:00
bowenliang123 2adc9bb355 feat(trend): drop the focused-category subtitle from the trend card 2026-10-04 17:09:29 +08:00
bowenliang123 501b2f8329 chore(release): v0.63.0 2026-10-04 11:24:35 +08:00
bowenliang123 4dcdfafbef feat(settings): trend duration curve visibility preference (default on)
The Context preferences card gains its eighth row — the trend
duration curve — served across both settings generations: the V3
`settings.register` schema and the V4 Config form (a `.volatile()`
field, so an edit never remounts the entry). The client settings store
parses it fail-open like its neighbours; the trend card's Duration
toggle seeds its mount state from it (default on), and in-chart
toggling stays mount-local and never writes back.
2026-10-04 11:12:50 +08:00
bowenliang123 01b1532b72 feat(trend): duration overlay — each step's active time as a curve over the bars
The trend card gains a title-adjacent Duration toggle: every request's
step active time (`activeMs`) drawn as a curve over the bars, read off
a right-hand quartile axis; turn granularity sums the turn's steps, and
a bar without the figure breaks the line instead of faking a point.

The fold stamps it at `step/end` onto the step's committed request
record: the whole step window (the model call plus its tool runs, per
the agent-loop's in-step `executeToolCalls`) minus the step's user
waits — approval decision intervals (`approval/asked` →
`approval/decided`, a crash-dangling pair closing at the step's own
end) and `ask_user_question` answer windows (the tool's body is the
wait itself, in both the blocking and the timed schema's foreground
phase). Overlapping waits price their clamped union, never twice.

The wire field is additive-optional and bumps the timeline projection's
stateVersion (25 → 26): requests already folded can never gain the
figure later, so cached rows refold from the durable log on upgrade.
2026-10-04 11:12:50 +08:00
quifox 06ec2af4ff fix(client): ignore stale detail reads after refold (#106) 2026-10-04 00:26:35 +08:00
quifox b65939e887 fix(host): fold V4 tool-registry developer messages into the timeline
V4 harness releases log a `developer/message` event whenever the effective tool registry changes mid-session (first shipped in 0.1.7-rc.2): the message nests under data.message with role `developer`, source kind `tool-registry`, and `tool-addition`/`tool-removal` content blocks, lands on the session surface, and the harness's own token meter prices it like any non-system message. The timeline fold had no case for it, so every such message vanished from the plugin's accounting — injection totals, per-request composition, the live surface, and compaction archives under-reported, and the Context tab's numbers could not reconcile with the harness meter or a compaction shadow price.

The fold now treats the event like an injected user message: the nested message is read structurally (the pinned 0.1.5-rc.1 dependency's deriveEventMessage predates the family), the node prices through the shared estimator with the harness's empty-content-is-zero rule, and the injection record takes the producer kind as its label. Developer messages never count as human inputs whatever a hostile source claims; malformed payloads leave the state reference-equal.

stateVersion 24 -> 25: earlier cached rows omitted these nodes and later events cannot recover them, so on upgrade every timeline row refolds once from the durable log (the dashboard warm-up rebuilds idle sessions). The wire shape and durable logs are unchanged. The V4 baselines now carry the event family, and the real-source compat matrix asserts surface tokens, request composition, warm restore, and a v24-row rebuild on both supported V4 lines.

Validation: 102 test files / 1,910 tests with per-file coverage 100%; the real-source matrix passed for 0.1.5-rc.1, 0.1.7-rc.2, and 0.2.0-rc.2; GitHub PR checks green (486069f fixed the workflow's pnpm setup that had failed every external PR at its first step).
2026-10-04 00:03:27 +08:00