Commit Graph
105 Commits
Author SHA1 Message Date
Bowen Liang 8bfcf65d31 feat(client): count whole-session human inputs on the stats board 2026-09-12 01:24:46 +08:00
Bowen Liang 8d2ddeff55 feat(client): move the context-event tallies onto the events card's kind filters
The Context Stats card's inject/compaction/prune cells duplicated what the
events card already tracks. Drop the three cells (the grid falls to five:
turns, steps, tool calls, cache hit, cost) and show each tally inside the
matching kind-filter button instead, sourced from the same precomputed
counts the stats card used.
2026-09-12 00:35:30 +08:00
Bowen Liang b68a6d50ad fix(client): cite the official model IDs in the cost tooltip
The rates themselves check out against the official pricing pages (both
currencies, both families, and the Beijing peak windows), but the flash
row's label named deepseek-v4.1-flash — the model VERSION, not an API ID.
The page's current flash ID is deepseek-flash, with deepseek-v4-flash as
the accepted legacy ID, so the label and the tip's applies-to list now
cite those; the tip also drops the colloquial hardcoded phrasing for a
plain for-reference-only note.
2026-09-12 00:28:36 +08:00
Bowen Liang 02ae427c8e style(client): rename the zh answer-decode label to 模型输出 2026-09-12 00:13:02 +08:00
Bowen Liang 3ed80aa9e0 feat(client): the Context card swaps its image count for the chat line's cache hit
The images cell said little about the session; the eighth cell now shows
the whole-session prompt-side cache-hit rate off the official tokenUsage
projection — the same source and formula as the chat stats line under the
composer, truncated to one decimal (cacheHitPercent gains a precision
parameter; the two-decimal default keeps the request detail's figure).
2026-09-12 00:05:45 +08:00
Bowen Liang 423e9423eb refactor(client): split the Token card by composition around the chat-line total
The donut center now carries the whole-session billed total — the same
figure the harness chat stats line shows under the composer (uncached
input + cache read + cache write + output off the official tokenUsage
projection) — instead of the cache-hit share.

The slices answer WHAT the tokens are rather than how the provider
cached them: the six composition categories share the provider-reported
prompt-side total by the composition card's own estimated ratios (their
counts carry the ≈ marker), and the provider-exact output closes the
ring, so the parts equal the chat line's figure by construction.

