Opening a category or an exact row grew the rows region's flex base, and
the capped card charged the whole deficit to the map — the only other
shrinkable item — folding it to its 40px floor on every pick. The map sat
in the fixed zone but still changed shape under the click.
Pin the map's flex base so opened rows scroll the rows region instead of
squeezing it; a percentage ceiling still folds the map when a capped card
is genuinely too short (a content-sized card ignores it). The /context
dialog's hard viewport cap is the one real pressure: while the map shows,
the dialog fills its cap and the browser card and rows region size from
the leftover, so the map folds exactly to what is left instead of
overflowing.
The sort toggle ordered rows by counts, recency, and path; the row's own
last segment — the bold name a reader scans for — had no order of its
own, so same-named files in different directories never grouped. Name
leads on the displayed last segment (either separator style, via the
shared leafNameOf) and the full path breaks ties between rows that share
a name.
The Preferences picker, the host Config union, the shared
DefaultFileSort vocabulary, and both dictionaries carry the fourth
option; glyphOf now reuses the same leaf helper instead of its own split.
The Input / Output capsules were dead weight beside the jumpable figures:
every destination sat below the fold. Human Inputs now opens the Context
Browser's User Messages category — the same one-shot catFocus relay the
skill and answer figures ride — and Read / Written / Searched / Images
land on the File Activity card with that kind chip picked: a one-shot
filter focus the card applies, hands back to the caller, and clears its
own text lens and open row for.
The pills became hover links: pointer, lifted wash, focus ring and an
action title, with Enter/Space parity through one shared jump wiring. A
pill carrying its own ? bubble keeps the native title off so a single
hover shows a single hint. The figure relay was unified as onJump
(onFigureClick is gone), and the browser reveal grew into revealCard,
which the File Activity landing shares.
The tab capped the browser column at the left cards' height bounded by
the host scrollport's slice from the card's top edge; that slice added
the scroll offset on top of rect coordinates, so a scrolled or short
pane clamped the card far below the left column — an empty gap under
the composition + trend cards.
Cap the column at the left cards' measured span alone (first card top to
last card bottom, rounded up): the row's stretch then makes it the
card's height, so it ends flush with the two cards at every width where
they sit side by side, while the DNA feed still scrolls inside the card.
Drop the pane-fit plumbing and its now-dead scrollport observation, and
replace the README's stale three-times claim.
The /context dialog scrolled as a whole while the DNA feed rode it, and
scrubbing the composition bar dragged the dialog too — the row reveal's
nearest scrollable ancestor was the dialog card itself. In the tab, the
3x left-column cap let the card outgrow the pane and change height on
every DNA toggle, dragging the head off-screen and the cards below with
it; band picks scrolled the page via scrollIntoView.
Make the card's own fixed zone the rule in every host: head, DNA lens
and block map stay put while only the rows region scrolls. The tab
column caps at the left column's height bounded by the host scrollport's
slice from the card's top edge (browserCapOf), so toggling DNA or
opening categories never moves the cards below; the /context dialog
becomes a filled flex column whose browser card shrinks instead. Row
reveals bind to the card (revealRowInCard): scrubbing and picks never
scroll above it, and the rows region contains its wheel so the page only
moves when the pointer leaves.
DnaBar's gradient stops were percentages of each bar's own total, so the
browser rasterized common strands differently per bar (sub-pixel bands
flipping visible/merged between steps) and inherited bands read thicker
and thinner across the chart. Stops now ride the shared per-token pixel
scale (CHART_H/maxTotal) and are byte-identical in every bar; the bar
height reuses the same scale. Live-measured on a 99-bar session with the
duration curve off: adjacent-bar common-row pixel diffs dropped 589 -> 11,
the remainder being genuinely new top-of-bar strands.
Row lists (the DNA flat feed, category bodies, tool schemas) previously read
as one undifferentiated white field. One rule keyed on the shared .lc-br-elem
row factory stripes every list: `nth-child(even of .lc-br-elem)` keeps the
phase stable across toolbars, notes and filtered-out rows, the tint is 60% of
the theme's interactive hover wash so it sits under hover and open states,
and an open row keeps its active highlight alone.
Opening a browser row/category persists focusCat with no visible UI; with DNA
on, TrendChart then scaled bar heights to the focused category's share
(focusOf) while the band data and the lifetime slices stayed in full-context
offsets — dark slices floating above half-height bars. The DNA faces now pass
no focus into preprocessTrend (the stacked faces keep it), and the previously
false-positive 'focus is ignored' test now discriminates: with the axis driven
by a second bar, focused heights (22px) diverge from the full-context share
(56px).
The client POSTed every plugin route as a root-absolute /api/dsh-context/*
path, which stripped a reverse proxy's /app/dsh prefix at the gateway —
every poll 404ed at it, the panel retried on an aggressive backoff, and
the deployment fell into the issue #159 reconnect loop. The six route
constants now live in one module as their browser-relative spellings:
fetch resolves them against the page's document base, the same idiom
dsh's own stream/socket clients ride, so a prefixed proxy keeps its
prefix and a direct deployment resolves to the exact same URLs before
and after. A host/client pairing spec pins each spelling to its route
registration, and docs/compatibility.md gains the §5.8 client-seam rule.
Fixes#159
Hovering a band on the DNA bar now lights the row it names below — a message
band its exact feed row, a tool-schema band the tools group row, the system
band the system row — the reverse of the row-hover-to-band link, so the two
surfaces read as one surface. The echoed row also scrolls into view, confined
to the nearest scrollable ancestor's own scrollport (the capped column): the
row only has to reach the scrollport's visible slice, since a capped column
usually extends below the page fold and a page scroll would drag the pointer
off the pinned band mid-scrub. Clicking a band still lands on the row and
expands it.
The DNA flat feed lays every message out at once, and an uncapped card would
outgrow the page. The browser column now measures the composition and trend
cards stacked beside it — per render, followed by a ResizeObserver for pane
drags and card reflows — and tops out at three times their sum, scrolling
taller content inside; the card head (title, composition bar, notices, and the
DNA feed's lens) stays pinned above the scrolling list. The /context modal
keeps its own single-card scroll and stays uncapped.
In DNA mode the category accordion yields to one flat list of every assembled
item: the model reads the prompt oldest-first, so the list presents that order
bottom-up — the newest message is row one, and the tool schemas (one group row
opening into the category body) plus the system prompt close the list. Each row
carries its #prompt position, a category chip tinted with the category's own
palette color, its introducing-step stamp and token price, and expands into the
same full content.
A text lens pinned in the card head filters the feed's rows and the open tools
group's schemas alike (the served system prompt text included); hovering a row
lights its exact band — or its band group for the header rows — on the DNA bar
without reporting upward; a category jump (catFocus) drops back to the
accordion, which has no row landing in the feed.
The card was pinned by the fixed 720px cap, so wide panes left large
proportional blank space on both sides. Width is now
min(calc(33.333% + 480px), calc(100% - 32px)): each side margin is
exactly 2/3 of the old layout's at every pane width, and the narrow
gutter floor drops from 24px to 16px.
Whole-text JSON payloads (a leading { or [ that parses) open on the shell's
JsonTree inspector instead of markdown: the switch offers Raw|JSON there
(markdown is meaningless on structured payloads), the tree opens two levels
deep via the expanders' own click path (focus restored, capped at 100), and
an expanded row drops its inline preview Chrome-DevTools style. The JsonTree
faces are pinned per baseline and probed by the compat matrix.
The 1px border-l2 hairline on a white layer-2 background reads as
borderless. Adopt dsh's own form-field recipe (0.5px solid border-l4,
the settings inputs/selects idiom) for all four native selects: the
overview coverage picker, the fleet sort/filter, and the context
browser step picker.
Archived sessions fold back into every figure the page derives off the
session list: rowsOfSnapshot now tags archived rows instead of dropping
them, and one coveredRowsOf pass scopes the KPI band, charts, heatmap,
skills, and the session grid together. A "Filter sessions" select next to
the list's search box offers dsh's own archived-filter set — All (show
archived) by default, Hide archived, Archived only. The Active Sessions
card follows the picked coverage (Sessions incl. the window's archived
count under All; Active/Archived Sessions over the full-list total under
the narrow scopes), archived cards wear an Archived tag and refuse to
open like dsh does, and kpisOf drops its now-unused listed param.
Wrap the meta row and the composition bar into one main column with the
enlarged map toggle as a right column stretching across both rows, so the
figures shift left of the card edge instead of crowding the button.
A bar-right toggle reveals a squarified treemap of the shown step's
context: category regions sized by tokens, with same-kind items folded
into one weighted block (every bash result together, skill loads by
skill, injections by form) and an 'others' tail past the fold cap.
Clicking a singleton block lands on its exact row (the DNA band pick's
path), a folded block filters the category's rows to the group's tag,
and a region header toggles the category like its row does. Block and
region hovers float a figure bubble and, outside DNA mode, report the
category to the shared composition hover; the open category keeps its
region framed.
The pinned step's per-category rows were a second rendering of the same
categories, drifted from the one above: their own grid, dimmed regular-weight
labels, a fixed figure column. The panel now renders the composition card's
own chip legend — same dot, 600-weight label, right-aligned figures with the
secondary share, same hover highlight and 160px grid reflow — in the shared
CATS order, so a category reads identically in both cards.
Delta mode has no counterpart above, so makeLegend gains a delta variant: the
chips print the signed change colored by sign, and both the share and its
"of used" hint drop out. The panel's chips report hovers back exactly as the
composition legend's do, and the reserved lane mirrors the chip grid so the
pending and loaded panels still measure alike.
The pinned step's per-category rows carried a mini bar beside the figures
— the share track in Total mode, a diverging zero-line bar in Delta. Both
go: a row now reads dot, label, figure, share, with the figures pinned to
the row's right edge, and the trend card's reserved lane mirrors that same
anatomy so the pending and loaded panels still measure alike.
The Fleet task board's opened card boards its owner's report-back from a
wire excerpt the route clips at 300 chars, so expansion could never show
the whole text even though DSH itself hands the fold the whole log. The
fleet route now reads a `full: true` body whose prompt/reply skip the
excerpt caps (only a hostile-payload bound stands), and the task board's
unfold asks for it once per session per mount: the excerpt renders
first and swaps in place on landing, the section meta carries
loading/excerpt-only states, and the scan never pays for the full read.
The head keeps a teammate's roster name in bold with their duty in the light
secondary tone (the member cards' desc idiom); the self and lead cards keep
their title. The label box grows to a fixed 3 lines so the duty wraps to 2 —
the card grows uniformly with AGENT_CARD_H, keeping every card's metric row
aligned and the link anchors on the card's edges. The geometry pins derive
from literals so a layout bug cannot mirror its own arithmetic.
The aside rides the shipped catalog's own address and options, addressed by the
node's direct parent; a face that cannot take the resource keeps the whole-view
jump on every supported line. The openers' test suites move to their own file
to hold the 1000-line cap.
Bold the card title at the weight the figures previously carried (650),
and drop the figures line's bold both in markup (<b> to <span>) and in
the CSS so the tokens/cost figures read regular.
The overview's quartile poles and the turn strip's width-quartile labels also mark the
full-span position (the last record / last turn), so the log's end carries its figure like the
quartiles do. The full-span pole insets by its own 1px width (calc(100% - 1px)): at exactly
left 100% the pole falls wholly past the scroller's clip, while the number label keeps reading
leftward inside the pane. The collapsed-at-start pole still drops — at the left edge its
label would clip and the pane edge already answers. Chip tips and the README now say quartiles
and the full span.
One toggle next to Duration flattens every retained step (or turn) into a continuous stacked
ribbon across the visible width — no horizontal scrolling, nothing computed while it is off:
the drawing geometry memoizes on [requests, mode, duration] so hover re-renders never rebuild
the polygons. Flags mark the record quartiles only (interior-only: short logs' collapsed edge
quartiles drop), and the turn strip stays proportional with its labels on the width quartiles.
Interaction keeps the bars': nearest-record hover with the shared tip, click to pin, scissors
markers, the duration-curve overlay on the same svg with its right-hand axis, Total/Delta
(signed up/down bands around the proportional zero line) and category focus. Exclusive with
DNA mode and Adaptive: activating one clears the others.
trendChart.tsx lifts focusOf/deltaOf/trendTipRows and the whole-log maxima to module scope
(preprocessTrend/trendMaxima) so both faces share one preprocessing path and one tip shape.
A cold /detail request folded the whole log synchronously inside the request
(~0.35s per 30k events on a fast host) and re-folded on every view. The fold
now yields to the event loop every 1,000 events (the result is asserted
byte-identical against a buildTimelineDetail reference), and the folded
payload is cached keyed on the observation's (persistence instance, session
id, durable revision) tuple — the same trust tuple dsh's own observation
reader uses — so an unchanged cold session re-serves in ~1ms while a changed
log, a swapped backend, an observation without a revision (a live-race cut),
or a config remount always refolds. The cache is bounded at four payloads
(each bounded by the fold bounds) and lives in the watch closure.
The observation lease now disposes on every path including the early returns
(a latent pin on hostile faces is gone with the restructure).
The pass used to hold the host at ~60% duty for the whole corpus window (a
fixed 100ms sleep between full-log folds). Rests are now proportional to the
measured work (at least a 25ms floor) at both granularities — between slices
and between sessions — capping the pass near 50% duty, and every 8s of
cumulative fold work the pass takes a logged 2s recovery pause and resumes on
its own (the issue's 'bounded, background, visibly paused' ask). Slice work is
billed exactly once across the two granularities, and every sleep wakes early
on abort so dispose never waits out a pause.
Pacing is injectable (BackfillPacing) for deterministic tests, mirroring the
cold-read gate's injectable reading; the sliced-fold suite moves to its own
spec file with the shared rig extracted to tests/host/helpers/backfill.ts.
What gets folded and the final cache rows are unchanged — only when the loop
breathes.
A whole-log cold restore lands on the host event loop as one uninterruptible
synchronous block (~1s per 30k events on a fast host, more on a Windows
desktop), and a 100+ session backfill strings those into a window the desktop
renderer watchdog cannot survive (issue #145: 118 sessions, 47s, renderer
killed).
Sessions over 2,000 events now fold in 2,000-event in-memory slices through
the public registry.restore, yielding between slices, with exactly one durable
write-back at the end (sessions.prepare + registry.hydrate with the rows
already at the log end, then sessionProjectionCache.write). The composed rows
are byte-identical to one single-shot coldSnapshot (asserted with the real
registry + the plugin's real units), no intermediate durable rows land, and a
crash mid-fold keeps the single-shot failure semantics. A session going live
mid-fold skips silently; a write-back failure stays fail-soft.
Faces are re-proved per deployment and any absence (older/unknown harness)
keeps the single-shot coldSnapshot path; all four faces exist on both
supported baselines (0.1.7-rc.2, 0.2.0-rc.2), verified against the tags.
The file card priced only the pathless branch of the harness handle — the one
dsh emits when the stored copy cannot be read — and disclosed that shortfall
with a `≥` lower-bound marker, which read as a bug beside the image rows' `≈`.
Every other token row reports a heuristic estimate, so this one now does too.
The tooltip claimed the handle named "where the stored copy can be read": the
path-bearing branch, which costs roughly 1.7x more and is never priced. Under
`≥` the marker covered that gap; under `≈` the explanation described a branch
the number does not come from, so the clause is gone from both locales. The
estimate itself is unchanged.
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.
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.
@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.