# dsh-v-token-insight Internals > Implementation dossier for plugin developers: registration slots & data sources quick reference, page anchors & style hooks, version scope checklists, and the integration risk log. For the user-facing introduction, see the root [README](../../README.en.md) (简体中文). ## Registration Slots & Data Sources Quick Reference | Need | Slot / Data source | Where | |---|---|---| | Third session tab | `conversation.view` list slot (chat=0 / trajectory=10 / this plugin=20) | ui-conversation / ui-trajectory precedent | | Tail lightweight readout | `conversation.chat.assistant-actions` list slot (owner `{messageId}`; copy → feedback(10) → this chip(20) → branch → timeEnd) | TurnTailNodeView's MessageIconActions | | Sidebar seat entry | `sidebar.footer.action` list slot (root scope, owner `{wide}`; order 20 after the cordis seat, directly above Settings) | ui-sidebar SidebarRoot / ui-cordis seat precedent | | Overview full-page frame | `shell.overlay` frame overlay (root scope; DOM order = stacking order: overview 40 < editor 50) | ui-layout AppFrame | | Overview geometry tracking | `closest("[data-shell-overlay]")` attribute anchoring → AppFrame frame → sidebar column (dsh-012: the `[data-slot="sidebar"]` outlet was removed; attribute anchoring is immune to display:contents wrapper layers) `getBoundingClientRect` + ResizeObserver; `[data-sidebar-collapsed]` as the collapse signal | ui-renderer SlotOutlet / ui-layout AppFrame | | All-session KPIs (tab) | `useProjection("tokenUsage" / "sessionStats" / "contextPressure" / "contextBreakdown")` | dsh-token-meter / dsh-session-stats | | Overview Tier 0 (list rows) | `useSessions` (root standard props) snapshot `byId` rows: `projectionValues / cwd / parentId / origin / running / blank / updatedAt` + `useWorkspaces` snapshot `archivedSessionIds` | runtime refreshList / projectList / WorkspaceManager | | Overview Tier 1 (ledger) | `localStorage: dsh-v-token-insight.ledger.v2.index` + per-session shards (`dsh-v-token-insight.ledger.v2.s.`, compact encoding + 5MB total budget); sedimentation from tab/chip + offline folding via the dsh-012 `ctx.remote` `session.follow` open-frame + `session.page` paging (inter-page yields + RTT backoff) | this plugin's ledger.mjs / pacing.mjs / dsh-012 `ctx.remote` | | Per-turn details | `useChat` (dsh-012 snapshot split): `usage / timing / interrupted / step / messageId` | client-side window (50 messages/page) | | Per-step model/request params | `useTrajectory` (dsh-012 view migration) `requests[]` joined on `(turn, step)` (`message.source` is omitted on legacy nodes) | ui-trajectory TrajectorySnapshotBuilder | | Model catalog (editor) | `ctx.remote.session.modelCatalog()` + local `subagentAddress` pre-check; subagent refusal → manual input | dsh-012 `ctx.remote` typert surface | | Load older | snapshot `hasMore / loadingOlder` + `ctx.get("sessions").binding(id).session.loadOlder()` | SessionFace (provided via entry inject) | | Jump back to turn | DOM click on the first tab (order 0 = chat) + `chat.locations.getTurn(turn)` first key → `[data-chat-anchor-key]` scroll | ui-conversation ChatView | | Drill-down (overview → session) | `ctx.get("sessions").open(id)` / `subagentAddress(id)`+`openSubagent(address)` → DOM click on the "Token" tab | SessionRuntime.select / selectSubagent | | Price entry / editor | Overview "Price Table" tab (`settings.general.item` removed) + `shell.overlay` frame overlay (root scope) | ui-layout AppFrame | ## Page Anchors & Style Hooks (for theming proposals) All four surfaces carry stable `data-plugin-anchor` anchors; the existing `tsn-*` class names are equally stable hooks — scope theming proposals by anchor, locate inner structure by class name, without breaking any behavioral contract: | Surface | Anchor (`[data-plugin-anchor=…]`) | Root node | Main inner structure (stable class names) | |---|---|---|---| | Sidebar seat "Token Stats" | `token-insight:seat` | `.tsn-seat-row` (expanded) / `.tsn-seat-rail` (56px rail) | `.tsn-seat-label`; button chrome mirrors the official Settings seat value-for-value | | Session Token Stats (per-session tab) | `token-insight:session-view` | `.tsn-root` > `.tsn-inner` | `.tsn-head` (title row + `.tsn-pill` coverage capsule [N/M turns + mini progress]), `.tsn-kpis` (billing main cards) + `.tsn-kpis--compact` (TTFT/speed/cost secondary strip), `.tsn-card` (context card, `data-level="warn/error"` pressure grading), `.tsn-charts` (`.tsn-span2` per-turn stacked spans two columns), `.tsn-table` (details: `.tsn-sort` sortable headers, `.tsn-num` right-aligned numbers + `.tsn-databar` token bars, `.tsn-model` model badges, gold `.tsn-money` costs), `.tsn-step-row` (expanded per-step details), `.tsn-load`, `.tsn-empty` (dashed empty state) | | Token Stats Overview (cross-session page) | `token-insight:overview` | `.tsn-dash-frame` | `.tsn-dash-head` (title bar), `.tsn-dash-kpis` (KPI row; hit-rate/session cards with `.tsn-kpi-meter` proportional fill, gold cost card), `.tsn-dash-tabs` (dimension tabs, active brand underline), `.tsn-dash-grid` (6-column chart card grid) > `Card(.tsn-card)`/`ChartCard` (`.tsn-dash-grid-span` full width), `.tsn-info-bar` (dual-caliber info bar), `.tsn-seg` (filter segmented control) + `.tsn-searchbox` (search icon), `.tsn-table` (`.tsn-table-scroll` inner scroll + sticky header, `.tsn-sort` click-to-sort, zebra rows, `.tsn-indent` subagent indent guides, `.tsn-drill` hover drill arrow, `.tsn-databar` token bars), `.tsn-hbars`, `.tsn-degraded-note` (warn banner) | | Price table editor modal | `token-insight:price-editor` | `.tsn-modal-root` > `.tsn-modal` | `.tsn-modal-head/.tsn-modal-body/.tsn-modal-foot`, `.tsn-entry-list/.tsn-form-grid/.tsn-tier-row` | Chart-side extra hooks (shared by both pages): `.tsn-anno` (cumulative annotation at the cost line's end point), SVG defs `id="tsn-grad-cost"` (fading area gradient, deterministic naming); stacked bars `rx=2` rounded corners + 1px segment gaps, dashed gridlines (integer Y ticks), `:has` hover linking (degrades to single-bar hover when unsupported). Naming convention: **seat + overview page = the cross-session "Token Stats" surface; in-session tab = the per-session "Session Token Stats" surface** — theming for the two can be scoped by their respective anchors without cross-talk. ## Version Scope Checklists ### v0.3 - [x] Shared design language (both pages): three-tier type scale (KPI 20/600 large numerals → charts → tables 12/20), card layer color `bg-layer-1` + border `border-l2→l3` hover, 150–200ms quiet motion + brand focus ring, full `prefers-reduced-motion` coverage, icons all SVG (no text glyphs ✕/▸▾) - [x] Session Token Stats tab: KPI primary/secondary split (four billing cards with large numerals + `tsn-kpis--compact` secondary strip), coverage capsule (N/M turns + mini progress, honest window-only caliber), context pressure color grading (70%/90%, coloring only the percentage text and meter border), chart area 1+2 relayout, details table row spacing/right alignment/model badges/expanded-row layer color + brand rule + arrow rotation, dashed empty-state card - [x] Token Stats Overview: hit-rate/session-count KPIs with proportional fill bars (`tsn-kpi-meter`, segment width = share, aria-hidden), 6-column chart grid relayout (trend/cost span full width, no holes for missing cards), active brand tab underline, per-session table segmented filter + details inner scroll area (sticky header) + subagent indent guides + hover drill arrow + dot status badges, dual-caliber footnotes merged into an info bar, unified warn tint banner styling - [x] Chart visual spec: dashed gridlines, stacked bars rx=2 + 1px segment gaps + `:has` hover linking (progressive enhancement), cost line gradient area (`tsn-grad-cost`) + end-point cumulative annotation, sparse turn ticks (one per ⌈n/8⌉), integer Y ticks (`niceScale`: 1/2/5×10^k steps, yMax rounded to a step multiple) - [x] Implementation-feedback revisions (user walkthrough, 7 items): two-tone zebra table rows (interactive hover/active tiers); overview default tab = Overview; token value columns get share bars (`.tsn-databar`, bucket semantic color/brand) + bold numerals; click-to-sort headers on both tables (`.tsn-sort` + aria-sort, missing values always last); KPI mini rings/dots replaced with proportional fill bars; gold bold amounts (light amber-600 / dark amber-400) + bold tokens - [x] Defect fixes: context breakdown "Tools" segment hardcoded purple → `--dsw-static-deepseek-400` (theme verified to have no purple tokens); added missing `.tsn-dash-grid-span` rule; segment fills migrated to style-applied var tokens; shadow rgba fallback removed (zero hardcoded colors) - [x] 11px footnote contrast in light theme (tertiary ≈3.7:1 < 4.5:1) → caliber footnotes/numbered sub-notes promoted to secondary; all unit tests green (baseline preserved + 9 new chart cases) ### v0.2 - [x] Plugin skeleton, registration slots, projection/window data reads, per-step pricing pure functions (with currency guards) - [x] KPI cards (incl. TTFT mean / tokens·s) + context pressure card (ContextMeter visual language) - [x] Per-turn details + expanded per-step call details (model/routing, four buckets, duration/first token, request params, no-usage annotation) - [x] loadOlder wiring (bottom-reach + manual) and window coverage notice - [x] Detail row click jumps back to the corresponding conversation turn - [x] Price table editor modal (entries/tiers/time windows/discounts, localStorage persistence, JSON import/export, live preview) - [x] Charts: four-bucket stacked bars / model horizontal bars / cumulative cost line (each with a table alternative, colors all via official tokens) - [x] pricing / stats-fold / price-store / rows-aggregate / ledger unit tests (`pnpm test`) - [x] Model info join (trajectory requests → per-step provider/model/requestConfig) + editor model catalog dropdown (one-click fill of current model, subagent fallback to manual input) - [x] Sidebar seat "Token Stats" (`sidebar.footer.action`, wide dual form) + overview full-page frame (`shell.overlay`, sidebar avoidance + ResizeObserver tracking + degraded full cover) - [x] All-session KPIs (Tier 0 list-row projection aggregation: archived included / blank excluded / missing-projection degradation) and dimension tabs (Overview / By Workspace / By Session / By Time / By Model / Price Table) - [x] Archived visibility (badges + filters + explicit data-plane-missing notice) and detail row drill-down (sessions.open → auto-switch to the Token Stats tab, in-page ledger expansion fallback) - [x] Local per-step ledger (idempotent sedimentation / session.history deep backfill [budget + coverage interval] / discount-window-hit aging buckets / orphan GC / corruption fallback / quota fallback / manual clear) - [x] Shared stat card kit `stats-ui.jsx` (Card / KpiCard / ChartCard / StatBadge / caliber footnote), shared by overview page and session tab - [x] Price table entry moved into the overview page (`settings.general.item` registration removed, inject baseline `+ui-sidebar / −ui-settings(-general)`) ## Integration Risk Log (Live Verification Status) 1. **turnTail chain / tab-entry standard props — verified at the framework layer**: `standardProps()` (dsh-client-ui-renderer client.js L533-567) assembles useSession/useProjection/t uniformly for all session-scoped entries; chain entries go through SlotOutlet (L314-318) via the same path. ✅ Verified; **pending live verification**: projection-key availability on addressed-subagent tabs (if absent, KPIs show "—" — capability-absence degradation, no error). - **Chain entries must declare `select` (found during integration, verified)**: chain-slot election calls `entry.select(ownerProps)` per entry (ui-renderer client.js L806-811); when undeclared the call throws TypeError → treated as "abstain" with console.error noise, and the entry never renders. - **Chain election is first-non-null-wins (framework semantics)**: the official `dsh-client-ui-deliverables` turnTail entry (output files) and this plugin's chip would be mutually exclusive single-election on the same chain. **Resolution: the chip moved out of turnTail** to the `conversation.chat.assistant-actions` list slot (inside the official action row: copy → feedback(10) → this chip(20) → branch → timeEnd; list slots render all entries, no election competition). assistant-actions renders only on turn-end rows (user messages have no extraActions, assistant step nodes have no toolbar — verified no duplication); the only edge case: turns with no closing text message (closing === null, e.g. pure tool turns) render no row and the chip is absent — such turns have usage but no message toolbar. 2. **PriceSettingsRow row contract — redesigned with the entry migration (live-feedback driven)**: the official row contract of the former `settings.general.item` (border-bottom + title/desc text block + right-side control) only made sense in the settings-page context; after the entry moved into the overview page's "Price Table" tab, the card title is carried by the shared Card, rows no longer repeat the title or keep a one-time migration note, and **the edit button sits at the row's left edge** (live feedback: "right side is unintuitive"). 3. **Locale interpolation — verified**: `translate()` replaces `{name}` placeholders via regex (dsh-client-locale client.js L1156); parameterized strings like `stats.window` match the official syntax. ✅ No implementation change needed. 4. **useProjection availability — degradation implemented**: keys like tokenUsage are registered by host plugins; sessions that don't carry them in their baseline (e.g. addressed subagents) read `undefined`, and views show "—" per "capability absence". **Pending live verification** (same round as risk 1). 5. **Subagents — verified during exploration**: subagents are independent sessions; this plugin's tab and chip appear automatically within the same session page shell and account only for that subagent's own usage (`openSubagent` follows the same `Session` render path). **Pending live verification**. 6. **Turn titles at compaction checkpoints — verified and hardened**: checkpoint user messages land as `CompactionSummaryNode` in the session view (kind:"compaction", "instruction envelope … never renders"), so they never become `user` nodes; `foldTurns` additionally skips compaction/context nodes explicitly with unit-test coverage. ✅ 7. **Currency rules (in spec)**: v0 display currency = the price table's first entry's currency; steps hitting entries with mismatched currency are treated as "unpriced (currency mismatch)" — never summing across currencies, never auto-converting. Time-window discounts prefer the request start time (`timing.stepStartTime`), falling back to the persist time when missing. ✅ Unit-tested. 8. **Projection field-name mismatch (found during implementation, fixed)**: the projection `tokenUsage` uncached-input field is `uncachedInputTokens` (official `billedInputTokens` caliber), differently named from the window node usage's `inputTokens`; `stats-fold.billedInputTokens` handles both shapes with unit tests. 9. **Model info missing on legacy nodes (found during implementation, solved via join)**: the `assistant/message` event's `message.source {provider, model}` is a required wire field, but ui-conversation's legacy `AssistantMessageNode` construction never copies `provenance`/`requestConfig` (typed, zero assignments) — reading nodes directly yields all "—". Solution: join `views.get("trajectory").requests[]` on `(turn, step)` (trajectory's own fold got the model from `message.source`; `requestConfig` comes from `request/header` events). Fill-only-missing, retry last-wins, return the original reference when no hit (unit-tested). Edge: the join depends on the trajectory snapshot and the loaded window; older session segments without persisted `request/header` have no requestConfig (the model is still available from message.source). 10. **Jump-back implementation (v0 trade-off)**: view-loop state lives in ui-conversation's private per-session chatStore (handle not exported); the plugin side has no official setView surface. v0 uses a DOM click on the tab bar's first tab (order 0 is always chat) + `chat.locations.getTurn()` key-based scroll positioning. **Pending live verification**: switching and scrolling behavior (incl. the degraded page-switch-only case when old turns aren't loaded). 11. **Editor modal host (v0 trade-off)**: the modal registers under the `shell.overlay` frame overlay (root scope; layer container pointer-events:none, children auto, z-index 20); live preview samples the current session's window via the sessions service. **Pending live verification**: stacking with the settings panel. 12. **Price table drift with official repricing**: semantically declared as "estimated values for reference only"; bill-level accuracy is not a goal. 13. **Chip full-window refolding cost**: each chip render runs one `foldTurns` over the window (plus one requests join); acceptable at v0 data volumes; the long-session optimization direction is a `chat.locations.getTurn(turn)` index. 14. **Whether host `session.list` returns archived sessions (pending live verification)**: verified that the client side does not filter; host behavior pending. If the host filters, the overview shows the explicit notice "archived sessions are not in the current data plane" (the `archivedMissingFromList` guard is implemented) — this directly affects the archived-visibility selling point. 15. **`sessions.open` availability for archived sessions (pending live verification)**: `select()` throws for ids not in summaries (verified); try/catch degrades to in-page ledger expansion; the main path awaits live confirmation. 16. **Seat dual form and placement (live verified)**: the `sidebar.footer.action` owner passes only `{wide}` (contract verified); live feedback said "the button outline is jarring" — root cause was a missing `border:none` (browser default button border). **Resolution: the seat button chrome now mirrors the official Settings seat trigger value-for-value** (ui-settings-general `SettingsRoot.module.css` `.trigger`/`.rail`: radius 12, height 42, `margin:4px -2px; padding:0 10px 0 8px`; rail 36×36 round, icons 16/18px, hover `interactive-bg-hover`); the wrapper `display:contents` participates directly in the shell's footerActions layout. 17. **data-slot measurement spike (pending live verification)**: the SlotOutlet anchor is `display:contents` (no box); the plugin measures its parent column (`sidebarCol`, a real box) right edge — sturdier than the design's original "measure the outlet" plan; RO frame-tracking during drag animation and the collapsed/rail tri-forms await a live walkthrough; the degraded `inset:0` is ready. 18. **In-overlay z-order coexistence (live verified and fixed)**: live testing found the overview's right side covered by another V-series plugin's dock (`.jyx-dock{position:fixed; z-index:900}`, preview window 950) — children inside the layer control their own geometry, and explicit z-index wins. **Resolution: the overview frame uses `z-index:1000` (> dock 900/preview 950, still below that plugin's interactive overlays menu 10000/refPop 10002 so popups aren't swallowed)**; the price editor `.tsn-modal-root` rises to `z-index:1100` to stay above the overview; the official settings panel is a root-context `z-index:1000` fixed layer (outside the overlay layer's z=20 context) and always sits above the overview — layering is correct. 19. **List-row projection staleness (live observed)**: projection seq exists only on ProjectionValueStore's internal rows (`values()` has no asOfSeq); list rows can't get per-key watermarks — the UI declares "based on the last computation" via fallback copy and does no client-side recomputation. 20. **L2 tier degradation in ledger aging (implementation trade-off)**: original entries stay exactly per-step within the consolidation window (2 days as of v0.4.0, see item 23 budget orchestration); aged buckets bill via defaultTier + discount-window hits (L3 preserved, L2 unrecoverable) — buckets are labeled "aged bucket" in both the session details and charts, with cost semantics recorded honestly. 21. **Deep-backfill RPC volume**: opt-in per session, 20 pages × 200 messages per session; a global "backfill all" is deliberately absent (as of v0.4.0 the backfill channel is the `ctx.remote` follow+page pair, see items 25/26; running sessions are excluded). 22. **Double-counting of ancestor usage in branched sessions (live-confirmed and fixed, the `usage-integrity` spec)**: the host's `sessions.fork` copies all parent events (including usage) into the child session log via `seed: events.slice(0, cut)`, and the host token-meter is seed-unaware — three sessions were measured with identical usage, triple-counted. Fix: fork sessions (with a `parentId`, not subagents) are **excluded session-wide** — their projections and ledger entries enter no KPI/workspace/by-model/trend/cost-line total (seeded and post-fork segments cannot be split at the entry level; session-wide exclusion is the accepted trade-off, so post-fork usage stays visible only in the session's own details); rows remain with a "branch" badge and the exclusion count is disclosed (`dash.fork.excluded`). 23. **Ledger v2 sharded storage and total budget**: index key + per-session shards + compact encoding (entry size ≈ −62%); five-step persist orchestration: routine consolidation (2-day window) + orphan GC → watermark estimate (5MB character budget + shard-size cache) → per-session tightened retry → full-consolidation tier re-estimate → evict whole sessions by `lastSeen` down to budget − 256KB headroom; total failure falls to in-memory mode. `lastSeen = 0` (recency unknown) is exempt from orphan GC (a missing timestamp must not escalate to wiping the whole ledger). 24. **Recorded pricing (persisted costs)**: every persist snapshots the single-pass pricing artifact into the shard (`{v, ev, fp, cur, cost, mc, bm, dc, um}`, with a PRICING_ENGINE_VERSION stamp + FNV-1a price fingerprint); cost display follows a three-tier resolution order (recorded price → per-session targeted recompute → full-ledger re-export), and price edits never rewrite history; pricing snapshots count toward the size cache and total budget. 25. **dsh-012 host migration, three landing points**: ① snapshot split — `legacy.nodes`/`locations`/`turnTimings`/trajectory now read the standard `useChat`/`useTrajectory` hooks; ② `connection.api` is gone — deep backfill switched to `ctx.remote.session.follow` (open-frame for the cursor) + `page` (seq-based paging, 20 pages × 200 messages budget; `binding.loadOlder` is openState-gated and was dropped), the model catalog moved to `remote.session.modelCatalog()`, and editor preview sampling goes through `eventSource` offline folding; ③ `data-slot="sidebar"` was removed — the geometry anchor now uses `closest("[data-shell-overlay]")` attribute anchoring. The `dsh.client.inject` baseline is empty; the host bootstraps on demand. 26. **Deep-backfill pacing (pacing.mjs)**: inter-page yields (40ms baseline; RTT EWMA >1500ms escalates ×2 capped at 1500ms, <750ms halves back down, hysteresis holds in between) — the plugin-side mitigation for two host OOMs caused by "rebuild/auto-backfill × subagent storm"; running sessions are excluded from backfill (moving targets).