The cache-read/cache-write/uncached bucket view and its four locale
keys are gone; tokens.total is new.
2026-09-11 23:26:36 +08:00
Bowen Liang 296324cc70 fix(client): remount trend bars on granularity switch so the entrance rise replays
A turn aggregate IS its last step's record (the same seq), so with a
plain seq key a step ↔ turn switch reused those bars' DOM nodes — the
entrance animation had already finished on them and CSS never replays an
animation on a reused node, leaving every turn's last step bar popping
in unanimated. Keying ChartBar by seq + granularity forces a remount;
mode (Total/Delta) switches already remount through the stack ↔ arms
structural change.
2026-09-11 22:14:54 +08:00
Bowen Liang d5dc784292 feat(client): ease-in entrance animations for the context charts
The donut slices sweep in clockwise from 12 o'clock, the composition
stacked bar grows open segment by segment, and the trend chart bars rise
from their baseline — all staggered in layout order, transform-only (no
layout thrash), running once per mount and disabled under
prefers-reduced-motion (the harness's own idiom).

- the stagger caps (trend bars 20, stacked pieces 8) so long logs and
  long DNA band lists still settle in well under a second
- the auto-compaction reserve band stays unanimated: as an overlay its
  growth would smear the stripes, and it reads as chrome, not data
- covers the tab, the /context modal and the right Sidebar, which all
  render the same chart components
2026-09-11 22:13:56 +08:00
Bowen Liang 08def011a0 fix(client): price deepseek-v4-pro at its own V4 Pro list rates
DeepSeek has ended the routing window: deepseek-v4-pro (DeepSeek-V4-Pro-0813)
now bills at its own published table instead of riding the V4.1 Flash triple —
USD peak hit $0.044 / miss $1.32 / out $3.96, CNY peak ¥0.3 / ¥9 / ¥27,
off-peak half price. Flash rates are unchanged.
2026-09-11 21:54:21 +08:00
bowenliang123 97ae8ff2ee feat(client): preview File Activity files in the right Sidebar
Clicking a file name in the File Activity card now opens the file's preview
tab in the right Sidebar — the shipped document-preview viewer, reached the
same way the built-in Files sidebar reaches it — on harnesses that serve that
column (dsh 0.1.5-rc.1+). The address is built with the harness's own
browser-safe `fileAddressFor`, inlined by the client bundle exactly as the
Sidebar's file types inline it, so a path under the workspace collapses to the
session-relative spelling and every route to one file settles on one tab.

The capability is OPTIONAL by contract, like the tab registration itself: the
opener reads `ctx.sidebarRight.openResource` through the runtime boundary and
re-proves it per call, and reports whether the column took the address. An
absent column, a face that refuses the address, or an unencodable log path
falls through to the pre-existing system-open behavior, so an older line is
fully functional and nothing becomes an inert click. Pattern rows and
directory targets are never previewable.

Adds the `@deepseek-ai/dsh-util-workspace-path` dev dependency (itself
browser-safe), allows it in the client bundle purity gate, pins the
resource-navigation seam in the compat matrix, and covers the address builder,
the card's preview-then-fallback click, and the optional opener per baseline.
2026-09-11 18:45:28 +08:00
bowenliang123 be3f598959 style(client): lead the placement menu with 全部 and shorten the tab option to 标签页 / Tab 2026-09-11 14:06:55 +08:00
bowenliang123 5322d1e6d4 fix(client): fail the placement open to 全部 on unreadable or unpersisted config
A config problem must never leave a Context entry hidden:

- a scope value the plugin cannot understand (present but invalid)
  degrades the placement to `all` instead of stranding the previously
  chosen one — the read path now distinguishes invalid from missing,
  and only a genuinely absent field (older Host half) keeps the
  current state;
- a placement write that fails to persist no longer keeps the
  optimistic echo in-session: unless the scope's truth itself carries
  a valid placement, the rollback degrades to `all` (other fields
  keep the existing keep-the-echo behavior).
2026-09-11 14:02:35 +08:00
bowenliang123 a4add07664 feat: add a placement preference (tab / sidebar / both) for the Context view
Closes #58: the Context panel hung off two registrations at once (the
conversation tab and the right-Sidebar guide entry), with no way to keep
just one. A new per-user `defaultPlacement` preference — the first row of
the Plugin configuration card — picks who carries the view: the
conversation tab, the right Sidebar, or both (the default, so existing
deployments see no change).

- shared: `DefaultPlacement` vocabulary (`all` | `tab` | `sidebar`) on the
  settings namespace; the Host schema gains a loose `defaultPlacement`
  field so a stale persisted value degrades to `all`
