Commit Graph
240 Commits
Author SHA1 Message Date
bowenliang123 dd05bbda4e feat(client): price a file attachment by the handle the model receives
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.
2026-10-13 23:15:39 +08:00
bowenliang123 f348196d3b style(client): calm the attachment cards, and stop re-reading thumbnails
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.
2026-10-13 21:52:40 +08:00
bowenliang123 6487602f1e feat(client): render message attachments in order and preview them in the sidebar
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.
2026-10-13 21:19:47 +08:00
bowenliang123 a5438855d7 fix(client): state the compaction trigger as expected, not certain
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.
2026-10-13 19:52:41 +08:00
bowenliang123 1ff26107b5 fix(client): describe the output reserve as the model's max output tokens
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.
2026-10-13 18:53:02 +08:00
bowenliang123 396ada8754 feat(client): rename the reserve masks to Compaction Headroom and LLM Output Reservation
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.
2026-10-13 17:18:19 +08:00
bowenliang123 863d3cdcdc feat(client): true-size reserve masks, with a trigger line when the ratio detaches it 2026-10-13 16:51:25 +08:00
bowenliang123 1358603759 feat(client): give the composition bar tooltip three named rows
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.
2026-10-13 16:15:14 +08:00
bowenliang123 e421a35b3b feat(client): split the Current Context reserve into headroom and output masks
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.
2026-10-13 14:47:28 +08:00
bowenliang123 e4b3012897 feat(client): print each day's tokens on its own bar in the usage chart
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.
2026-10-12 13:29:54 +08:00
bowenliang123 018e92964f feat(client): add an update capsule to the Insights page head
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.
2026-10-12 13:24:35 +08:00
bowenliang123 2ce5d9a7db feat(client): narrow the comms timeline by the picked agent
- 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
2026-10-12 02:21:17 +08:00
bowenliang123 ac5b36b784 feat(client): unfold task cards into boxed brief, agent, and reply sections
- 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)
2026-10-12 02:05:11 +08:00
bowenliang123 a4c737b354 feat(client): rebuild the task board as the family's work board
- 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
2026-10-12 01:29:48 +08:00
bowenliang123 2835230485 feat(client): tighten the network graph cards and drop the corner jump
- 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
2026-10-12 00:38:17 +08:00
bowenliang123 d648edec1e feat(client): give the members board's other agents the member-card form
- 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
2026-10-12 00:37:44 +08:00
bowenliang123 928abdac97 feat(client): rebuild the Fleet tab around one stable three-shape layout
- 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)
2026-10-11 23:35:29 +08:00
GrivnandClaude Opus 5.5 edf6e24b44 fix(client): let the window be dragged by the insight page head (#136)
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>
2026-10-11 21:02:56 +08:00
bowenliang123 293e954fb1 feat(client): lay the Fleet team and task boards out as two columns
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.
2026-10-11 18:07:24 +08:00
bowenliang123 fd782119e8 feat(client): organize the preference card into three titled groups
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.
2026-10-11 16:41:45 +08:00
bowenliang123 d75b539419 feat: system prompt prefix/suffix settings joined into every session
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.
2026-10-11 13:55:14 +08:00
bowenliang123 f0ee3195ac feat: rebuild the Fleet tab as the agent fleet cockpit
Split the Agent network card into graph + cursor-following inspector
(delegation prompt, latest reply, descriptor, owned tasks, comms), add
the members board and the status-grouped task board off the agentTeam
projection with inline dependency chains, a filterable/sortable roster,
and an on-demand comms timeline over relay/mailbox/settled records.

Data rides a new /api/dsh-context/fleet route (one observeSession read
per session through the shared cold-read gate); hover, pin, owner
filter, and task focus are lifted to the view so every panel steers the
same inspector.
2026-10-11 07:52:58 +08:00
bowenliang123 9b0618a7a6 feat(client): gate the Fleet tab behind a preference
The preferences card gains a "Fleet tab" row (Show/Hide, default Show): the tab is
registered or unwound live, and the placement preference still has the final say —
'sidebar' serves no conversation tabs at all. The ninth preference field rides the
same Config schema and settings transport as the rest, including the fail-open read
(a value the plugin cannot read degrades to Show) and the rollback when a write is
refused.

The three on/off rows (entry visibility, the Fleet tab, the duration curve) now
share one row shape and one set of Show/Hide labels instead of a copy each, and the
mount/unmount/subscribe watcher both gates ride is one helper (toggleGate.ts) —
watchInsightPage and watchFleetTab were the same skeleton twice, down to the
registration-handle guard.

One preference now costs several coordinated edits in the settings store, so the
snapshot comparison is a shallow compare over the state's own keys and the refused-
write rollback is one table lookup, not a chain that has to grow with every field.

The stats board's family cell hands its jump to the caller: statsContext no longer
reaches into the tab bar for the Agent Network view. contextView owns the targets —
the Fleet tab when the preferences serve one, else the right Sidebar panel that
keeps the card inline — and drops the cell's button role when it serves neither, so
a hidden tab can never leave a dead button behind.
2026-10-10 20:21:57 +08:00
bowenliang123 ace15cac11 feat(client): move the agent network onto its own Fleet tab
The Agent network card leaves the Context tab's foot for a page of its own —
the Fleet / 编队 tab, right of Context (order 30). The card is unchanged,
but both mounts now read one shared `useContextSession` data path and fold
the current agent's figures with one `agentSelfOf`, so the panel and the tab
cannot disagree; the Fleet page keeps the same cold-start retry note and the
same baseline gate. The tab label key splits into `tab.context` / `tab.fleet`.

The right Sidebar's Context panel keeps the card inline — its column has no
room for a tab of its own — so the center Context tab renders it only for the
sidebar host, and nothing else leaves that tab.

The stats board's Agent Network cell used to scroll to the card further down
the same page; it now activates the Fleet tab (activateContextTab is
generalized to activateViewTab). Its miss arm is unreachable for our own
tabs: the board and the Fleet tab ride the same placement gate.

The bundle smoke's fake registry now records `register()` calls instead of an
inject callback's return value, matching the real registry — one inject may
declare several registrations, which the second tab exposed.
2026-10-10 20:10:55 +08:00
bowenliang123 3aed76b493 feat(client): reveal an injection's item from its events row
An injection row's label is now a link into the Context Browser: hovering
underlines it, and a click selects the step that first carried the item,
opens its category, expands its row, and scrolls the browser card into
view. An injection event's seq IS the seq of the surface node it added
(fold.ts), so the events row and the browser row are the same item; the
node's own `gone` stamp bounds the reveal to the steps before its removal.

The step comes from the existing locateStepOf(), which now sorts the wire
the way the browser's own entry stamps do — a re-ordered log must not move
the reveal past the step that actually carried the item.
2026-10-10 19:28:00 +08:00
bowenliang123 f1505420a5 feat(client): stamp turn/step positions as the compact T1 S9
The events card's position column spelled the same fact out in full
("第 1 轮 · 第 9 步"), crowding the row's meta tail on narrow panes. It now
uses the notation the browser's own rows already carry (browser.stepAt):
T1 S9, T2 S3→4, T50 S8 → T51 S1.

eventAt() is shared, so the request detail's marker chip and the trend
chart's marker tooltip read the compact form too — one notation for one datum.
2026-10-10 19:27:32 +08:00
bowenliang123 667e1a9c71 fix(client): hold every data-dependent lane so the page stops reflowing
Opening the Context tab, and every step landing after it, moved the cards
below: the page grew 343px on a cold open and the trend card jitters by a few
pixels on each step. Seven independent sources, all the same shape — a box
whose height came from its content instead of from its role:

- The brief lane under the chart held a 76px floor but its rows were 24.5px
  bare and 26.5px carrying a chip, so the lane measured 79.5px or 81.5px
  depending on the step. Every row is now a constant 27px.
- The detail header let its metric pills share a wrapped line with the title,
  so its height followed which pill topped each line as the figures changed.
  The pills now claim a full row of their own.
- The file card's empty state replaced the controls and the 320px list with
  a one-line note, a 265px collapse on the first read of a session. The band
  and the list lane now render in every state, and the lane holds the list's
  height.
- The file card's filter band rewrapped as its tallies gained digits; it now
  reserves its folded height, which depends on the card's width alone.
- The context browser's category rows wrapped as the per-step item counts
  changed; they now stay on one line and the name ellipsizes.
- A title carrying only text wrapped when its scope chip changed length
  (longest in English), resizing the card mid-scrub. Text-only titles keep
  one line; titles carrying toggle groups keep the wrap they need.
- The trend card's pending body rendered as a one-line note and grew by its
  whole height when the first step arrived, shoving the page down. It now
  stands in the panel's OWN boxes — same classes, same grid — so the detail
  fills the card instead of growing it.

Measured across 360px–1440px in both locales: every card's height variance
while scrubbing a 40-step session is 0, and the cold-open fold is 5px.
2026-10-10 17:15:37 +08:00
bowenliang123 861ee77d03 refactor(client): spell out the tools that fit, fold the rest into one chip
The stats card's tools row capped the tally at an arbitrary top three, so a
session that used five tools showed three and quietly dropped two, and the
row wrapped to a second line whenever the count grew a digit — which resized
the node, the stats card, and every card below it mid-scrub.

ToolPills now keeps ONE line, like the Input/Output row above it, and folds
whatever does not fit into a trailing "其他 +N". The fit is measured, not
guessed: an off-flow gauge carries every pill and every chip width at its
natural size, and a layout pass picks the widest set that fits alongside the
chip it would actually carry. A pill's width does not depend on whether its
neighbours are on screen, so the gauge answers for the tail the row is not
showing, and the row renders only what fits — nothing is ever clipped or
overlapped. Measuring the live chip instead would feed the pass's own output
back into its input; measuring only the widest chip would cost a pill to
digits the chip never shows.

With no tally — the split generation's head carries none until the detail
lands — the row holds a lone "其他 +0", so the card reads as a figure
settling into place rather than a blank gap filling in. Because the row never
wraps, the node's height depends on the card's width alone and stops
resizing as a session's tool mix changes.
2026-10-10 17:14:54 +08:00
bowenliang123 887615497d feat(client): show the agent cards' work state in the harness's own glyphs
The header popup marks each subagent with a rolling spinner while it
works and a green dot once it is done; the cards had no marker at all,
because their only candidate signal never existed. SessionSummary has
no completion field, so the row read was dead and always false — the
popup takes its answer from subagentTiming.lastTurnCompleted, and so
does a card now. A session that is not a subagent never carries that
projection, so its own folded steps against a stopped session stand in.

The glyph is the harness's own StateDot primitive at one fixed size, so
a card flipping between states never shifts its title.
2026-10-10 12:33:05 +08:00
bowenliang123 cd26173459 style(client): widen the agent card grid
The natural cell pitch was 208px, which left the two-line titles on a
subagent's card hugging the edge. Every card takes the same, larger
pitch — 276px — so the graph stays one grid and no card claims room
its neighbour does not get. A stage narrower than the pitch still
compresses it, so the family never overflows horizontally.
2026-10-10 12:00:41 +08:00
bowenliang123 38fe279213 feat(client): give every agent card a duration and a step count
The card footer carried the run's duration alone, so two things were
missing: a node that never produced timing rendered a footer with no
figure on the left, and the current agent's card showed nothing at all —
the harness only ever writes a subagentTiming projection onto subagent
sessions, so a parent session has no duration to read from the list.

The step count joins the duration, and the current agent's own
whole-step time reaches its card from the tab's timing totals. The
inspector reads the same figure under the same name, and the README
follows.
2026-10-10 11:59:54 +08:00
bowenliang123 32cb738e88 feat(client): rework the agent network as live cards
Each node becomes a card: title over a fixed two-line zone (every card's
rows align), the agent's consumed tokens with an estimated price when the
models.dev book has the rate, a composition bar replacing the donut (free
window is the track), and a footer with the run's duration at the left and
the bar's occupancy figure at the right.

Links are S-curves fanned out of the parent card's foot in the family hue
with joint plugs at both ends; hovering or focusing a card lights its whole
lineage (ancestors + subtree) while the rest of the graph dims. Sibling
order follows the parent's subagentCatalog projection (the harness header
popup's own spawn order) when cached, with the activity heuristic as the
degrade. Narrow panes fold the family into single-column bands instead of
crushing cards, and the stage measures its content box, so the phantom
horizontal scrollbar at exact-fit widths is gone.

The inspector gains the hovered node's composition readout; keyboard focus
drives the same inspector/lineage affordance as hover. Card surfaces step
one layer up with the l2 border (the inspector's own nesting recipe) so the
cards read clearly in both themes.
2026-10-10 11:24:47 +08:00
bowenliang123 ba8d7c7624 refactor(client): unwrap the zero-argument component factories
makeDonut() and makeSliceList() took no arguments, closed over nothing,
and returned a component their callers then held in a per-mount local —
seven call sites paying for a factory that bought no identity stability a
plain module-level export does not already give. Both export the component
directly now; sliceList's inline props type becomes a named interface,
since it was written out twice in the signature.

objectsOf and rpcCallOf lose an export keyword each: whole-tree search finds
no consumer outside the module that defines them.
2026-10-10 02:18:13 +08:00
bowenliang123 9aaa11591a test(client): split the oversized specs into siblings with shared harnesses
browser.spec.ts (2360), trendChart.spec.ts (1629), contextView.spec.ts (1504), and
overview.spec.ts (1065) exceeded the 1000-line limit. Each splits along its existing
describe seams into same-directory siblings, with the fixtures that more than one
sibling needs extracted once into a shared harness.

The move is verbatim: test titles, describe blocks, and every assertion line are
preserved exactly (2061 tests before and after, no duplicate titles, no skips), and
the suite reports the same coverage.
2026-10-10 01:21:15 +08:00
bowenliang123 87a2893190 refactor(comments): clean up the test-suite comments to the guideline
Apply the AGENTS.md comments guideline to every .ts under tests/: remove
what the code already expresses, keep only what it cannot.

Removed: spec-header coverage inventories the describe/test tree already
names; comments restating the next line's assertion or its message; helper
JSDoc whose body already says it; within-file duplicates; five stale
claims (trendChart's "six aggregated turns" against agg.length === 8,
agentGraph's "six categories" against CATS' seven, fold-timing's "cheapest
call LAST" against a loop that feeds it first, fold-surface's unreachable
plain-JSON note, contextView's "two turns" for a one-turn fixture).

Kept and condensed: the projection-freeze class, strict wire/state-schema
hazards, retention and pricing windows, pairing rules, hostile-input
rationale, fixture and expectation-array ground truth, jsdom mechanics,
and issue-numbered regressions.

Comment lines 2604 -> 2014 (-22%); 85 module headers kept, 30 coverage
inventories dropped; every surviving comment line is within the 140-column
limit, with 36 wrapped lines joined onto fuller ones.

No logic, string, identifier, import or directive changed: a comment-strip
gate proves each file's comment-free code is byte-identical to HEAD, and a
line-level proof pairs all 845 changed lines with an unchanged code part.
pnpm run lint && pnpm run test (106 files / 2061 tests, per-file 100%
coverage) && pnpm run build all pass.
2026-10-10 00:50:55 +08:00
bowenliang123 eb8368fb64 refactor(comments): drop two stale test comments
Remove browser.spec.ts's file header (a coverage inventory the describe blocks
already state) and reword trendChart.spec.ts's tooltip note that quoted the zh
UI string, keeping the assertion's intent in English.
2026-10-10 00:27:04 +08:00
bowenliang123 7c87ce2b76 refactor(comments): rewrap test comments onto fuller lines
Merge consecutive short comment lines in tests/** up to the 140-col limit
without touching their text: comment-stripped content, comment text (whitespace
normalised) and line-comment runs are all proven identical per file. This is the
line-count half of the cleanup only; the content pass over tests/** is still
outstanding.
2026-10-10 00:26:42 +08:00
bowenliang123 def268b7bb perf(trend): rise only the newest columns on mount
Every retained bar carried `animate-lc-bar-in`, so the chart's mount animated
each one — the host retains up to maxRequestSteps (1500) requests — and every
animating bar layerizes on its own. Only the columns a pane can reach are ever
seen: the chart mounts anchored to the newest bars, so the rise is now drawn by
the newest RISE_CAP (200) columns, the stagger counted from the window's edge
so those columns still cascade left to right. Bars appended to a live session
keep rising, and a log shorter than the window is unchanged.

Measured on the 218-step session, the per-bar animations cost 17ms of the worst
frame gap and 16ms of blocking on top of the strip fix. With both changes the
open measures 243 animations, a 133ms worst frame gap (was 250ms) and 98ms of
blocking (was 173ms); with every CSS animation disabled it measures the same
133ms, so what remains is plain mount cost. Three tests pin the window in
total, DNA and DNA+delta modes.
2026-10-09 23:42:35 +08:00
bowenliang123 20826dcaf7 perf(timing): sweep the strip's bands on one wrapper, not one animation each
Opening the Context tab on the 218-step session started 1584 CSS animations at
once, 1324 of them the timing card's strip — one `animate-lc-stacked-in` per
band. Tracing the open showed 1582 animationstart / 1578 animationend
dispatches, 343 Layerize events (392ms of layer churn) and a 250ms worst frame
gap where the shell's own tab switch measures 67-83ms. Disabling just the
strip's per-band animations on the same build already cut that open to a 150ms
gap and 109ms of blocking (from 173ms).

The bands now ride one `.lc-tstrip-fill` wrapper carrying the single sweep, so
the strip wipes open on one composited transform over the same track. Packing,
colours, the kind hover link and the tip are untouched; the per-band ripple it
replaces was capped at eight stagger slots, so a packed strip already settled
as a sweep. A regression test pins the single wrapper and the absence of any
per-band animation class.
2026-10-09 23:42:13 +08:00
bowenliang123 5856324fc5 i18n(timing): label the decode-text bucket 模型回复 / Answers
Rename the timing-stats text row from 模型输出/Answer to 模型回复/Answers to match the Answer wording already used by the stats card, and update the affected assertions.
2026-10-09 17:58:32 +08:00
bowenliang123 6051e39180 feat(overview): a custom date range beside the range presets
The Insights page's range group gains a calendar picker: one panel that
takes BOTH ends, in either order. A backwards pick — end before start —
is sorted on the second click, so `orderedDayRange` hands every reader
downstream (the scope window, the ledger-day predicate, the chip that
names it) a pair with `from <= to`; `dayRangeWindow` orders it again at
the seam, so a hand-built pair can never filter the page to nothing.

The trigger IS the scope: one pill carrying the start and the end, fed
the LIVE window — a picked range when one owns the scope, the preset's
own window read as days otherwise, so "last 30 days" prints the thirty
days it covers and the two controls can never disagree. Only a picked
range takes the chip's brand voice and grows the way back, and the panel
keeps no second copy of the two dates: the chip above it is already
showing them, live, while the pick runs.

Underneath, the range model moves from a start-only floor to a two-sided
`RangeWindow`, which closes the top of a picked scope: the ledger-day
predicate gains the ceiling it never had, so a day beyond the range no
longer leaks into the skill card, and both picked days count whole
(midnight through the last millisecond). `days.ts` gains the calendar
arithmetic this needs — a day's epoch bounds, its weekday, month
stepping, and one month laid out as Sunday-first weeks — with the
round-trip key validation its siblings already used.

The panel portals to `<body>` and seats itself off its trigger's rect,
the containment escape the page's hover tips already take: the page is
the `lc-ov` query container, so an in-tree popover would be both
captured and clipped by the one scroller. It measures itself before
paint, clamps inside the viewport, flips above the trigger when it would
fall off the bottom, and retracts on any scroll, resize, outside press,
or Escape.

Closes #128
2026-10-09 16:49:07 +08:00
bowenliang123 d3f2399440 feat(overview): scope the first range option to today, not the last 24h
The Insights page's first range option read "Last 24 hours" while the
ledger it scopes is day-grained, so the rolling window and the day floor
underneath it disagreed: past midnight the 24h window still admitted
yesterday's ledger day whole, and before 1am it admitted only part of it.
"Today" is the local calendar day from midnight — the window the day-
granular heatmap beside it already draws.

`rangeStartOf` opens the window on the local midnight by date-field
arithmetic (the harness's own `setHours(0, 0, 0, 0)` idiom, as in
`startOfLocalDay`), which stays exact across DST-short and DST-long days
where an epoch-ms subtract drifts into the neighbouring day. The harness
helper itself is only reachable through a deep
`@deepseek-ai/dsh-client-ui-chat/src/*` path — a package this plugin does
not depend on — so the idiom is mirrored at its one call site instead of
taking the dependency.

Both locales carry the shorter 今天 / Today; the deprecated
`ov.range.24h` key is gone rather than aliased.

The specs that anchored time on raw epoch offsets silently assumed UTC
(`11 * 86_400_000`), and the panel fixture at `NOW - 1h` would have
failed whenever the suite runs in the first hour after midnight; every
boundary is now anchored on a local `Date`, so the day floor is proved
identically in every timezone.
2026-10-09 15:06:22 +08:00
bowenliang123 00df001055 feat(overview): filter the skill card by name
A case-insensitive substring filter rides the skill card's totals row,
narrowing the rows, the summary counts, and the overflow line; the
input stays mounted on an empty match, next to a zeroed summary and a
dedicated no-match note. The bars keep the scope's heaviest tally as
their denominator so they hold still while the filter narrows.

A pin the filter hides now falls back to the detached detail block with
its head row — previously the head rendered only for pins outside the
scope, leaving a filter-hidden (or render-bound-hidden) pin unnamed.
2026-10-09 10:53:45 +08:00
bowenliang123 48302d1962 feat(overview): the activity column's skill-load card
The Context Insights page's second insight card lists every skill loaded
within the page's scope (the range floor, or the heatmap's pinned day):
a color-dotted row per name with its load tally, a proportional share
bar scaled to the heaviest row, and the last load's relative time,
ordered by the title's loads / last-used toggle. A row click pins the
session list to that skill's loaders; the pinned row wraps with its
detail into one ringed unit — the description first, then a fact grid
(origin, usage, exact last load, the full path as a click-to-copy
value). A structured hover card answers the same in a floating register.
The catalog read joins description, origin, and path onto the tallies by
name and degrades to bare tallies when the route cannot serve.
2026-10-09 00:57:42 +08:00
bowenliang123 540e067f99 refactor(i18n): name the settings entry "Plugin Settings"
The bare "Settings" / "设置" label read as the shell's own settings next
to the plugin's; both the page-heading chip and the plugin-info row now
name the plugin's own page.
2026-10-08 18:16:37 +08:00
bowenliang123 4f5b266ba8 refactor(overview): move the plugin settings entry into the page heading
The Context Insights page's preferences entry sat as a full-width row
under the activity heatmap, at the bottom of the insight column and easy
to miss. It now rides the page heading as a quiet chip directly right of
the title, cut like the balance capsule beside it, with the long
"open plugin settings" hint carried on the hover title.
2026-10-08 18:16:11 +08:00
bowenliang123 d4d5f52734 feat(stats): figure cells jump into the context browser
The session node's second-row cells (Skill Loads / Answers) become
keyboard-accessible jumps (role=button + Enter/Space, the team card's
idiom): clicking scrolls the Context Browser card into view and opens
the matching section on the live surface — skill loads expand Skill
Injections with the row lens cleared; answers expand Assistant Messages
with the Answer kind chip picked, so tool-call-only steps filter out.
Hover/focus carry the team's affordance (soft wash, brightened label,
brand ring).

The browser gains a one-shot `catFocus` prop mirroring `nodeFocus`:
select live, open the named category, pick the optional kind chip,
clear the lens, report handled so the same cell can fire again. The
cells stay inert where no browser mounts (no handler, no affordance);
tooltips name the destination in both locales.

Verified end to end: lint:fix + test (100% per-file coverage) + build.
2026-10-08 16:22:41 +08:00
bowenliang123 16e3a2083e feat(stats): second session-node row — skill loads & answers figures
The Current Session node now reads two figure rows: turns/steps above
skill loads/answers, each row's cells splitting the width evenly and
centering (one column pair whatever a label's length).

- Skill Loads (counts.skills): the distinct skill names among the
  retained skill-tagged inject events — `/name` invocations and
  `skill`-tool loads; the `<available_skills>` catalog digest rides an
  untagged inject event and never counts. Derived at view time, so no
  stateVersion bump: cached rows recompute the exact tally on serve.
- Answers (state.answers, head scalar): one per assistant message
  carrying a non-blank text block — tool-call-only steps are work, not
  replies. A running total over the complete log (the humanInputs
  precedent), so the timeline stateVersion bumps 26 → 27 and the v20
  warm-up rebuilds idle sessions' rows on the first dashboard open.
- The subagent count leaves the session node (the Agent Team ledger
  row still carries it); docs/compatibility.md records the bump.

Verified end to end: lint:fix + test (100% per-file coverage) + build.
2026-10-08 16:20:54 +08:00
bowenliang123 4199d9ea2d style(trend): quiet the duration curve — tertiary gray with a knockout halo
The overlay rode the brand token (near-black in light, near-white in
dark), making the secondary curve the loudest element on the chart and
colliding with the bar hover outline and the DNA highlight slices that
share the token. It now paints in label-tertiary gray — the right-hand
axis labels' own family — at a thinner 1.25px, over a 3px card-bg
underlay that knocks the bar colors out behind the line so it lifts
clear instead of fighting them.
2026-10-08 11:37:09 +08:00
bowenliang123 f6b59f3041 feat(browser): predict the pending step stamp for the live tail
Rows the log holds no request for yet — the newest assistant reply, the
tool results still waiting to be sent — now wear a PENDING step stamp
instead of none. The prediction follows the harness's step loop
(agent.ts): a newest user message opens a new turn, a text-only final
reply means the turn ended, and pending tool results or an in-flight
injection continue the same turn, so the next step is lastStep + 1.
dsh names that step at step/start before the request exists — the
session log's own tail confirms the prediction (step/start Turn 1 Step
42 over exactly the rows that lacked a stamp).

The estimate reads like the confirmed stamp but with a quiet dotted
underline and a 'not yet sent' tooltip in both locales; the confirmed
stamp replaces it the moment the request logs. Past-step views never
speculate: they reconstruct only what their request already carried.
2026-10-08 09:06:07 +08:00