The slash menu renders one source-title row per trigger source, titled
from the harness's own slash.menu dictionary; the plugin's source name
`context` is not a key there, so the row showed the bare raw name and
read as a duplicate command (closes#67). Candidates now carry a
localized `section`, which the menu renders in place of the
source-title row — 上下文 in Chinese, Context in English.
The hover-link's re-light rules are a hand-enumerated CSS list keyed by
category, and the skill bucket joined the vocabulary without one — hovering
the browser's skill section (or the overview's) dimmed every bar with
nothing lit. Add the missing rule and pin the list-must-track-Category
constraint next to it.
The fold buckets all three skill-content shapes the harness injects — the
<available_skills> catalog digest, a user-explicit /name invocation's
instructions message, and the content a skill-tool load returns — into a
new 'skill' composition category instead of burying them in inject/tool
(closes#66). Skill-tool loads keep their tool identity stamped, so the
call tallies, hit counts, and failure marking are unchanged.
stateVersion 18 → 19: the per-category sums change shape, so cached rows
refold from the durable log on upgrade (docs/compatibility.md).
The trend chart, overview, Token donut, stats, and the Context browser all
gain the seventh bucket (orange-500); the browser's skill section pins its
rows like injects, so long sessions keep listing what the sums count.
detail.step/tip.step now read "第 s 步 (共 n 步)" / "Turn t · Step s of n" —
the same per-turn tally a turn-mode bar's stepCount carries, so step and
turn granularities agree on how busy a turn was. A shared turnStepsOf()
getter feeds the trend tooltip, the detail head, the files scope label,
and the browser picker/meta rows.
The Context stats grid's cache-hit cell now carries the chat stats line's
figure two decimals deep and explains itself through the shared stat-tip
bubble (the whole-session cache-read share of billed input), localized in
both locales.
The Tailwind responsive fold (ff10cff) retired the `.lc-col` flex rule and
moved every flex-row child onto per-element `flex-1 min-w-[min(360px,100%)]`
utilities — but the file-activity card's own root was missed. As an lc-card
size container its intrinsic inline-size is zero, so under `flex: 0 1 auto`
the whole card collapsed to its chrome (~18px) in EVERY host at EVERY width,
crushing its content into one-character columns (issue #65). The root now
rides the shared column idiom like its sibling cards.
Two more narrow-pane defects fell out of the same audit:
- A content-box card read the `min-w-[min(360px,100%)]` floor as its content
width, so on sub-360px panes every floored card overflowed its row by its
own padding+border (18px), clipping past the pane edge. The plugin ships no
preflight, so `.lc-root .lc-card` now sets border-box itself.
- The events card's kind chips had no wrap fold of their own, so the English
chip row spilled past the card edge under 380px; it now rides the same
`@max-[380px]/lc-card:flex-wrap` variant as the file card's chips.
Verified in the browser across the right-Sidebar pane at its drag minimum
(~300px), 510px, 822px (the two-across threshold) and 856px, the full-width
tab at 390px and 1440px, and the /context modal, in both locales: the file
card shares its row again, no vertical crush, no right-edge clipping, no
horizontal overflow.
Fixes#65
New 'Tool Schemas - Default sort' row in Settings -> Plugins -> dsh-context
(ahead of the File Activity one): size, call count, or name, persisted per
user through the dsh-context settings namespace. The Context browser reads
it as the mount-time default for its tools toolbar; in-toolbar toggling
stays session-local and never writes back. The default default is now call
count. Degradation mirrors the existing preferences: the host schema is
loose (a stale persisted value falls back to the default), the client drops
values outside the vocabulary, and a host without the settings surface (or
a section missing the field) keeps the schema default, so the tools list
stays fully functional either way.
The Context browser's tool-definition rows now carry a per-tool call-hit
tally (xN) over the shown step's assembled surface — one paired tool-result
node is one completed call, the same accounting as the stats' toolCalls, so
the tally follows the step picker (a past step re-tallies over its own
surface). The tools toolbar gains a count sort ranked by that tally; ties
break by tool name. A zero tally names a definition the shown step never
called.
The five animations become --animate-* theme tokens with their keyframes
(emitted only when used): the donut sweep, stacked-bar and trend-bar
entrances, the agent link's flow dash and the running node's glow. The
entrance delays ride the element-scoped --lc-i stagger slot inside the
token shorthand and resolve per element, the running glow becomes a
conditional animate-lc-agent-glow class on the halo, and the sheets'
prefers-reduced-motion blocks become motion-reduce:animate-none on the
animated elements. The sheets keep what utilities cannot own: the
transform origins, the flow dash's linecap/dasharray, the halo's state
fills, and the seg arc's dasharray transition.
Element transitions deliberately stay in the sheets: all of them follow
the harness motion tokens (--ds-transition-duration/--ds-ease-in-out with
host fallbacks) over exact per-element property lists, where preset
durations/easings would freeze host-following values and Tailwind's
property bundles (transition-colors etc.) would transition more than the
design intends. transition-behavior and transition-delay have no use in
the plugin.
Verified in the browser via computed styles: the donut sweep resolves to
lc-donut-in 0.45s cubic-bezier(0.4,0,0.2,1) with a per-element delay and
backwards fill, and emulated prefers-reduced-motion yields animation:
none.
The donut rings' fill-none/stroke-4, the agent ring's track fill/stroke
and 9px arcs, the resting transparent halo fill, the link lines' width
and opacity, the node caption's fill token, and the jump glyph's
fill-current become fill-*/stroke-* utilities on the elements, and their
sheet rules shrink to what the utilities cannot own: the state-class
fills and the free remainder's color-mix (state-driven, kept per the
established boundary), the flow dash's linecap/dasharray/animation, and
the live link's opacity override — which still wins its tie with the
base utility because sheets inject after the utilities channel.
Data-driven strokes keep flowing as before: donut slices and
family-colored links carry var() values through inline styles and the
stroke attribute (var() cannot live in a presentation attribute), and
the icon logo keeps its fixed brand fills.
Verified: full chain green, all emitted utilities present in the client
bundle, and the rendered donuts, agent ring, links and jump glyph
unchanged in the browser.
All seven hand-written @container blocks move out of the sheets: the
head-band stack, the stats grid's wide/narrow columns, the donut row's
gap fold and stack, and the events/file rows' narrow-card wraps now ride
@min/@max-[…]/lc-card utilities on the elements (named lc-card /
lc-head-row containers). Reviewing that migration against the shared
column idiom folded the head band's 720px container query away entirely —
the rows were already flex-wrap rows, so the head cards take the same
`flex-1 min-w-[min(360px,100%)]` floor as every other column and wrap
intrinsically. The count grid became repeat(auto-fit, minmax(108px,1fr))
— 108px is the floor where the longest English label fits — replacing
the 700px/320px breakpoints and filling the card at every width instead
of jumping 3 to 6 columns.
What stays hand-written: the 240px donut stack and 380px row wraps (the
ring/legend and the 92px/46px reservations are measured-content facts,
and Tailwind's preset container scale is rem-based where this plugin is
px-anchored), plus the lc-card container definition whose .lc-root scope
deliberately keeps the /context modal's cards container-less.
Verified in the browser across 1280/900/736/640/390 in both locales and
in the /context modal: same folds at the same widths, no overflow, no
horizontal scrollbars. The visible delta is the count grid filling
intermediate widths with 4/5 columns.
The 20 hover/focus rules that are leaf styles (no state class re-asserts
them) become hover:/focus:/group-hover: variants on the elements' own
class lists, and their sheet rules are deleted: row and button hover
tints, retry/close/gate brightness, input focus borders, underline
hints, the three styled-tip reveals (named group/tip), the plugin-info
row underlines (group/pi), and the agent halo's hover/focus wash
(group/agent — the React hover state keeps driving the inspector;
lc-agent-hover stays as a test anchor).
The hover/lit(-on) cascade pyramids stay hand-written in the sheets —
the gran/rich-seg/rich-copy/fa-chip toggles and the settings card's
open state need ordered overrides over their own hover rules, several
of those state classes double as test anchors and DOM query hooks, and
moving them would mean a chip factory for zero visual change.
Transitions, animations and all harness --dsw-* tokens stay in sheets.
Behavior notes: variant utilities carry (0,2,0) specificity, so they
keep beating the sheet base rules exactly as the old :hover rules did;
Tailwind media-guards hover behind (hover: hover), so touch devices no
longer get sticky hover styling (dsh web is desktop-first).
Replace the hand-picked hex literals with the nearest standard Tailwind
tokens now emitted by the CSS channel: the category/timing/ring accents
(indigo, amber, green, purple, blue, teal, pink, violet, red, slate-400)
and the neutral chart tracks and zebra fills, recast as color-mix over
--color-neutral-500 at the original alphas.
Donut and agent-graph ring segments now carry their stroke as an inline
style instead of the presentation attribute: the colors are CSS variables
and var() is unusable in an attribute. Free segments keep their class
rule by omitting the inline stroke. Visual output is unchanged (the v4
values are the nearest equivalents of the former v3 hexes); harness
--dsw-* semantic tokens, the logo fills and the language identity colors
stay as-is.
Declare the 11 Tailwind palette tokens the plugin's sheets and TS data
layer consume in the Tailwind entry's @theme static block, values lifted
verbatim from the pinned tailwindcss theme.css. static is load-bearing:
the consumers reference them via plain var()/inline styles, never a
compiled utility, so used-only emission would never see them.
The global-CSS channel now runs @tailwindcss/node's compiler over a new
entry sheet (src/client/styles/tailwind.css) and feeds the resolved
@source globs through the oxide scanner; scanned sources join the watch
graph so class-list edits rebuild the sheet. The entry carries no
preflight and no theme import — just the spacing scale anchored to 4px —
and emits utilities unlayered, so the injected <style> cascade matches
the hand-written sheets it precedes. lightningcss and the Tailwind
packages are pinned to one exact version each: the config process loads
the oxide and lightningcss native bindings together, and duplicate
copies break the napi calls.
The Context Stats card row now names the models.dev source (DeepSeek
peak/off-peak aware), and the compatibility page's upgrade section
drops the stale 'stateVersion stays unchanged' claim: the cost rework
bumps it 16 -> 18, so cached rows refold and idle sessions re-fold on
their next live event.
Replace the hardcoded DeepSeek rate table with a client-fetched price
book: @opencode-ai/models reads the models.dev registry once per page
load, every entry is re-proved at the boundary, and a failed fetch
degrades to a dash + note with backed-off automatic retries.
- cost totals rekey per (provider, model); only DeepSeek splits a
pricing period (peak windows at list price, all other hours half
price) — every other provider bills everything at list price
- provider ids resolve through a shared rename table (deepseek-official,
kimi-coding, minimax-cn, zai-coding-cn) with verbatim passthrough for
unmapped ids; model ids match exactly, case-insensitively, or by
suffix (k3 -> kimi-k3), ambiguous matches price nothing
- CNY display converts at the fixed 1 CNY = 0.15 USD; the tooltip lists
per-1M rates for the models the session billed, with peak|off-peak
pairs and the DeepSeek note shown on DeepSeek sessions only
- stateVersion 18: cached rows refold from the log, which rebuilds the
new cost keys and periods
oxlint-tsgolint stays an explicit exact devDependency: oxlint declares
it only as an optional peer dependency, so no package manager installs
it for us and type-aware linting fails loudly without it.
No config changes needed: the config only uses deps.neverBundle /
deps.alwaysBundle and dts, all carried over. The rolldown-plugin-dts
0.28.5 bump emits direct export declarations instead of declare +
re-export in lib/index.d.ts; the exported surface is unchanged and the
built JS is byte-identical.
The Context Stats card's inject/compaction/prune cells duplicated what the
events card already tracks. Drop the three cells (the grid falls to five:
turns, steps, tool calls, cache hit, cost) and show each tally inside the
matching kind-filter button instead, sourced from the same precomputed
counts the stats card used.
The rates themselves check out against the official pricing pages (both
currencies, both families, and the Beijing peak windows), but the flash
row's label named deepseek-v4.1-flash — the model VERSION, not an API ID.
The page's current flash ID is deepseek-flash, with deepseek-v4-flash as
the accepted legacy ID, so the label and the tip's applies-to list now
cite those; the tip also drops the colloquial hardcoded phrasing for a
plain for-reference-only note.
The images cell said little about the session; the eighth cell now shows
the whole-session prompt-side cache-hit rate off the official tokenUsage
projection — the same source and formula as the chat stats line under the
composer, truncated to one decimal (cacheHitPercent gains a precision
parameter; the two-decimal default keeps the request detail's figure).
The head band's four cards quartered one row; now the session-shape card
sits beside the plugin card, and the two donut cards share the row below
(token / timing). Each row halves its width and folds to stacked when a
half would drop under ~350px.
The wider cards unlock roomier insides: the context card's eight cells
can lay out in ONE line on roomy panes (no longer pinned to 2x4), and
the donut legends flow into as many 160px-min columns as the card offers
(the composition legend's auto-fill idiom) — one in a narrow pane, two
on a classic 1280px viewport, more when the rows stack full-width.
The donut center now carries the whole-session billed total — the same
figure the harness chat stats line shows under the composer (uncached
input + cache read + cache write + output off the official tokenUsage
projection) — instead of the cache-hit share.
The slices answer WHAT the tokens are rather than how the provider
cached them: the six composition categories share the provider-reported
prompt-side total by the composition card's own estimated ratios (their
counts carry the ≈ marker), and the provider-exact output closes the
ring, so the parts equal the chat line's figure by construction.
The cache-read/cache-write/uncached bucket view and its four locale
keys are gone; tokens.total is new.
A turn aggregate IS its last step's record (the same seq), so with a
plain seq key a step ↔ turn switch reused those bars' DOM nodes — the
entrance animation had already finished on them and CSS never replays an
animation on a reused node, leaving every turn's last step bar popping
in unanimated. Keying ChartBar by seq + granularity forces a remount;
mode (Total/Delta) switches already remount through the stack ↔ arms
structural change.
The donut slices sweep in clockwise from 12 o'clock, the composition
stacked bar grows open segment by segment, and the trend chart bars rise
from their baseline — all staggered in layout order, transform-only (no
layout thrash), running once per mount and disabled under
prefers-reduced-motion (the harness's own idiom).
- the stagger caps (trend bars 20, stacked pieces 8) so long logs and
long DNA band lists still settle in well under a second
- the auto-compaction reserve band stays unanimated: as an overlay its
growth would smear the stripes, and it reads as chrome, not data
- covers the tab, the /context modal and the right Sidebar, which all
render the same chart components
V4 Pro now carries its own list prices, so the "until V4.1 Pro ships, Pro
requests are routed to and billed at V4.1 Flash rates" note in the cost
tooltip is no longer true; lead the covered model list with the current
deepseek-flash spelling.
DeepSeek has ended the routing window: deepseek-v4-pro (DeepSeek-V4-Pro-0813)
now bills at its own published table instead of riding the V4.1 Flash triple —
USD peak hit $0.044 / miss $1.32 / out $3.96, CNY peak ¥0.3 / ¥9 / ¥27,
off-peak half price. Flash rates are unchanged.
Clicking a file name in the File Activity card now opens the file's preview
tab in the right Sidebar — the shipped document-preview viewer, reached the
same way the built-in Files sidebar reaches it — on harnesses that serve that
column (dsh 0.1.5-rc.1+). The address is built with the harness's own
browser-safe `fileAddressFor`, inlined by the client bundle exactly as the
Sidebar's file types inline it, so a path under the workspace collapses to the
session-relative spelling and every route to one file settles on one tab.
The capability is OPTIONAL by contract, like the tab registration itself: the
opener reads `ctx.sidebarRight.openResource` through the runtime boundary and
re-proves it per call, and reports whether the column took the address. An
absent column, a face that refuses the address, or an unencodable log path
falls through to the pre-existing system-open behavior, so an older line is
fully functional and nothing becomes an inert click. Pattern rows and
directory targets are never previewable.
Adds the `@deepseek-ai/dsh-util-workspace-path` dev dependency (itself
browser-safe), allows it in the client bundle purity gate, pins the
resource-navigation seam in the compat matrix, and covers the address builder,
the card's preview-then-fallback click, and the optional opener per baseline.
AGENTS.md carried the full compatibility matrix — per-generation event
vocabulary, the moving V3 client seams, and the runtime gate's internals —
duplicating what docs/compatibility.md exists to document. Restate only the
requirements there and point at that page, keeping the agent-facing anchors
that have no user-facing home: the single shape-driven fold path
(`logShapes.ts`'s `firstTokenTimeOfStream` / `replaceRangeOf` / `isTokenChunk`,
`applyTimeline`), the upstream implementation mirrors (`dsh-token-meter`'s
`estimateSystemMessage`, `dsh-llm`'s `assistantStreamFirstTokenTime`), the
gated-inject rule for newer-only seams, and the baseline verification
mechanism.
docs/compatibility.md gains the previously undocumented baseline gate
(including the running-tree-first probe and fail-open behavior) and names the
V2 stream paths in full.
The baseline gate resolved the harness version from the healed
`$DSH_HOME/profiles/node_modules` mirror first, but that mirror is only as
fresh as the last CLI profile boot: it is rewritten by whichever installation
last healed it, and a packaged desktop host never heals it at all. A leftover
global CLI could therefore pin the mirror below the baseline and gate a
healthy desktop harness — an upgrade modal plus fallback (unfolded) projection
units on every launch (issue #59).
Resolve through the module tree the plugin is actually bound to instead: its
own ESM resolution (`import.meta.resolve`, the same pipeline its runtime
imports use, so an embedding shell that redirects plugin imports — the
packaged desktop's ASAR resolver bridge — redirects the probe too) of the
library packages it imports. A witness that lands inside this package's own
tree is the plugin's dependency closure (a `link:`-installed dev checkout's
pinned devDependencies), never the harness, so it is discarded. The home
mirror stays as the fallback for a dev checkout and for a running anchor that
cannot answer; the CLI package is still probed only through it.
Fail-open is unchanged: any unresolvable package, non-file URL, or unreadable
manifest degrades to `undefined`, which never trips the gate.
A config problem must never leave a Context entry hidden:
- a scope value the plugin cannot understand (present but invalid)
degrades the placement to `all` instead of stranding the previously
chosen one — the read path now distinguishes invalid from missing,
and only a genuinely absent field (older Host half) keeps the
current state;
- a placement write that fails to persist no longer keeps the
optimistic echo in-session: unless the scope's truth itself carries
a valid placement, the rollback degrades to `all` (other fields
keep the existing keep-the-echo behavior).
Closes#58: the Context panel hung off two registrations at once (the
conversation tab and the right-Sidebar guide entry), with no way to keep
just one. A new per-user `defaultPlacement` preference — the first row of
the Plugin configuration card — picks who carries the view: the
conversation tab, the right Sidebar, or both (the default, so existing
deployments see no change).
- shared: `DefaultPlacement` vocabulary (`all` | `tab` | `sidebar`) on the
settings namespace; the Host schema gains a loose `defaultPlacement`
field so a stale persisted value degrades to `all`
- client: `placement.ts` mounts each registration through its own factory
and, on a preference flip, disposes exactly what the new value drops
(the harness's own dynamic-mount idiom); `watchSidebarContextTab`
returns its deferred-inject disposer so the sidebar mount can be taken
down and put back; the settings card leads with the placement row