- client: `placement.ts` mounts each registration through its own factory
  and, on a preference flip, disposes exactly what the new value drops
  (the harness's own dynamic-mount idiom); `watchSidebarContextTab`
  returns its deferred-inject disposer so the sidebar mount can be taken
  down and put back; the settings card leads with the placement row
2026-09-11 13:44:21 +08:00
Bowen Liang 189609874b style(client): shorten the actual label and right-align the estimate pair 2026-09-11 01:03:19 +08:00
Bowen Liang 61bb138ddc feat(client): pair the live surface's estimate with the freshest actual prompt 2026-09-11 01:03:01 +08:00
Bowen Liang 8f7f01a9f9 feat(client): drop the preview badge from the browser meta row 2026-09-11 01:02:42 +08:00
Bowen Liang 037a2e80e7 feat(client): part the donut slices on a hairline divider
A ring with a neighbour on each side now gives half a 0.5-unit gap off
both ends of every arc (1 unit ≈ 1% of the circumference, so ~1px on the
96px card ring), letting the card's own background draw the line between
neighbouring colours. A lone painted slice keeps its full ring — a gap
there reads as a nick — and the cut never takes more than a quarter per
end, so a sliver like the 0.2% overhead wedge never vanishes whole.
2026-09-10 20:19:14 +08:00
bowenliang123 8212526d6c feat(client): brand the sidebar Context tab with the document emblem
Bundle the colourful document sheet as the plugin's own icon and fill the
right-Sidebar tab type's two glyph seats with it, the way the shipped files
type does: the guide capsule's `icon`, and a new `sidebar.right.pane.tab.title`
registration that draws the emblem beside the chip label.

The chip's close control sits out of flow over the title's last 14px and an
active chip fades the 16px before it, so the shipped files chip stays readable
only by its short label. The label now carries a trailing gutter so that fade
lands past the text instead of on the last glyphs.

Registration turns rollback-safe (each disposer is tracked) so a partial
failure unwinds cleanly, and the obsolete primitives-icon resilience spec goes
with the icon read it guarded.
2026-09-10 17:32:40 +08:00
bowenliang123 1a8163a752 refactor(client): follow the 0.1.5-rc.1 guide-entry contract
`SidebarRightGuideEntry` carries `order`, `title`, and an OPTIONAL
`description` again at dsh 0.1.5-rc.1 — it was required at 0.1.5-alpha.1,
dropped at 0.1.5-alpha.2, and restored optional now. The plugin's guide
capsule re-adds its `description` thunk and the `sidebar.guideDescription`
dictionary keys, so the right Sidebar's guide entry renders the one-line
subtitle the shipped Files type does.

The mirrored face marks the thunk optional: a line whose guide body ignores
it simply goes without, so the same contribution stays valid across the
whole 0.1.5 line.
2026-09-10 11:37:21 +08:00
bowenliang123 489864d63a feat(cost): price the deepseek-flash model name at the Flash rates
The family matcher required a `v4` marker, so `deepseek-flash` requests
went unpriced. Accept `deepseek` as an alternative marker — proxy
spellings like `deepseek/deepseek-flash` land too, while foreign
flash-named models (gemini-2.0-flash) still match no family. The stats
board's price-list label and both locales' cost tooltip name the new
model beside deepseek-v4.1-flash.
2026-09-10 11:22:42 +08:00
bowenliang123 d5b4247654 feat(client): copy a rich section's raw text from the browser
Every rich detail section (system prompt, tool schema descriptions,
messages, tool results, compaction summaries, and the same cards inside
the /context modal) gains an icon-only copy control beside the Raw/MD
switch. It copies the section's exact SOURCE text, never the rendered
markdown, via the harness's own writeClipboard primitive; success flips
the glyph to a check for a beat, a rejected write claims no success, and
a repeat click inside the window is a no-op — the shipped code-block
control's behavior. Both icons and the writer are platform-module
exports present on all three supported baselines.

The added control tipped the head's trailing group out of the clipped
section card at the narrowest pane, so rich-text heads now fold that
group onto a second line under width pressure. The wrap is scoped to
those heads: the call-name head must stay one line (long MCP names
ellipsize instead).
2026-09-10 11:12:13 +08:00
Bowen Liang b857d56db3 fix(client): drop zero-time slices from the timing legend
A slice that never happened still rendered as a row — "0.0%" plus a count
borrowed from elsewhere on the card — which reads as "this ran 323 times in
no time". The ring has always skipped zero arcs; now the legend matches it,
so the card shrinks to the slices that actually carry time.

Zero slices are common: across 307 local sessions, reasoning is zero in
23.5%, answer text in 14.3%, tool args in 13.7%, and tools in 14.3% (a
one-turn "hi" session collapses from six rows to two). Overhead is the
residual and effectively never zero, so the change bites the decode and
tool rows. Every slice is a share of the wall total, so dropping rows never
distorts the remaining percentages.
2026-09-10 02:23:23 +08:00
Bowen Liang e0532936fe feat(client): render the generation split and stop borrowing call counts
The Timing card's model slice expands into Thinking / Answer / Tool args
whenever the host served the split; an older cached row (or a log whose
stream carried no block markers) keeps the single LLM Gen row, so nothing
degrades. The ring shares the generation window across the buckets in card
order, clamped exactly as tools already were.

The decode rows carry no call-count qualifier: they count decode BLOCKS,
and one model call emits zero to many of them (a single call routinely
requests several tools; most calls emit no reasoning at all), so the
session's call count was a false per-slice tally. Measured on a real
236-call session: 236 calls produced 302 tool-call blocks, and 1078 of
3037 calls emitted no reasoning. TTFT and tools keep their counts — those
happen exactly once per call/run.

`timingOf` narrows the new optional scalars without materializing absent
ones, and the fast path applies the same non-negative gate as the slow
path so a hostile bucket cannot slip through.
2026-09-10 02:22:43 +08:00
Bowen Liang 6bdd6bdf59 fix(client): give the sidebar Context panel the tab's main-row layout
The right Sidebar's Context panel split its three main cards into three
columns, so a pane too narrow for three folded into a ragged 2+1: a hole
under the composition card and the trend card alone on its own row. The
conversation tab read differently, from the same component.

Drop the per-host arrangement: both hosts now use one two-column row
(composition over trend in the left column, the browser beside it and
stretched to the pair's height). The panel's row simply folds to a single
column at the shared 360px floor instead of splitting across two rows.
The head row is untouched, so the panel still shows its two donut cards.
2026-09-10 01:37:25 +08:00
Bowen Liang 47c470043f feat(client): render timing durations in compact locale-free units 2026-09-10 00:24:05 +08:00
Bowen Liang 50e89301cd fix(client): reprice the session-cost estimate for the 2026-09-10 Flash rates
DeepSeek reprices the Flash series effective 2026-09-10 12:00 BJT
(off-peak per 1M tokens: hit ¥0.02/$0.003, miss ¥1/$0.15, out ¥4/$0.6;
peak at 2x). Between the V4.1 Flash launch and the V4.1 Pro release
every Pro request is routed to V4.1 Flash and billed at the Flash
rates, so pro shares the flash triple until V4.1 Pro is priced — swap
that one reference when it ships. The stats-board tooltip names the
flash row deepseek-v4.1-flash, and both locales note the interim
routing. Fold logic is untouched: the substring family match already
lands v4.1 spellings (new fixture), and the peak windows are
unchanged. Logs spanning the cutover estimate entirely at the new
table — the cell is a reference estimate by contract.
2026-09-09 23:41:31 +08:00
Bowen Liang 5f2498e973 refactor(client): follow the 0.1.5-alpha.2 guide-entry contract
SidebarRightGuideEntry now carries only `order`, `title`, and `icon`,
and the guide page renders a glyph + title capsule, so the plugin's
contribution drops its description thunk and the now-unused
`sidebar.guideDescription` dictionary keys. The mirrored face and the
sidebar's version labels state the supported 0.1.5-alpha.2+ floor.
2026-09-09 23:03:25 +08:00
Bowen Liang 8978db50c9 feat(client): put the Context browser before the trend in the sidebar panel 2026-09-09 02:47:24 +08:00
Bowen Liang b1c0f46d3c feat(client): drop two head cards from the sidebar Context panel 2026-09-09 02:45:50 +08:00
Bowen Liang 52c3b0f824 fix(client): refuse a partially numeric systems list on the fast path 2026-09-09 02:43:15 +08:00
Bowen Liang b93b0e24f9 feat(client): add the Context tab to the right Sidebar 2026-09-09 02:43:04 +08:00
Bowen Liang 6819fa7fe6 feat(compat): fold the V2 and V3 session-log generations alongside V0
The supported range (dsh 0.1.2-rc.1+) spans three durable-log generations, and the fold only read the V0 spellings — so on 0.1.5-alpha.1 sessions the system prompt priced 0, TTFT/generation stayed 0, and startSeq/endSeq replacements appended instead of replacing.

Read SHAPES, never a detected version (the probe can be wrong: a healed profile mirror may name another release than the running harness):

- system/message surface nodes feed systemTokens plus a bounded systems list (the harness's "last nonempty surviving system" rule), replacing the V0/V2 request/header.header.system envelope figure; the browser resolves the prompt in force per step and fetches its text from either event shape.
- The first token comes from the embedded assistant stream (assistant/message.data.stream / assistant/attempt.data.stream) when the log carries no V0 assistant/chunk deltas, mirroring dsh-llm's assistantStreamFirstTokenTime.
- Replacement endpoints accept both startSeq/endSeq and start/end.
- tool/ptc-dispatch books nested file ops alongside tool/code-dispatch.

Statistics were differentially verified against the harness's OWN projection values on real V0, V3-native, and V3-migrated logs: system/tools/message tokens, request and turn counts, TTFT, generation, tool time, and every billed cost bucket match exactly.

The new state fields are additive-optional, so stateVersion stays 15: a bump would orphan the key for idle sessions (the #37 regression).
2026-09-09 02:09:38 +08:00
Bowen Liang 7c081433c4 fix(client): remove the chat stats-line jump affordance
The hover-underline/click jump the plugin wired onto the harness chat
stats row conflicts with the reworked stats-line detail view shipping
in dsh 0.1.5. Drop the composer-dock entry, its styles, i18n keys, and
tests wholesale - the stats line renders untouched again.

Closes #53.
2026-09-09 01:19:26 +08:00
Bowen Liang d3227ff3c2 fix(client): stop a chart edge swipe from navigating the browser history
A trackpad swipe past either edge of the trend chart's horizontal scroller
chained into the page and the browser read it as its history gesture, so a
few scrolls in the Context tab could leave the whole app for another page in
the tab's history (back at the left edge, forward at the right).

Two layers, matching what the harness already does for its own horizontal
scrollers: `overscroll-behavior-x: contain` on the chart scroller and the
agent graph's stage stops the chaining (Chromium/Firefox), and overscroll.ts
cancels the horizontal-dominant wheel the scroller cannot consume at either
boundary for WebKit, which still navigates on horizontal overscroll with the
property set (WebKit bug 240183). Vertical-dominant gestures are untouched,
so a wheel over the chart still scrolls the page.
2026-09-08 19:55:40 +08:00
Bowen Liang c3ffbed94c feat(client): rescale the Context Trend bars to the visible range
A title-row Adaptive switch redraws the chart against the bars currently
visible in the scroller instead of the whole retained log: the axis follows
the scroll and pane resizes, so the first step's system prompt + tool schemas
(a spike far off screen) no longer flattens every bar the reader is looking
at. Per-bar totals scale total mode, the diverging arms scale delta mode, and
the window reads the exact 14px column + 2px gap grid, so a column parked in
the gap never joins it.

The switch reuses the Context Browser's DNA-toggle chrome and stays
mount-local (no settings write); off keeps the whole-log scale. A window
carrying no figure of its own — an empty history, a zero-width pane, or an
all-zero delta window — falls back to that scale, and unchanged maxima
re-render nothing.
2026-09-08 19:49:10 +08:00
Bowen Liang 14ecf44ce4 refactor(client): drop the Context Trend card's subtitle hint
The "✂ marks compaction/prune, Step/Turn switches granularity" caption
restated controls that are already visible in the card's own title row,
so it only crowded the header. The subtitle span now renders solely for
the transient focus state ("Showing {cat} only"), and the unused
trend.hint key leaves both dictionaries.
2026-09-08 19:35:41 +08:00
Bowen Liang 40a054b99b fix(client): guard the DNA bar against a stale hover key
A snapshot push can drop the band under a resting pointer (compaction,
tail slide out of the served window) without a mouseleave, and the dead
key exact-matched nothing: the whole strip sat dimmed with nothing lit
— the very state the open-category pin's breakdown gate rules out.
Honor a band hover only while its key names a rendered band, so the
mirror/pin fallbacks resume underneath.

Also correct the hover-link comment: DNA hovers stay on the bar and
never report back to the overview (the link is one-way in DNA mode,
as the existing test already pins).
2026-09-08 05:25:32 +08:00
Bowen Liang 481783de4e feat(client): keep the open category lit in the browser's composition bar
The browser's accordion already focuses the Context trend chart on the
open category; the composition bar now holds the same highlight as a
resting state — category mode lights the category's segment, DNA mode
lights the category's bands as a group (the parts' group key). A
pointer hover overrides the pin and the pin resumes on leave; the pin
drops while a previewed/picked step holds nothing for the category, so
the bar never reads all-dimmed with nothing lit. No tooltip floats for
the pin: category mode mounts no tip slot, and the group-keyed pin
exact-matches no DNA band.
2026-09-08 00:09:55 +08:00
Bowen Liang 568d837f80 feat(client): add a DNA mode to the Context browser's composition bar
A title-row toggle redraws the composition bar as one band per context
item — the system prompt, every tool schema, every surface message — in
prompt order (the header prefix first, then the message flow by seq),
the order the model reads the context and the order items joined it.
The header epoch's own seq is bookkeeping (dsh logs request/header at
dispatch of the first request using it), never an ordering key: a
mid-session refresh must not park the schemas behind the tool results
they describe.

Bands floor at 0.35% of the occupied region so a tiny item stays a
hoverable filament (tooltips still report true shares); hovering names
the item with its time, a mirrored category hover lights the category's
bands as a group, and clicking a band opens its category + element row
below, scrolling it into view.
2026-09-07 23:58:34 +08:00
Bowen Liang d52c43ae18 fix(client): plot raw fold figures in the trend chart so constant categories stay flat
The trend bars multiplied every category by a per-request anchor factor
(provider prompt / heuristic total). That factor drifts with the heuristic's
ratio error as the context composition changes, so categories that never
change — the system prompt, tool schemas — plotted as steadily growing
(1668→1789 on a real log whose fold value is a constant 1851), and the delta
mode invented non-zero changes for them. The same screen already shows the
raw figures everywhere else (step detail, browser, overview), and dsh's own
token-meter breakdown projection serves un-rescaled heuristics per request,
so drop the anchor: segments, axis, tooltip and the focused view all plot the
fold's figures directly. Provider usage stays visible in the step detail's
Actual Prompt / Output / Cache chips.
2026-09-07 22:49:55 +08:00
bowenliang123 3a404b696e fix(client): keep the browser's on-demand fetchers reactive to the history face
A mount could race the declared inject (a watch rebuild remounts the
Context tab and the /context modal before the injected fiber re-fires),
and the fetchers snapshot the absent face once — the browser then stuck
on the static degradation for its whole lifetime, with no details
renderable in any category. The face now lives behind a
subscribe+useSyncExternalStore seat (useHistoryFace), and both
components rebuild their fetchers when it lands or is revoked, so an
open section auto-fetches without any user action. The /context modal
also gains the session image loader the tab already wires, so
attachment cards stop degrading to metadata-only there.
2026-09-07 15:38:40 +08:00
bowenliang123 bcbbc3367f feat(client): focus the trend chart bars on the browser's open category
Expanding a category in the Context browser now plots every trend bar on
that category alone: the axis rescales to the category's own max (provider
anchor preserved) in both total and delta modes, the hover tip names the
category, and the card subtitle reads "Showing {cat} only" until the
category collapses. The /context modal keeps its unfocused chart.
2026-09-07 13:45:41 +08:00
bowenliang123 1eb10e64e2 refactor(client): drop the T prefix from turn strip labels
The strip label is now the bare turn number (the block's hover title
keeps "T{n}"). Without the prefix, one/two-digit turns fit the 10px
base font at the fixed 16px column pitch with no shrink; the uniform
shrink now kicks in from three-digit turns (7px) and floors at 6px for
four-digit ones.
2026-09-07 12:39:15 +08:00
bowenliang123 47cfff6884 fix(client): shrink turn labels to one strip-wide font size instead of thinning them
At the fixed 16px column pitch even a single-digit "T1" at the 10px base
font grazed its neighbour, so the collision chain hid every other label
(the blank T2/T4 blocks in the turn strip). Keep every label on the
single line: the render pass now picks one font size for the whole
strip — the largest at which the tightest adjacent label pair still
clears the gap (analytic glyph-width estimate, no measurement, 6px
floor) — and the measured chain in updateTurnLabels stays only as the
last-resort guard past the floor.
2026-09-07 12:29:47 +08:00
Bowen Liang 4d144147a0 refactor(client): import React members directly, dropping the central react module
Every client file reached hooks and element types through the central
require seam (src/client/react.ts) and spelled them React.useState /
ReactNS.ReactElement. Import the members directly instead — named value
imports for hooks, inline type-only imports for element types — and let
the bundler own the single require('react'), which is exactly what the
platform module table answers.

This needs JSX off the classic runtime (it required a React identifier
in every tsx scope): tsconfig moves to jsx: react-jsx, so the bundle
sources jsx from 'react/jsx-runtime', already seeded by every supported
harness (verified against the dsh-v0.1.2-rc.1 platform.ts). The bundle
smoke's fake module table gains the specifier to stay a faithful shell
stand-in. h() survives as a local createElement alias at the slot
render-prop seams and the lightbox portal imports createPortal from
react-dom directly.

Verified: the built client.js requires react, react/jsx-runtime, and
react-dom exactly once each; the compat lane (bundle smoke + the
real-baseline matrix) passes against the fresh artifacts; the full gate
stays green with per-file coverage at 100%.
2026-09-07 08:59:03 +08:00
Bowen Liang 2d371dc72c fix(client): refit the system-open affordance to the session RPC seam
The File Activity card's hover-underline + click-to-open affordance died
with the harness connection refactor: the hostDescription.canOpenPath
fact and the connection.api.host.openPath RPC that the capability probe
and opener read no longer exist on any supported harness line, so the
probe answered false everywhere and every file name rendered inert.

Re-point both at the session controller's workspace-opener remotes
(session/canOpenWorkspacePath / session/openWorkspacePath) over the
generic '/api' channel — the same Connection RPC the split timeline's
detail channel rides — with the wire contract proved against a live
host: `args` must be a plain object keyed by the remote's declared
parameter names (an array is rejected host-side), and the capability is
now an RPC round-trip instead of a synchronous host fact, so it lands
in view state behind the harness's own isLoopback gate; a probe that
settles after unmount drops its answer. The shared connection-RPC
resolution is deduped into rpcCallOf, which makeDetailFetcher reuses.
2026-09-06 23:52:51 +08:00
Bowen Liang 6faf8f16bf feat: count the searched target alongside the matched files in the op log
A grep/glob with the complete matched-file meta rowed ONLY per hit file —
the searched path (or the workspace-wide pattern) vanished from the File
Activity card entirely. Both now book: the call's target row first (the
searched path, carrying the pattern(+include) detail; a pathless search
rows the pattern itself, marked as a pattern), then one row per matched
file with its hit count. The degenerate single-file search (the target IS
the matched file) keeps one row. Truncated or malformed metas keep the
honest fallback (the call target only).

