Add @stylistic/object-property-newline to the src stylistic block, so an
object that already spans multiple lines cannot pack several properties
onto one line. Objects written entirely on one line stay compact, which
keeps small inline props objects untouched.
The autofix split 199 packed properties across i18n.ts, statsTiming.tsx,
fileActivity.ts, index.ts, fold.ts and imageTokens.ts. Note that this
rule's fixer only inserts the newlines, so @stylistic/indent repairs the
resulting indentation on a second lint:fix pass.
The assistant category's search box gains Thinking / Tools / Answer chips, each carrying the shown step's message count; clicking one keeps only the replies carrying that kind (intersected with the text query), clicking again clears, and the pick follows the text filter's lens rules (reset on a category switch, kept across step picks). Kinds classify from the conversation join's blocks (reasoning / tool-call / text, legacy content joins included) and fall back to the surface node's calls/text when the join misses.
A seeded fork (a dsh-better-sidebar Side tab, a subagent_fork child,
a UI fork) replays its parent's event log verbatim, usage settlements
included, so its cumulative cost fold re-priced history the parent
session had already booked - a family of idle Side tabs billed the
parent's whole history again (#94).
The fold now zeroes the session-cost totals at the tagged
`session/end-seed` marker the session core stamps at the inherited
cut, in both projection units: the timeline's cost book and the
activity day ledger (whose armed step slot dies with the cut, so a
child settlement cannot attribute to a parent day). Untagged resume
markers reset nothing; nested seeds re-mark at their own cut, so a
session's totals are always its own post-seed spend. stateVersion
bumps (timeline 22, activity 3) refold cached rows from the log on
upgrade.
When one models.dev provider priced the whole scope, the cost cell
renders as an anchor opening https://models.dev/providers/<pid>/ in a
new tab — the natural "check these rates" follow-up to the tooltip's
listing lines. A multi-provider scope stays unlinked (the tooltip names
each face instead), and the anchor reuses the framed cell chrome with
the link chrome reset.
The cost and subagent-cost tips render a per-model price table: each
billed model lists its non-zero bucket rates at two decimals, closed by
a line naming the registry face — the models.dev provider id and model
id — its rates resolved from, so a gateway route prices visibly against
its actual carrier (the confusion behind issue #93). priceFaceOf
carries that face out of the resolution: resolution candidates keep
their branch model id, the branch lookup returns its hit, and priceOf
delegates to the face. The peak|off pair display gives way to the
peak-window footnote, and the CNY conversion note rides the CNY display
only.
A gateway provider id outside the models.dev registry falls back to the
model-side resolution, where azure's self-named SDK package won
`deepseek-v4-pro` and its flat listing (no cache_read published) billed
cache hits as plain input at 4x the official input rate (issue #93). A
`deepseek-` model id now prices from DeepSeek's own registry branch
first — the same first-party specialness the peak/off-peak split
already grants DeepSeek — and falls back to the model-side tiers when
the first party does not list the id.
Fixes#93.
The KPI band, the aggregate stats row, and the 3:7 body now share one
scroll region (.lc-ov-scroll) under the fixed head; the columns drop
their own scrollers and keep natural heights (align-items: start), and
the folded width's whole-card scroll branch goes away with them.
compatibility.md documents the ledger's per-day pricing records (the
1 → 2 bump, the warm-up's cold refold on upgrade, the additive-optional
wire field's degradation); the README names the chart among the
dashboard's sections.
The dashboard's first row becomes a 1:1 column pair: the KPI band's six
figures (three per row) beside a new last-7-days usage chart whose card
stretches to the same height. Each day pairs a billed-tokens bar (the
heatmap's blue) with its priced fee (green), folded off the merged daily
ledger and priced by the same book and estimator as the KPI cost cell —
per-turn attribution, never the session's last-active day. The plot is
framed like a chart: hairline gridlines at quarters of each series' own
peak, a solid baseline, tick rails on both sides (tokens left, cost
right, dashes while nothing prices), and a day strip mirroring the
columns' flex geometry. The pair folds to one column at 932px of
container and the KPI grid to 2x3 at 640px; the bars' box keeps a
min-height floor so percentage heights survive the folded single-column
row. The cost legend qualifies its scope (price-mapped models only)
while the tooltip line stays bare.
Each metered settlement now books its billed buckets into its day under
the (provider, model, period) in force — the same SessionCostUsage raw
material the timeline's session-cost totals carry, attributed to the day
the open step INITIATED (step/start armed, step/end cleared, settlement
instant as fallback) so fees land on the turn's own calendar day. The
fold tracks the route in force (last request/header wins) and splits
DeepSeek peak/off-peak off the same instant, so its per-day records
price identically to the session-cost totals. stateVersion bumps 1 → 2:
the dashboard's warm-up cold-refolds every stale row from the durable
log on upgrade. The client's activityOf re-proves the additive-optional
record with the same sanitizer the session-cost rides — a malformed
record drops the fee whole and keeps the day's tokens.
Drop @deepseek-ai/dsh-invariants and @deepseek-ai/dsh-scope (unreferenced
since the two-baseline re-fit) and the duplicate zod entry from
devDependencies; the dependencies entry stays the runtime one. Declare
react-dom as an optional peer - the client bundle requires it and the
host's platform module table provides it, mirroring the react peer.
Refresh dev-only tooling in range: oxlint 1.85.0, oxlint-tsgolint,
simple-icons, @types/node, jsdom, eslint-plugin-sonarjs, playwright-core.
Everything coupled to the dsh baselines stays put on purpose: the
primitives resolution shim at the versions dsh-v0.1.5-rc.1 itself
resolves, dev cordis 4.0.2 against the vendored packages' exact peers,
lightningcss 1.32.0 deduped with @tailwindcss/node, and the zod floor at
^4.4.3 to match the harness's own. Drop the two orphaned
minimumReleaseAgeExclude rows along with the removed packages.
dsh provider route keys are arbitrary strings (subscription plugins
register 'claude'/'codex', users declare gateway profiles), while the
models.dev registry is keyed by vendor slugs — so every session billed
through such a route fell to the cross-provider scan, which priced only
when exactly one branch carried the model. The Cost tile dashed
hundreds of sessions whose model ids match models.dev exactly
(issue #91): claude-* resolves under 9-20 resellers, gpt-* under 20.
pricesBookOf now also extracts each provider's registry npm package and
builds a resolution index over the book (cost.ts priceIndexOf): every
model id lands under its lowercased key and its '-'-suffix tails, so a
provider the book does not know prices by model id alone. The vendor
branch is picked from registry data, never a per-model hardcode:
- 'vendor/model' dialects: the org segment names the vendor (exact, or
the registry id behind a variant org — deepseek-ai -> deepseek),
falling back to the bare last segment;
- the model's own vendor: a provider whose @ai-sdk package is named
after itself (@ai-sdk/anthropic -> anthropic) — mirrors and gateways
ride the generic openai-compatible package or someone else's;
- a provider the id itself names (deepseek-v4-flash -> deepseek),
matching '-' and '/' separators;
- a lone carrier, as before.
Each tier takes only a unique or unanimous rate (float-noise tolerant);
disagreeing candidates price nothing rather than guess. A provider the
book DOES know still never falls back — known branches stay
authoritative.
The rename table gains only 'deepseek-account' -> 'deepseek': the
native account route shares DeepSeek's period-based list, so the fold
now splits its peak/off-peak buckets too (stateVersion 21 refolds
cached rows that booked everything under 'peak').
Measured against the live models.dev dump over dsh's real vocabulary —
both native routes, every llm-pi-ai catalog route x its catalog models,
and issue #91's subscription/proxy ledger routes (1397 pairs, 49
routes): priced pairs 931 -> 1157 (+24%), routes with at least one
priced model 35 -> 47, with zero regressions and zero rate drift
against the old resolution; issue #91's routes go 0/35 -> 35/35.
The Cost KPI sub-line now reads 'N of M sessions priced' so unpriced
sessions are visible instead of reading as the full count (also #91).
Bumps @opencode-ai/models consumption onto the refreshed registry.
Fixes#91
The bar's dashed hover outline draws 1px+1px outside its box, and the
first bar sits at the scroller's content origin, so the frame's left
line fell outside the clip box at scroll 0 and vanished (the right
line of the newest bar clipped the same way when scrolled to the end).
Reserve 2px of horizontal padding in the chart for that ink and mirror
the same padding on the turn strip so its blocks stay aligned with the
bar columns.
The step-granularity chart had no position landmark along its column
grid (the turn strip numbers turns, not steps). Every 5th bar now
plants a flag at its right edge: a 1px pole rising from the baseline
into the top padding band, the bar's cumulative step number boxed at
the pole's top and streaming left over its own bar, so the banner
never contributes to scrollable overflow at any step count. Turn
granularity plants none (the turn strip already numbers that grid);
the flag renders under the scissors glyph on a same-bar collision.
The 0.1.7 line's SettingsForms service retired the `get(ns)` section
read, so the balance route never resolved the DeepSeek API key on V4+
deployments and the capsule stayed hidden. deepseekSectionOf now folds
over whichever face the running line serves: the registered
`llm-deepseek` section through V3, and on V4+ the Config-form
`describe()` projection, matching the provider's row by its own
apiKeyEnv-bearing section shape and preferring the settings ids the
generations have served it as. Rows fold in isolation, and a describe
that throws is no section — the capsule still arms nowhere it cannot.
The per-baseline `balance` probe pins the settings face and the
provider row's sources against each tag's real sources.
The panel's first row is now the range's six KPI figures in one
full-width line, lifted out of the insight column; the aggregate Token
and Timing Stats pair moves below it, and the 3:7 body keeps the
activity heatmap and the preferences entry beside the session column.
The band folds 6 -> 3x2 at 1000px of container and 2x3 at 640px, so no
cell ever ellipsizes its figure.
The panel's first row folds the range's sessions into the two donut cards
the per-session Context tab opens with. Token Stats splits the billed
volume by the SAME composition categories: each session's billedParts
estimate sums by category (output exact), and the center total stays the
merged billed figure the Tokens Used cell shows. Timing Stats reuses the
per-session card verbatim over a summed TimingTotals whose additive-
optional decode-split and throughput fields sum their carriers only, so
older cached rows keep the un-split shape and no-chip fallbacks. The pair
sits at half width over the 3:7 body and folds to one column at 700px of
panel width.
dsh's 0.1.6 selection refactor moved session selection to the view owner:
uiWorkspace.openSession is the verb the built-in sidebar's row click uses,
and the sessions service no longer carries open(). The plugin's jump (the
Context Dashboard's session cards and the Agent network card's nodes) still
called the retired sessions.open spelling, so on 0.1.7-rc.2 the panel closed
but no session opened — silently, by the verb's fail-soft design.
One shared openSessionVia (services.ts) now serves both call sites: it
re-proves both faces per call, prefers the view owner's verb (invoked bound
to the service instance), and keeps the retired sessions.open as the
degradation path. The per-baseline sessionNav seam pins both spellings
presence-as-declared in the real-code matrix, and the always-on client-face
probe drives the jump through each generation's exact composition (the V4
baseline serves no sessions-side selection at all — the exact shape issue
#90 died on).
The 0.1.7 sidebar wraps every tab body in a definite-height `.tabBody`
whose `overflow: hidden` clips the grown tab root out of reach, while the
dockkit pane body above it never sees the overflow — the Context pane in
the right Sidebar could not scroll at all. The sidebar-scoped root now
keeps its own scroller (the shared `height: 100%` + `overflow-y: auto`
chrome, inset aside): the height resolves against the 0.1.7 tab body or
the 0.1.5 pane body alike, and degrades to the pane scrolling wherever
neither height resolves — one scroller either way.
The plugin.settingsOpen hint - the Plugin Info card's row value and
tooltip, and now the insights panel's settings row hint too - read
"Open preferences" / 打开偏好设置; both locales now name what the jump
actually opens, this plugin's own settings page ("Open plugin
settings" / 打开插件设置), not the shell's preference center. The two
settingsCard comments quoting the jump's label follow.
The Context Insights panel's insight column now closes with a framed
settings row: the plugin gear plus the localized jump hint, styled as
the column's third card (the session-card hover). The click rides the
same guarded preferences jump as the Plugin Info card's row
(settingsJump.ts) and closes the overlay with it - the jump drives the
shell chrome behind the panel, so staying open would hide its landing.
The panel spec drives the jump end-to-end against a simulated
Plugins-page chrome, from the panel-entry click to the card-open
control.
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.
dsh 0.1.7-rc.2 dropped the offset parameter from
sessionProjectionCache.cachedSnapshot (the lifecycle identity now rides
the header alone). The warm-up's fixed 3-arg probe bound the branded
offset onto `keys`, where `new Set(0)` threw for every session with
served rows - so the whole corpus read as unserved and took the full
cold-read path on each process's first dashboard open (contained per
session, but minutes of repeated decode+fold on a large one).
The probe now tries the 0.1.5-rc.1 spelling (meta, offset, keys) first
and falls back to the 0.1.7-rc.2 spelling (meta, keys); a record-less
session returns undefined without throwing on either face, which stays
the correct "not served". Covered by a two-face fixture.
The placeholder promised less than the user wanted and the matcher
delivered less than the placeholder should: the search box only
substring-matched title and directory. It now also matches the
session's newest own message (timeline.lastUser, the card preview
text), case-insensitively, and a non-string fold shape safely never
matches. The placeholder copy follows in both locales ("Search title /
path / last message..." / "搜索标题 / 目录 / 最后消息...").
The overlay card sized to its content up to a max-height cap, so every
list/filter change wobbled the panel's height and its centered top and
bottom gaps. The card now takes a fixed height - the viewport minus a
40px band top and bottom (the width keeps its 16px side frame) -
resolved against the definite-height backdrop so no viewport units are
involved; the inner columns keep scrolling their own overflow and the
folded width keeps scrolling the card whole.
The card's depth always rode billed tokens; now a Sessions/Steps toggle
in the card's top-right corner (default steps) picks the metric, and the
day tooltip gains a steps line beside the sessions count. Sessions and
steps shape the window's days on different spans - one long session
drops a step a day, another day stacks a run - so the join tracks an
independent window maximum per metric and never scales one off the
other's peak. A billed day with no settled step (a legal wire shape)
stays gray under steps yet still tips and pins. The toggle rides the
shared lc-gran idiom inside a lc-heat-ctl wrapper - one auto margin for
the hint text plus the group - and specs cover both scales plus the
panel-level switch.
The month captions sat ~2px off the cell tops and the weekday gutter
only 4px from the grid - cramped at a glance. The month band grows to
18px (the label's line-height rides it, so the caption centers in the
band) and the gutter gap to 8px; the band is shared by every column and
the gutter, so the centered cell rows stay aligned.
The single-glyph 一/三/五 and M/W/F read as cryptic initials; the gutter
now carries the full names 星期一/星期三/星期五 and Mon/Wed/Fri. The
weekday gutter drops its 10px single-glyph width and self-sizes to its
widest label (the locales differ), with nowrap keeping a full name on
one line.
Two borrowed from GitHub's contribution graph. The grid gains the
Less→More legend under its right edge - the five swatches reuse the
cells' own depth classes, so the key can never drift from the scale it
explains (ov.heat.less/more, 少/多). The month gutter now also names the
window's opening column, so a month already running at the window's
edge is announced instead of left blank; month-start columns keep the
existing rule and text. The component grows a column wrapper to hold
grid and key, and the specs cover the legend's five steps and both
label paths (the wi>0 opens-on-the-1st case guards the short-circuit's
covered branch).
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.
Every runtime seam the plugin rides is source-identical from
0.1.7-alpha.2, so no code changes; the supported V4 baseline moves
from 0.1.7-alpha.2 to 0.1.7-rc.1 (the alphas leave the matrix). rc.1
adds the harness-side plugin peer-compatibility admission gate
(evaluatePluginCompatibility); the plugin's >=0.1.2-rc.1 dsh peers
satisfy it under the gate's includePrerelease check, so the bundle
composes with no exemption. The release workflow fetches the rc.1 tag
for the compat matrix and points the plugin checker at the npm next
dist-tag (0.1.7-rc.1).
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.
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.
The Plugins page's package cards draw each bundle's icon from the Host's
package metadata (manifest-relative SVG/PNG/JPEG/WebP, inlined as a data
URI), which we never declared - the cards fell back to the default
artwork. The Context emblem's single graphic source is now the
package-root icon.svg: the package.json "icon" field points the Host's
reader at it, and the client build inlines the same file through a
"*.svg?raw" channel (the bundled ContextIcon re-renders it at every
requested size), so the code carries no second copy of the paths. A spec
pins the file and the component in lockstep; the files list ships the
asset.
The Config-form jump stops settling for the card list: after the panel
entry's click, a bounded poll (40 ticks at 100ms, riding the same
scheduler contract the tests drive) follows the page's package load and
opens the bundle's page through the card's title control — the one
button inside the shipped data-plugin-package list item that is not the
enable switch, so the match survives every locale. A poll that finds
nothing (still loading, plugin absent, or the page already open on the
detail view, where the list is unmounted) silently degrades to the card
list.
The User preferences delta moves from deferred to shipped: one schemastery
entry schema serves both generations, the browser seat and transport per
line, the deliberate strict-key relaxation, and the settings.yaml legacy
import that carries persisted values into the profile entry. The README's
Settings section names where the card lives on each release.