File Activity only folded top-level tool-result nodes, so PTC sessions
(run_code as the only tool) showed an empty card and the Minimal preset
went unseen. The fold now covers every surface that executes a file op:
- Nested Code-Mode calls: each settled sub-call in the conversation
node's subCalls tree folds as one op, attributed to the nested tool
and located on its parent run_code result (FileOp.parent; the op's
own seq names the dispatch event, which has no surface row). The op
log shows the program's description as the "why". Narrowing is fully
defensive: malformed blocks drop out, and the walk is bounded by a
depth cap plus a visited-set cycle guard.
- str_replace_editor (Minimal preset): view reads; create,
str_replace, and insert write with their own line deltas.
- Searches resolve to real files when the result's presentation meta
carries the complete matched-file list (FileOp.hits shows the count,
include filters join the detail); a capped or malformed meta falls
back to the searched path/pattern as before, and meta attribution
survives an aged-out call head.
Closes#24
The scroller's scrollbar-width: thin made Chromium and Safari drop dsh's
global themed scrollbar skin (any non-auto scrollbar-width/-color silences
::-webkit-scrollbar* rules), falling back to the OS overlay bar that
auto-hides on macOS. Drop the property so the skin takes over; reserve the
bottom padding through its --dsh-scrollbar-width token.
The three rows draw from different slices of the log, and the tag bubbles only named the content, never the window — scrubbing read like the rows did not match up. Each bubble now opens with its window and closes with the one cross-bar fact that explains the half-step offset: inputs are what entered after the previous reply (nothing new on a turn opener), calls made here land in the NEXT bar's In row, and the opener recalls itself on every step of its turn.
Hovering around the trend chart used to change the detail panel's height: the brief section only rendered rows that resolved (the turn opener vanishes outside retention, usage-only replies carry no reply node), and collapsed entirely - margins included - when nothing did.
- the brief lane ALWAYS renders: a fixed three-row min-height, rows appear inside it, unknown steps leave blanks instead of collapsing
- the In row is one of those constants - an empty step shows a muted "(nothing new)" placeholder in place of its chips, and stays inert (a row with no node has nothing to locate)
The 26px gradient overlays at the chart's left/right edges (shown when the history overflows) read as washed-out white patches; drop the overlays, their edges state machine, and the .lc-chart-fade styles. Scroll anchoring, turn-label centering, and the hover tip are untouched.
The absolutely positioned tooltip contributed scrollable overflow inside
.lc-chart-scroll: a wide reply preview hovering a right-edge bar inflated
scrollWidth and toggled the horizontal scrollbar per hover, jumping the
whole trend card. A new positioned .lc-chart-wrap now anchors the edge
fades and the tip beside the scrollable content; syncTip glues the tip to
its bar visible slice via transform (edge-clamped, centered when wider
than the viewport), which also pins the left fade that used to drift
off-screen with the scrolled content.
Header rows (trend toggles, event kind chips, browser picker), the
file-activity filter/meta strips, and brief input chips now wrap under
width pressure; detail rows fall to one column; browser row tags and
tool-schema param columns shrink so long values ellipsize instead of
forcing their row past the card. Desktop-wide rendering is unchanged.
A new Agent Network card at the foot of the Context tab: the whole agent family (current agent, ancestors, subagents at any depth) as a live, fully responsive node map — fused context rings per session, family-colored lineage, halo status language, hover inspector, and click-to-jump into any agent's session. 693 tests, 100% coverage.
Drop the static REGISTRY_URLS array; readVersion(host) now concatenates
the plugin name and `/latest` path per call so the registry list holds
only the differing domain.
Order registries as official npm then npmmirror; each source failure
(non-ok response, missing/non-string version, network error) advances to
the next source, and only an all-source failure resolves to null so the
plugin info card keeps its baked-in version. TTL cache and module-level
promise reuse are unchanged.
The cards re-reconciled on every context-tab render — including pure
hover/select renders that never touched their inputs. FileCard and
RequestDetail are now wrapped in React.memo at the factory level, and
the view hands them reference-stable inputs across those renders:
convOf and the locate handlers are useCallback'd, the step brief and
the file activity fold are useMemo'd (the latter with convOf in its
deps so a conversation-snapshot update can never serve a stale join),
and the hook order stays ahead of the loading early-return (React #310).
Composition-chip hovers and kind-filter toggles now skip both cards
entirely.
Two compounding defects made every incoming event flash the whole card:
- Rows were keyed by seq PLUS reverse-index; the list renders newest-
first, so an appended event lands at index 0 and shifted every
existing row's key, forcing React to destroy and rebuild the entire
list on each push. Rows now key on the durable seq alone, so a push
prepends one row and keeps every other row's DOM node.
- The truncation-title layout effect had no deps: every render (hover
included) re-read scrollWidth/clientWidth for every label — one
forced synchronous layout per row — and rebound the resize listener.
It now resyncs only when the events list changes, with the resize
listener bound once (root resolved inside the callback so the
empty-to-non-empty transition keeps it live).
The chart's layout effect ran after every render (no deps), so any
hover/select re-render near the right edge re-ran the stick-to-end
check — and by the time it read el.scrollWidth the value was already
the post-append width, which could misfire the anchor. The effect now
runs only when granularity, focusTurn, or the request list changes,
and the near-end check compares against the PREVIOUS pass's scrollWidth
(kept in a ref), so a reader near the right edge still follows an
appended bar while a mid-chart reader is never yanked back, and a
hover/select render never touches the scroll at all.
Replace max-height with height on .lc-events/.lc-fa-list so both card
bodies hold a constant 320px instead of growing with row count, keeping
the dashboard layout stable as events and file entries accumulate.
React 18 dev replays a failed render through a fake DOM event; jsdom
reports it as an uncaught window error and vitest forwards it to an
uncaughtException, which no console.error spy can catch. A window error
listener that prevents the default keeps the deliberate throw inside the
test, alongside the spy silencing React's own boundary message.
parseCallArgs returns null for a call that never carried arguments; the
path===null guard already implied args!==null (pathOfArgs yields null for
null args) but did not narrow it, so the delta/detail reads below failed
typecheck. State the args check explicitly.
- Remove the MAX_OPS=8 cap: an expanded file renders its whole operation
log instead of folding older ops behind a static "+n earlier ops" line.
- Remove the unresolved counter and footnote: a file-kind call whose
arguments aged out of the retained window names no target and is now
skipped outright, with totals[kind].ops incremented only after path
resolution — every op the card counts, it also rows.
- Drop the files.unresolved/files.moreOps strings and the dead
.lc-fa-more/.lc-fa-note rules.
The file list now defaults to most-operations-first (ties broken by
recency) and a lc-gran toggle on the meta strip switches to
most-recently-touched order; the sort applies to the filtered copy, so
the fold keeps serving entries latest-first.
Expanded files read as contained layer-2 cards with a hairline frame, the
per-operation log hangs on a tree-guide rail under the chevron, count badges
and op tool chips become pills, and rows get roomier padding with tabular
timestamps.
The bottom-right card no longer lists model-visible messages; it answers
what the agent DID to the user's files up to the picked step. Tool-result
nodes (live + archive) join the conversation snapshot for call arguments
and fold into one row per file with per-purpose counts (read/written/
searched), estimated line deltas from edit/write arguments, multimodal
and directory form glyphs, and failure dots. Header chips double as
purpose/form filters next to a path search box; a row expands into the
file's operation log, and each operation reveals the exact tool result
in the Context browser. The card scrubs live with the trend chart pick
(exclusive next-request bound); ops whose paths aged out of retention
are honestly counted in a footnote.
The chip row lived in CurrentComposition, was fed the fold's per-tool
toolList summary, and the chips focused the Context browser on a tool
via a one-shot toolFocus callback. The browser already exposes the
tool schemas through its headers section, so the chip row was a
duplicate entry point that no longer pulled its weight.
Removes the lc-tools DOM block, the toolFocus/onToolFocusHandled
prop wiring across ContextView/ContextModal/Browser, the
'tools.top'/'tools.more' translations, the .lc-tools* CSS, and the
now-unused toolList summary across the wire + state schemas
(stateVersion 9 -> 10). estimateToolSchema stays because headers.ts
still prices individual tools for the browser's tool-schema cards.
The Context browser joined full content against the chat conversation
window; when a surface node's seq sat outside it, the fallback paged
blindly backwards through session.loadOlder() (up to 20 pages of 50
messages prepended into the shared chat window) and finally dead-ended
on a static note.
sessions.history already supports seq-anchored pages cut on whole
append-origin message boundaries, so ONE call with beforeSeq: seq + 1
returns exactly the page holding the event whenever the durable log
still has it. The browser now:
- fetches on miss with that single anchored read and maps the raw page
into the node shapes it already renders (new client/historyPage.ts:
user messages, assistant blocks, tool results paired with their
in-page call head, compaction checkpoints with their summary);
- merges fetched nodes under the zero-cost window join (join wins);
- reports honest states instead of one dead end: loading, absent from
the session log (compacted or cleared), failed with a retry button;
- drops the sessions.provide contribution and the MAX_AUTO_PAGES
machinery entirely - the chat scrollback is never mutated again.
Hosts without the connection api keep the static preview-plus-hint
degradation.
The source-level suite needs no lib/ artifacts, so PR checks test
first and build afterwards solely to feed scripts/bundle-smoke.mjs;
the release workflow gains the same smoke step between test and
publish.
All 19 root-level specs and the fake-React viewBed helper are
superseded by the source-level suite (606 tests, 100% per-file
coverage). The packaging behaviors they pinned (bundle handoff, CSS
channel, HMR disposal) live on in scripts/bundle-smoke.mjs.
The old suite exercised the built lib/ bundle, so v8 remapped almost
nothing (8% src coverage) and every run needed a build first. The new
suite imports src/ directly under two vitest projects (node for
host/shared, jsdom for client), rendering real React 18 trees against
the real dictionaries and ui-primitives, with faithful in-memory
harness faces (cordis inject/effect, locale chain, slots, sessions
provide) instead of plugin mocks.
- 43 fine-grained, stateless, single-module spec files (606 tests)
forked across workers: ~2.5s wall, no build step
- v8 thresholds at 100/100/100/100 per file over src/**
- host specs drive the real projection definitions (init/apply/view)
plus one full cordis + SessionStore + SessionProjectionRegistry
integration; the framework's snapshotJsonValue pins the
projection-cache precondition on every intermediate fold state
- oxlint's src ruleset now covers tests/** as well
- typecheck covers the tests via tsconfig.tests.json
The session-log divergence check runs under bun against src/ directly,
tracking the working tree instead of the last build. Also add
scripts/bundle-smoke.mjs (pnpm run test:bundle): the one check that
must exercise the built bundle — module-loader handoff, CSS channel
tagging, registration disposal and HMR style-tag semantics in jsdom
with real React.
Every annotation carries its evidence inline (guard dominance chains,
the 8:1 aspect clamp making the wide-solve arm unreachable, a
500k-dimension fuzz with zero hits). browser.tsx also drops two
redundant `?? null` in JSX render positions — undefined and null
render identically.
SlotRegistration.inject is invoked without a session id for root-scope
keyed slots (settings.plugin.item), and slot components receive
harness-injected props beyond sessionId (useProjection, hooks). The
narrower types made the plugin's own registrations fail a strict
typecheck of the test harness's faithful slot registry.