The fold's op-log semantics change, so stateVersion bumps to 15 and cached
rows refold.

Verified: 1147 tests, per-file 100% coverage; live on a grep-heavy session
the card counts 80 search ops — the searched directories row alongside
their per-file hits (plus a deterministic fold over the real log: 6 target
rows + 107 hit rows).
2026-09-06 12:03:07 +08:00
Bowen Liang 5c99a476f3 feat: fold the file-operation log host-side (full-log File Activity coverage)
The File Activity card derived its ops client-side from the served tool
nodes joined with the conversation WINDOW: a call whose arguments had aged
out of the loaded tail named no target and vanished from the card silently,
so the "up to latest" scope actually covered only the chat window. The join
was also the only meta/subCalls carrier, and the per-call op assembly was
written twice (top-level and Code-Mode nested paths).

The derivation moves to the host fold, which sees every lifecycle event with
its full payload: `tool/call` arms name + raw arguments (the pending-call
map's entries grow `argsRaw`), `tool/result` books the ops off the armed
arguments plus the result's presentation meta (grep's by-file match groups,
glob's path list, read's exact window), and `tool/code-dispatch` buffers
nested Code-Mode ops under their run_code root until the parent result
flushes them with `parent` + the program description. The log is bounded
(`maxFileOps`, default 400) with a `fileOpsFloor` coverage stamp, rides the
detail payload (and the inline value on channel-less hosts), and is plain
JSON end to end. stateVersion bumps to 14; cached rows refold from the log
to rebuild the op log.

