Each column is labeled when a month begins inside it: the month changes
between its Monday and Sunday, or its Monday is the 1st; the label names
the Sunday's month. Months that began before the window stay unlabeled.
New insightsEntry field ('show' | 'hide', default 'show') in the
dsh-context settings namespace, edited from the Plugin configuration
card. The sidebar entry subscribes to the store and renders null while
hidden; the seat stays registered so flipping back needs no remount.
Fails open everywhere: a schema-less host, an unserved namespace, a
section without the field, a stale persisted value (loose union), or a
write that never persists all resolve to the visible entry.
- Page the session grid at 12 cards per page: a filter change re-anchors
the pager at the first page, and clamping keeps a shrunken list's view
in range instead of showing a blank page.
- Redesign the session cards: 3 columns, a project pill, a first-active-
day line under the relative time, wrap-clamped titles with uniform row
heights, a turns-and-steps lead figure, and the cost label shortened
to 费用/Cost.
- Restack the insight band as one folding row — the KPI 2x2 block beside
the activity heatmap — and drop the aggregate composition card with
its now-unused fold, keys, and styles.
The raw session list carries every session while the workspace browser
hides its archive set, so the dashboard surfaced archived ghosts and
read as duplicates and stale rows. rowsOfSnapshot now drops rows present
in the workspaces snapshot's archivedSessionIds, failing open — an
absent seat or a hostile shape archives nothing.
A cross-session insight surface: the sidebar-foot entry (stacked above
Settings by the harness's own foot layout, with a running-sessions badge)
opens a frame-wide overlay through the shared module store. The panel reads
the root-scope useSessions/useWorkspaces standard kit — every list row's
host-cached projection values — so no session log needs opening:
- KPI band over the picked range (7d / 30d / all): active sessions, billed
tokens, estimated cost (models.dev book), cache-hit rate.
- Activity heatmap (GitHub-style, 12 weeks, square cells filling the card):
a day click pins the session grid to that day.
- Aggregate composition donut over the range's sessions.
- Session cards with a mini composition ring, billed/turns/cost figures and
a workspace-group / project breadcrumb (deduped when both names match);
grid filters by range, pinned day, group chips (incl. Ungrouped), and
search; sorts by recency, tokens, or context size; a card click opens the
session through the harness's own sessions.open and closes the panel.
Subagent-origin rows are excluded, mirroring the workspace browser's own
predicate (plain open() cannot address them). Every row is sanitized in
isolation — a hostile row degrades to a metadata-only card, never an error.
The Context Dashboard's heatmap and session cards read the session list's
projection column, which only serves durable cache rows. The new
contextActivity unit folds each assistant settlement into a per-day ledger
(billed tokens + completed requests, keyed by local day, 400-day retention)
alongside the existing contextTimeline/contextHeaders units.
A session folded before a unit existed — or one that predates the plugin
entirely — has no row until it next goes live, so a one-pass warm-up
(backfill.ts) cold-reads every such session's durable log through the
projection cache's own cold-read ladder (coldSnapshot): each unit seeds
from its cached rows, folds the remainder, and the refreshed checkpoint is
written back. Live sessions are skipped (a throwing liveness probe skips
conservatively too), unreadable logs are logged and skipped per session,
and the whole pass rides a deferred inject across the
sessionQuery/sessionProjectionCache/sessionPersistence/sessions services.
The Context card's cost cell now prices the current agent TOGETHER with
every subagent session's cumulative billed totals, and a new subagent-cost
cell prices the subagents' share alone; both hover bubbles explain their
scope, the merged one listing the per-model rates of every billed side.
The subagents' usage folds out of the session-list snapshot's warm
contextTimeline rows (subagentCostFoldOf, whole descendant subtree,
cycles cut), with cold relatives' slim heads standing in. Those head
fetches leave the Agent network card's factory closure for a shared
page-scope cache (agentHeads.ts) the stats board's seat reads too, so a
relative is fetched once no matter how many cards consume it; the card's
list-snapshot subscription moves there as useSessionsSnapshot.
The fetched composition heads lived in component state while the promise
cache lived in the factory closure, so a remount (tab switch) reset the
composed rings back to green and the cache hit kept every relative from
ever re-fetching. A cached read now replays into the fresh instance, with
an identity bail-out so the settled replay on every snapshot tick cannot
loop. Also drops a redundant route wrapper and aligns the spec helper with
the fetch transport it stubs.
The session-list block serves projection values only from the host's
projection cache, so a relative that never attached since the timeline unit
last changed lists pressure-only (occupancy without composition) and its
ring degraded to the single fallback arc — every subagent ring looked green
until its session was opened. The card now fetches those nodes' slim heads
from the /api detail route (one read per session per page, failures sticky)
and re-folds the forest with the row's missing contextTimeline injected; a
row's own value always wins, and any fetch failure degrades back to the
pressure-only ring.
The split generation's detail transport rode connection.rpc.handle, whose
registration reads webServer off the connection service's own context —
cordis's strict-read guard rejects that for plugin callers, so the channel
never armed and every detail read fell through to the static fallback (405).
Mount the handler through connection.fetch.register instead (the same
exact-route seam the harness's own file-upload and media-reference routes
use), riding the authenticated /api fence; the client POSTs it same-origin.
The detail payload now also carries the slim wire head at the same fold cut
(the composition scalars the Agent network's cold-node ring fetch renders
from); a missing or malformed head drops out alone, never rejecting the
payload the detail cards consume.
injectionSourceName read source.kind/plugin unguarded on its first line,
so a null/undefined source would have thrown the fold's caller (issue #69).
The fold's only call site already gates on isInjection, so the reachable
behavior is unchanged; the guard closes the trap for future callers and
matches isInjection's tolerance for foreign source shapes.
The Context browser's category header carried only the hover utility, so
the tint vanished the moment the pointer left an expanded card and the
header melted into its body. The open state now keeps the hover
background via a lc-br-cat-open state class - the same token as the
hover and hover-link tints - on every category card alike.
The slash menu candidate carried only its name, section, and description,
so the row rendered the bare "context" while every built-in command row
showed its icon and localized title. The candidate now carries the plugin
emblem and a cmd.label title (the name renders as the trailing alias on
builds that show titles). Both fields are pure display data: harness
builds that predate their rendering ignore them and keep the bare name.
The Context browser's Injected Context and Skill Injections rows previewed
the raw injected content, while the Context Events card named the producer
(plugin id, reconciled instruction files, durable kind). The fold now
stamps that identity on the surface node alongside the event, and the rows
prefer it for the collapsed preview - the content stays one expand away,
and the row filter still scans the folded text. Nodes folded before the
stamp (additive-optional on the wire and persisted state, no stateVersion
bump) degrade to the previous content preview.
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 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.
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 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.
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
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 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
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.
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
A ring with a neighbour on each side now gives half a 0.5-unit gap off
both ends of every arc (1 unit ≈ 1% of the circumference, so ~1px on the
96px card ring), letting the card's own background draw the line between
neighbouring colours. A lone painted slice keeps its full ring — a gap
there reads as a nick — and the cut never takes more than a quarter per
end, so a sliver like the 0.2% overhead wedge never vanishes whole.
Bundle the colourful document sheet as the plugin's own icon and fill the
right-Sidebar tab type's two glyph seats with it, the way the shipped files
type does: the guide capsule's `icon`, and a new `sidebar.right.pane.tab.title`
registration that draws the emblem beside the chip label.
The chip's close control sits out of flow over the title's last 14px and an
active chip fades the 16px before it, so the shipped files chip stays readable
only by its short label. The label now carries a trailing gutter so that fade
lands past the text instead of on the last glyphs.
Registration turns rollback-safe (each disposer is tracked) so a partial
failure unwinds cleanly, and the obsolete primitives-icon resilience spec goes
with the icon read it guarded.
`SidebarRightGuideEntry` carries `order`, `title`, and an OPTIONAL
`description` again at dsh 0.1.5-rc.1 — it was required at 0.1.5-alpha.1,
dropped at 0.1.5-alpha.2, and restored optional now. The plugin's guide
capsule re-adds its `description` thunk and the `sidebar.guideDescription`
dictionary keys, so the right Sidebar's guide entry renders the one-line
subtitle the shipped Files type does.
The mirrored face marks the thunk optional: a line whose guide body ignores
it simply goes without, so the same contribution stays valid across the
whole 0.1.5 line.