/** * dsh-widgets — session-stats projection. * * Folds the live conversation (settled nodes, running tool calls, the open * turn/step timeline) into the window-scoped numbers the stats-line widgets * print, and projects the same nodes into the 轨迹 three-lane beats. * * Pure functions over host-provided shapes: no React, no DOM, no module state. * The dock collector calls them on every pass; everything here is deterministic * for a given input, which is what makes the numbers testable in isolation. */ import { type CompactionSummary, type GoalInfo, type JobInfo, type ModelSelectionInfo, type PermissionInfo, type SubagentEntry, type ToolCallSummary, type TrajectoryBeat } from '../lib/contract/types'; /** Session stats shape collected by the dock collector. */ export interface Stats { turns: number; steps: number; llmMs: number; toolMs: number; ttftMs: number; ttftSteps: number; decodeMs: number; decodeTokens: number; usage: { inputTokens: number; cacheReadTokens: number; outputTokens: number; } | null; contextPercent?: number | null; contextWindow?: number | null; contextTokens?: number | null; contextBreakdown?: { systemTokens: number; toolsTokens: number; messageTokens: number; } | null; todos?: Array<{ content: string; status: 'pending' | 'in_progress' | 'completed'; }> | null; heatmapGrid?: Array>; heatmapRaw?: Record; trajectory?: TrajectoryBeat[]; tools?: ToolCallSummary; compactions?: CompactionSummary | null; modelSelection?: ModelSelectionInfo | null; agentPreset?: string | null; goal?: GoalInfo | null; permissions?: PermissionInfo | null; subagents?: SubagentEntry[] | null; jobs?: JobInfo[] | null; /** The open turn's own elapsed ms (earliest step start → now), or null. */ liveTurnMs?: number | null; /** The open model step's own elapsed ms, or null when no step is in flight. */ liveStepMs?: number | null; } /** * Normalize the `modelSelection` projection into the contract's own shape. * * The projection's wire value is read defensively: a bundle whose session * controller is older, newer, or absent hands us anything at all, and a card * must never be the thing that throws inside the selector (the slot renderer * answers a throwing selector by abdicating the whole entry — every card's data * goes with it). Unknown members are DROPPED rather than passed through, so a * widget can trust the shape it is handed. * * @param value - the raw projection value (any). * @returns the normalized value, or null when there is nothing to report. */ export declare function normalizeModelSelection(value: unknown): ModelSelectionInfo | null; /** Normalize the `goal` projection; null when this session has no goal. */ export declare function normalizeGoal(value: unknown): GoalInfo | null; /** Normalize the `permissions` projection; null when no permission service is composed. */ export declare function normalizePermissions(value: unknown): PermissionInfo | null; /** Normalize the `subagentCatalog` projection: identity rows only, unknown * members dropped. Returns null when the projection is absent (which is NOT the * same statement as "no children" — that is `[]`). * * `activeMs` is attached from the client session list when it is reachable: the * parent cannot read a child's `subagentTiming` through its own projection (that * key folds THIS session's log), so the duration comes from * `byId[childId].projectionValues.subagentTiming` — the same detour the official * subagent list takes. Absent list ⇒ the field stays absent, never 0. * * @param value - the raw `subagentCatalog` projection value (any). * @param summaryOf - lookup for one child's session-list row, or null. * @param now - the wall clock the open-turn duration is measured against. */ export declare function normalizeSubagents(value: unknown, summaryOf?: ((id: string) => unknown) | null, now?: number): SubagentEntry[] | null; /** Normalize this session's `jobsBySession` entry: identity + liveness, unknown * members dropped. Returns null when the list is unavailable (NOT the same * statement as "no jobs" — that is `[]`). */ export declare function normalizeJobs(value: unknown): JobInfo[] | null; /** * Project the live conversation into the official 轨迹 layout's three lanes: * 输入 (user/steering message) · 模型 (assistant step) · 工具 (tool call). * * Beats are ordered by their START time and trimmed to the newest * `TRAJECTORY_WINDOW`. In-flight work is included as it happens —running tool * calls plus open, not-yet-assembled assistant steps —which is what makes the * card move while the model is working instead of only after a turn settles. * * @param settled - Conversation nodes (`useChat().legacy.nodes`). * @param runningCalls - Live tool calls (`useChat().legacy.runningCalls`). * @param timeline - Turn/step timeline (open steps have no node yet). * @param now - performance.now() at this collection pass. */ export declare function deriveTrajectory(settled: ReadonlyArray, runningCalls: ReadonlyArray, timeline: any, now: number): TrajectoryBeat[]; /** * Fold the conversation's tool calls into the 工具调用 card's summary. * * Why this exists: the card used to print ONE number (cumulative tool time), so a * slow turn could not be told apart from a hung tool. The name, the error flag and * the two timestamps are all already on the nodes the client loads — a * `tool-result` carries `call.name`, `isError` and `callTime`, and a running call * carries `name` and `time` — so this costs one pass over the same array * `deriveStats` already walks. * * @param settled - Conversation nodes (`useChat().legacy.nodes`). * @param runningCalls - Live tool calls (`useChat().legacy.runningCalls`). * @param now - `Date.now()` at this collection pass (the running call's elapsed). */ export declare function deriveTools(settled: ReadonlyArray, runningCalls: ReadonlyArray, now: number): ToolCallSummary; /** * Fold the conversation's compaction markers into the 上下文压缩 card's summary. * * Why this exists: context is trimmed silently. A long session shows the meter * drop and the transcript lose rows, with nothing saying how much history was * folded away or when — so "why did the model forget that?" has no answer on * screen. The markers are already nodes in the stream the collector walks * (`kind: 'compaction'`), so this costs one more filter over the same array. * * A `null` `shadowedTokenCount`/`shadowedItemCount` means the summary event was * outside the loaded window: the compaction is still COUNTED (it happened) while * its figures stay null, so the card can print `—` instead of a fabricated 0. * * @param settled - Conversation nodes (`useChat().legacy.nodes`). */ export declare function deriveCompaction(settled: ReadonlyArray): CompactionSummary; /** Fold assistant/tool-result nodes into the same window-scoped stats as the shipped StatsLine fallback. */ export declare function deriveStats(nodes: ReadonlyArray): Omit;