The per-call parsing is ONE shared module (shared/fileOps.ts — opsOfCall
plus the kind/path/delta/read-window/search-attribution primitives),
consumed by the host fold and by the client's inline-generation fallback
(the legacy window derivation stays for old hosts, now through the same
parser, closing the duplicated assembly). The client aggregates + filters
(`activityOfOps` joins the archive's `gone` stamps at render time; nested
ops scope by their parent result's seq, matching the legacy node-level
rule); the card, its chips, rows, deltas, and locate bridge render
unchanged.

Verified: 1142 tests with per-file 100% coverage (new parser matrix,
fold-ops, aggregation, end-to-end through the real registry + detail
endpoint); the compat vocabulary probe gains tool/code-dispatch (present at
the 0.1.2-rc.1 tag). Live on the real deployment: the big session's card
grew from 4 window-bound files to 37 files / 135 ops spanning every turn,
and folding the real Code-Mode logs books nested ops with parent + program
stamps (e.g. 136 nested reads across one session, errors flagged).

Known upstream boundary (unchanged): `tool/code-dispatch` persists no
result meta, so nested reads keep the limit-estimate footprint and nested
searches the call-target form until the harness carries meta there.
2026-09-06 09:47:26 +08:00
Bowen Liang 62ae0f9c69 perf: split the context timeline into a slim wire head plus an on-demand detail channel
The contextTimeline projection value rode every harness delivery channel
WHOLE: each session.list row, every control baseline, every follow
snapshot, and a full-value broadcast on every state change (with a
full-value schema parse per push). The heavy collections (request records,
context events, the served surface window, the removed-node archive) are
only ever rendered by an OPEN Context tab/modal, so they moved off the
wire value.

