A file block never reaches a model as bytes: dsh projects every one to
`fileHandleText` at request assembly, and the token meter prices that handle in
place of the block's structural JSON price. An attachment therefore costs a
fixed ~60 tokens whatever its size, so the card now shows that price beside the
size — computed from the three facts a log carries (name, byte size, digest
prefix) and priced with the meter's own text arm, now shared with the fold's
message price instead of restated there.
The handle also names the stored copy's read path, which only the live
environment resolves and no log holds, so the row prices the pathless arm and
reads as a lower bound (`≥`) rather than an `≈`. The fold keeps the meter's
fixed structural price for the same block, exactly as the harness does: one
figure answers the request, the other the surface.
Three cards-side-by-side fixes: the hover invite is one pill over the
thumbnail instead of a mask that blacked out the glyph; a file card drops the
type row its extension badge already states (an image keeps it, having no
badge), which leaves name plus size; and the thumbnail read now keys off the
attachment id instead of the reference object, which the fold re-proves on
every render — a row re-render no longer re-reads the bytes through the
harness loader. The in-flight card also reports itself busy to assistive tech.
The invite as an extra meta row read as clutter and grew the card on hover.
It now covers the attachment's 64px thumbnail (a dimmed mask with the label
centered), so the facts beside it never move, a long English label wraps
inside the tile, and keyboard focus shows it too.
The optional-column story the File Activity card already documents gains its
second consumer: `/api/dsh-context/attachment`, what it authorizes (only an id
the session's own log cites, and where the harness's gateway is stricter
still), how a verbatim file's named alias differs from the hard link a
suffixless image object needs, and what degrades when a face is absent.
The bullet now covers what the block actually shows: images and files in the
message's own order, one card each, and the one-click preview in dsh's preview
column. The screenshot still depicts the image cards (its caption says so)
until a fresh one is shot.
A user message's image and file blocks now render as one ordered attachment
block in the Context browser: images keep their session-authorized thumbnail,
other files show their type glyph, extension, and stored size, and the row
chip counts every attachment. One click IS the preview — the card hands the
attachment's named host path to dsh's own preview column (the File Activity
card's `previewAddressOf` → `openResourceVia` path), which renders markdown,
code, PDF, office, and image viewers under the file's real name. A refused
open reports on that card alone; the image lightbox and the per-type modal it
replaced are gone with `images.tsx`.
The path comes from the plugin's own `/api/dsh-context/attachment` route (the
same authenticated `/api` fence, Connection exact-route registry, and
cold-read gate as the detail and fleet routes). It answers only an id the
session's own log cites — a depth-bounded, total walk over the event payload
(user content, tool-result blocks, spliced inbox deliveries) — and answers
the store's named alias: `attachments.fileHostPath` for verbatim files, and a
published-or-reused named hard link for images, whose objects are digest-named
and suffixless while the preview column dispatches viewers by suffix. The
link shares the immutable object bytes and lives in a disposable cache
(`<home>/cache/dsh-context/previews/`); a store that is not host-file-backed,
or a media type the harness never admits, resolves the typed null the card
reports as an unavailable preview.
`Section` moves to its own module so the attachment block heads itself
without importing the tool card, and the compat matrix probes `FileTypeIcon`
alongside the icons it already pins.
The trigger percentage is derived from dsh-compaction-basic's mirrored
shipped defaults rather than read from the deployment's configured policy,
so hedge the copy instead of asserting it outright.
The mirrored headroom has no named constant in dsh-compaction-basic, so
there is no symbol to grep for; name the resolveConfig fallback to read
instead.
The output mask's tooltip said this reply's output is reserved at the
window's end, but the value is the model's configured max output tokens.
Update the zh/en copy and the two tooltip assertions that pin it.
The output mask striped at -45deg while the headroom mask striped at
45deg, so the two read as a chevron at the seam instead of one hatched
reserve region. Same angle now; the stale colour-blind hatch-direction
claim goes with it.
The two tooltip labels named the mechanism, not the thing on screen: "Max
output tokens" reads as a statistic, but the mask draws the window's tail held
back for the routed request's reply, and "Compaction headroom" said 余量 — slack
— where it is in fact reserved space. Say both as the regions they are:
Compaction Headroom 65.5k
LLM Output Reservation 256.0k
本次回复的输出预留在窗口末尾
Both English labels now share one Title Case so the three-row tooltip reads as
a set. The note lines keep their sentence form, so only labels changed.
The bar's tooltip was one nowrap string per hover kind, the only tooltip in
the plugin not yet multi-line. Split it into head / figure / note so all four
kinds share one skeleton and wrap:
Tool Schemas ≈6.4k · 3% of used context
Free Window 733.2k · 73% of window
Compaction headroom 65.5k
Auto-compaction fires at 68% of the window
Max output tokens 256.0k
This reply's output is reserved at the window's end
Every figure now names its denominator. The segments share the USED total and
the free track the WINDOW, which the old single line never said; the two masks
had no share at all and now state where they sit in the window instead, read
off the same clamped trackPct the masks lay out with rather than a second ratio.
Replaces the hardcoded 12/88 left clamp — a guess at half the tooltip width that
only held around a 920px pane, and cut the bubble off at narrower ones. The
percentage anchor stays the source of truth; a layout effect measures the tip and
adds a px shift only when the rows overhang. Rects, not clientWidth, whose whole-
pixel truncation lands the residue straight back on the overhang.
Also adds role="tooltip", and box-sizing so the width cap means what it says.
dsh-compaction-basic REFUSES a target at config time when the output
reservation plus headroom leave no pressure budget (resolveCompactSpec
throws TargetPressureConfigError, compactIfNeeded never runs) — it does
not clamp. The reserve comment claimed a clamp, and the geometry acted
on it: a too-small window collapsed threshold to 0 and painted a
full-width headroom band, claiming a trigger the harness never fires.
The bar now reserves nothing and paints no masks for such a window,
matching the harness. Also removes the redundant reserve comments the
review flagged and restores the dashed-border rationale the mask CSS
had displaced.
The Current Context bar showed a single hatched band mirroring dsh's
default 80% threshold ratio, which reads wrong on any window where the
pressure budget binds instead (small windows, or a large routed output
cap). It now renders the two real reservations the harness spends the
top of the window on, priced by dsh-compaction-basic's own min() rule:
- the compaction headroom mask (mirrored default 65,536 tokens), then
- the routed request's output reservation, read from the durable
request/header's materialized config.maxTokens rather than guessed.
The output cap folds off request/header at fold time, so the timeline
projection's stateVersion bumps 27 → 28 to refold cached rows from the
durable log. Geometry lives in a new client reserve module; the bar
tiles [threshold, window] with the two adjacent masks, each with its
own name-plus-length tooltip.
Documents the three non-obvious dependency rules that a source scan
alone mis-reads: primitives publishes shiki/katex/mdast/micromark/
anser/clsx/diff/simple-icons as bare imports the plugin must declare;
dsh-client-store + dsh-util-code-language (and zustand/immer behind
them) are reached only through the primitives dist; and tailwindcss
is deliberately not a direct dependency.
@tailwindcss/node already depends on tailwindcss at the same exact
version (4.3.3), so a top-level entry would only risk a second copy
resolving in the build process. The one-copy pin the native-binding
rule requires is now held solely by @tailwindcss/node's exact
dependency; update the tailwind.css comment to match.
Reading a day's volume meant hovering every column or interpolating between
the left rail's quarters. Each blue bar now carries its own figure on its
top edge, so the common case reads straight off the chart; a day with no
tokens prints nothing, since there is nothing to say.
The label is a child of its bar rather than positioned against the column,
so it rides the bar's height without a second source of truth for where that
top is. The gridlines move into the bars' own box for the same reason: with
headroom taken above the plot, `bottom: N%` still reads the bar scale, so the
822.1M line keeps marking the 822.1M bar instead of drifting by the headroom.
Measured across ten pane widths: the labels overlap below a ~30px column, so
they fold out under the same 400px container query the day strip already
uses — the hover tip carries the exact figure there, and keeps doing so
everywhere else.
The Context tab's Plugin-info card already hinted at a newer npm release;
the Context Insights page head had no such hint. Add one beside the
balance capsule, in that page's own capsule idiom rather than the card's
inline chip: label plus the version in the warn token, linking the release
notes through the page's portaled hover bubble.
The registry check moves out of pluginInfo into a useNewerVersion hook both
surfaces read, so the second hint costs no new fetch path, state, or
mechanism — one TTL-cached read still answers both. Its own coverage moves
with it into latestVersion.spec, leaving each surface's spec asserting only
what that surface renders.
The two head capsules share one CSS chrome instead of a copy, and the head's
gap drops to 8px so the capsule cluster tightens; the title keeps its wider
16px lead-in, and the fold at narrow widths leaves every wrapped row on the
same left edge with no overflow.
The task card folds at a default height and grows with the network card
beside it, so the two columns' bottom edges keep meeting: an observer
measures the graph's NATURAL content height (the stretched column's own
height would self-fulfill the cap) beside the members card's, and the cap
takes the larger of its default and their difference. The comms card folds
at its own static cap. On both, the toolbar pins and the rows scroll under
pure-CSS edge fades that announce cut-off content with no JS.
watchPlacement inferred "this side is mounted" from the mount handle
being a function, so a face that seats the seat and hands back no disposer
(a stripped or foreign `slots`) read as never mounted — and every later
preference publish registered a second copy of the same seats. Track the
mounted state directly; the handle is unwound when there is one.
The same package can mount twice into one page's slot ledger — a static
profile bundle plus a later activation of the same package. The
client-plugin runner force-assigns a fresh shadowing priority to every
registration, so the registry's same-id duplicate guard never fires across
the two mounts, and the conversation header — which renders the raw ledger
and matches the active view by entry id — paints both copies of every
list-slot seat.
apply() now reads the raw ledger first (SlotRegistry.entries) and mounts
nothing when this bundle's chat-to-Context jump entry is already seated.
The read fails open: an absent, shape-shifted, or hostile face reports
unseated and the mount proceeds, so an older line never blanks a working
mount. The entries seam is pinned per baseline.
The harness-side defects (the runner forcing a rank on additive list
slots, and the header rendering the raw ledger) are left to
deepseek-ai/deepseek-harness.
- A pick anywhere (graph node, member card) now steers the comms timeline
beside the task board: rows narrow to the traffic the picked agent sent or
received, the dropdown steps aside for the pick chip (label, never a raw
id), and an agent without traffic says so
- The pick governs while it stands: the dropdown's own choice yields on
landing and never resurrects stale once the pick clears; without a pick the
dropdown keeps its free per-participant browsing
- The pick key resolves once in the view (roster name -> session id, a
subagent's own id) — the one key both boards narrow by; zh/en gains the
no-comms note
The members board's 26px avatar boxes dwarfed the 20px hue icons the task
board settled on; one base class now carries the shared size, so the members'
cards, the task rows, the band heads, and the opened agent boxes all read at
the same scale — and the task board's size patches fold away.
- A task card opens on a pick (or Enter/Space on the row): the task brief, the
owning agent, and the final report-back each box up in the context tab's own
section chrome, with the raw/Markdown switch on every box head — Markdown the
default, the report-back stamped with its time
- Closed rows keep the compact read, the description clamped to two lines and
rendered through the shared Markdown renderer (memoized per text, so board
hover re-renders never re-parse)
- The row pick no longer pins: pinning stays with the owner chips, the band
heads, and the agent box inside an opened card; Enter on a nested control
keeps activating that control instead of folding the card
- zh/en gains the box labels (task brief / agent / final reply)
- Plain subagents join the board: each delegation reads as a task of its own
(the prompt as the subject, the session state as the status) with the run's
live figures on the row — tokens · duration · steps and the pressure-tinted
occupancy; the row itself is the agent (family-hue icon, session id on hover)
- One vertical, owner-grouped layout replaces the status/owner switch: roster
members first (the lead in brand hue, teammates by stable roster hue), stray
owners the roster never served, then headless delegation rows in spawn order,
the unowned pool last; status lives on the rows
- The toolbar gains search (subject/owner/id) and status filter chips with
counts; the completion bar now counts every row on the board
- A pick anywhere narrows the board to that agent — graph nodes and member
cards included, subagents by their own session id (the pick chip names the
label, never a raw id); the members board reports picks raw and the view owns
the toggle, so a release lets go of the paired narrowing wherever it came from
- Renamed to Tasks (team tasks and subagent delegations); zh/en updated, the
dead view-switch keys removed
The legend joins the header stack — after the chips row, before the stage —
instead of dangling under the canvas, so the category colors read against the
cards before the forest scrolls. The bottom margin keeps the chips -> legend ->
stage rhythm at 10px.
- The cockpit row splits evenly: the graph column drops its flex-[2] weight
- The network card's flex basis returns to its own content (flex: 1 1 auto):
a 0 basis let engines size the row line under a deep forest's canvas,
clipping levels behind the card; content now sets the floor and the card
still stretches to the boards column's height
- Cards keep a 178px minimum width (slot pitch floor 150 -> 190) with tighter
padding (9x10 -> 7x9) and a 100px height, so dense families read wider and
calmer
- The corner arrow button is gone: in the Fleet seat a card click selects and
the jump stays with the boards' own open buttons, while the sidebar seat
keeps opening on click; the select-mode subtitle no longer names an arrow
- The other-agents section renders the family's plain subagents, ancestors, and
siblings as the same cards the team roster uses — label as the identity (two
clamped lines), node-state dot, status + model line, landed latest reply, and
an open button; the card hue follows the graph's family hue
- Member cards gain the run's activity meta (tokens · duration · steps, with
the pressure-tinted occupancy figure), team cards included
- A compact card layout (8x10 padding, tighter gaps) and a 200px grid floor
keep the two-column read the common case; the fleet-row list is gone
Side by side in the cockpit row, the Agent network card now fills its column
(the browser view's lc-col-browser idiom), so its bottom edge meets the
members-plus-tasks column instead of dangling above it. Wrapped onto its own
line on a narrow pane the stretch is a no-op — the card keeps its natural
height.
- Overview strip: family stats (agents/running/done/tokens/est. cost) with an
adaptive progress bar — shared tasks when a team serves any, else subagent
completion, and no bar for a solo session
- Agent network: taller link channel (level pitch 144→168), node cap 25→40,
and a card click now selects (ring + the task board's owner filter) instead
of jumping sessions; the jump moves to a corner button on each card
- The graph's cursor-following inspector leaves the Fleet tab (the boards
carry that detail); the sidebar mount keeps it
- Members board absorbs the roster: team cards section plus a compact
other-agents section, one search/status/sort toolbar steering both; the
standalone FleetList card is gone
- Task board: completion bar, status/owner grouping switch, owner pinning and
hover lineage from task rows, transitive prerequisite-chain highlighting,
and a guidance empty state without a team
- Both boards stay mounted with guidance notes in team-less and solo
sessions, so the page never reshapes between session kinds
- Hoist displayCurrencyOf into cost.ts (was duplicated per mount)
On DSH Desktop for macOS the shell moves the window by elements marked
`data-window-drag`, and the shipped pages mark their head, which their
top padding stretches over the hiddenInset title bar band. The insight
page head already takes that band (`--dsh-frame-top-clearance`) but was
not marked, so the window could not be dragged from the top of the page.
Mark the head. The harness keeps buttons, links, fields and anything
outside its root out of the drag area, so the settings chip, the
balance link, the date range and the range group stay clickable, and the
date picker's portaled dialog is unaffected.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The card titled itself 任务看板 / Task Board while its empty state already
quoted the harness's agent-team UI verbatim (暂无共享任务,可以通过对话创建).
The title now carries the same term the harness's client-ui-agent-team
serves (共享任务 / Shared tasks), the two member-card tips follow
(在共享任务中…), and the subtitle describes the card as the agent team's
shared task list (智能体团队共享任务清单).
The Fleet tab's Team Members and Task Board cards stacked vertically no
matter how wide the pane ran. They now share a `.lc-cols` row — the
plugin's own two-column idiom (flex-wrap + the shared
`flex-1 min-w-[min(360px,100%)]` floor the head-band cards ride) — so
they sit side by side as two equal-height columns and fold back to
full-width stacking below two 360px floors plus the row gap.
Gating semantics are unchanged: the task board still serves whenever the
team projection exists, even without a family forest (the duo row then
carries it alone at full width), and the members board still needs the
forest for its roster join. Tests pin both the together case (same
`.lc-cols` parent) and the forest-null case.
Three review passes (standards, contract, over-engineering) over the whole
settings surface left four follow-ups worth taking:
- sync() wraps its snapshot parsing in the compatibility.md parser gate's
bounded catch: a served value that throws on property access keeps the
current state instead of throwing into the mirror's fan-out, with the
mandated hostile fixture beside the happy path.
- the attach disposer now clears the scope, so a set() after disposal never
writes into a dead form; a superseded attach keeps the newer scope bound.
- dead surface trimmed: six type re-exports nothing imported through the
module, a single-caller query helper, and an orphaned export on the
settings-card props interface.
The card's eleven rows had grown into one flat list whose only structure
was "Topic · Field" prefixes baked into the labels. The rows now sit under
three titled groups — Entry & Visibility, View Defaults, System Prompt —
so the card reads as where the plugin appears, what its surfaces show by
default, and what it injects, in that order.
The harness projects no group chrome into the `plugins.bundle.config`
slot, so the card draws its own: group heads mirror the harness's
Plugins-page `.sectionTitle` (14px/500 primary, one weight step above the
pinned-400 row labels), separated by its row-separator token. Each group
carries `role="group"` + `aria-label`; the prompt hint moves inside the
prompt group it describes.
Labels drop the prefixes the group titles now carry ("Trend Chart ·
Default granularity" → "Trend Chart · Granularity", "System Prompt ·
Prefix" → "Prefix"), zh/en in lockstep. The spec test asserts the three
groups, their order, and the hint's seat; row order is unchanged.
i18n.ts had grown past the repo's 1000-line limit with the two dictionaries
inline (the prefix/suffix keys pushed it further). zh — with DictKey, the
key-set source of truth — and en, pinned to it at compile time, move to their
own files; i18n.ts keeps the shared Translate type and re-exports.
Two new per-user preferences, `promptPrefix`/`promptSuffix`, hold free text
joined at the very start / end of the harness's assembled system prompt for
all agents (subagents included) — a per-deployment persona or behavioral rule
riding the harness's own prompt registry instead of session-log surgery:
- src/host/promptInjection.ts registers two global prompt sections through a
deferred `ctx.inject(['systemPrompt'], …)` (a composition without the
registry degrades to a no-op; the registration unwinds with this plugin's
fiber). Orders anchor on the harness's own section-order registry, so a
central renumbering keeps the prefix first and the suffix last. Text is
registered `interpolate: false`, so `{{...}}` in user text stays literal.
- The section text providers read the volatile config references the loader
rewrites in place on preference edits, so an edit applies from the next
turn without remounting the entry; new sessions carry the text from their
first turn through the harness's own system-prompt projection.
- The settings card grows two free-text rows (local draft, committed on blur
and on unmount; a host push never clobbers in-progress typing), zh/en
strings, and the served-value sanitize/degrade path for the string fields.
Verified against a running harness: the committed text persists in the
profile layer, and a new session's system prompt starts with the prefix and
ends with the suffix. docs/compatibility.md documents the seams, and the
compat matrix probes them on both supported lines.