Wire head (buildTimelineHead, ~1KB): the composition breakdown, the stats
board's precomputed counters (turns/steps/injects/compactions/prunes), the
newest request's billing summary (the headline's derived anchor), the
timing/cost totals, and `detailRev` — a revision marker that bumps exactly
on detail-mutating folds, the open tab's refetch signal.

Detail channel (host/detail.ts): the pre-v0.9 generic Connection RPC shape
returns as one narrow endpoint, `/dsh-context` `detail`, serving the
collections off the registry's live fold state (`stateOf` — no second
fold, no private copy). A nested `ctx.inject(['connection', 'sessions'])`
arms it load-order-free; a deployment whose faces never compose keeps the
inline wire value unchanged. The baseline-gate fallback path is untouched.

Client (client/timelineSource.ts): one per-session detail store shared by
the tab and the /context modal — fetch on open, then debounced
trailing-edge refetches whenever the pushed detailRev outruns the served
detail (single-flight, latest-wins by revision, refold reset, failure
backoff, absence stops trailing). Detail-driven cards show a pending/retry
note instead of a misleading empty state; the counters and headline render
off the head immediately. Both wire generations are reconciled per value
(new client + old host renders the inline value untouched; old client + new
host degrades to empty charts in the stale-watch window, never a white
screen). The chat-jump relay holds its one-shot pin until the detail lands.

Fold state keeps the full shape: no stateVersion bump, no cached-row
invalidation (the slim view is recomputed per read for cold checkpoints).
Compat matrix gains the detail-channel seam probes (HostConnectionRpc,
the browser caller, the registry stateOf), verified against the
0.1.2-rc.1 tag.

Measured over 263 real session logs from a live deployment, plus live RPC:
- per-session wire value: 84–150KB (top sessions) -> 0.6–0.9KB (~100-165x)
- all sessions aggregate (session.list rows / control baselines):
  4.4MB -> 0.2MB (~22x)
- per-client push traffic over one big session's lifetime:
  157MB -> 1.8MB (~87x), and the per-push full-value zod parse shrinks
  by the same factor
- detail payloads now flow only for the session being viewed, only while
  its tab/modal is open (120KB measured live for the biggest session);
  background sessions cost zero detail traffic

Verified visually against the running harness (big/small/cold sessions,
a live turn refreshing in place, past-step reconstruction, header epoch
and tool schema reads, the /context modal, the Agent network card) with
byte-identical card output. 1110 tests, per-file 100% coverage.
2026-09-06 04:08:20 +08:00
Bowen Liang cb93e4c74c refactor: share the settings vocabulary and dedupe the client fetch/overlay patterns
- The settings preference vocabulary (DefaultGranularity/DefaultTrendMode/
  DefaultFileSort/PluginSettings/SettingsField) was declared in both
  host/settings.ts and client/settings.ts — declared once in
  shared/types.ts now, re-exported at both ends.
- The Context browser's two fetch-on-miss state machines (row content,
  header epoch) were line-identical and had already drifted (the row
  read's idle note said 'no content') — one useFetchOnMiss hook plus one
  fetchMissNote helper (components/fetchOnMiss.tsx).
- The /context modal and the upgrade gate duplicated the capture-phase
  Escape handling; useEscapeClose (components/escapeClose.ts) is the one
  overlay contract, and the gate gains the modal's focus restore.
- The five/four identical useProjection reads in contextView/contextModal
  collapse into services.ts's projectionOf; agentTree drops its private
  asRecord copy for the exported one.
- agentTree's compact duration formatter is renamed fmtDurationCompact —
  same name as format.ts's locale-aware fmtDuration with different
  behavior was an import trap.
- Dead surface removed: Snapshot.occupancy (unfolded since 0.11 and
  undeliverable under the strict wire schema), CAT_COLORS,
  HistoryEventLike, and three exports nothing consumed (EVENT_ICONS,
  ContextViewProps, the FileSort alias).
- Comment drift fixed: the conversation join has a single useChat seat
  since the matrix narrowed to 0.1.2-rc.1+ (three sites), and
  services.ts's header no longer claims the module is type-only.
2026-09-05 22:19:01 +08:00