@mingozhou/dsh-replay 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +164 -0
  3. package/README.zh.md +164 -0
  4. package/cordis.patch.yml +6 -0
  5. package/lib/client.js +2478 -0
  6. package/lib/core.js +720 -0
  7. package/lib/index.js +111 -0
  8. package/lib/types/demo/main.d.ts +1 -0
  9. package/lib/types/demo/samples.d.ts +21 -0
  10. package/lib/types/src/client/api.d.ts +43 -0
  11. package/lib/types/src/client/components/AuditView.d.ts +8 -0
  12. package/lib/types/src/client/components/CompareView.d.ts +9 -0
  13. package/lib/types/src/client/components/ForkTreeView.d.ts +7 -0
  14. package/lib/types/src/client/components/ItemDetail.d.ts +6 -0
  15. package/lib/types/src/client/components/OverviewView.d.ts +8 -0
  16. package/lib/types/src/client/components/ReplayApp.d.ts +12 -0
  17. package/lib/types/src/client/components/ReplayModal.d.ts +8 -0
  18. package/lib/types/src/client/components/SessionPicker.d.ts +6 -0
  19. package/lib/types/src/client/components/TimelineView.d.ts +6 -0
  20. package/lib/types/src/client/export.d.ts +13 -0
  21. package/lib/types/src/client/i18n.d.ts +24 -0
  22. package/lib/types/src/client/index.d.ts +12 -0
  23. package/lib/types/src/client/mascot-image.d.ts +1 -0
  24. package/lib/types/src/client/mascot.d.ts +17 -0
  25. package/lib/types/src/client/viewer-entry.d.ts +1 -0
  26. package/lib/types/src/core/audit.d.ts +39 -0
  27. package/lib/types/src/core/compare.d.ts +31 -0
  28. package/lib/types/src/core/cost.d.ts +30 -0
  29. package/lib/types/src/core/decode.d.ts +25 -0
  30. package/lib/types/src/core/forktree.d.ts +33 -0
  31. package/lib/types/src/core/index.d.ts +7 -0
  32. package/lib/types/src/core/timeline.d.ts +96 -0
  33. package/lib/types/src/core/types.d.ts +88 -0
  34. package/lib/types/src/index.d.ts +20 -0
  35. package/lib/viewer.js +812 -0
  36. package/package.json +107 -0
  37. package/scripts/build.mjs +131 -0
  38. package/src/client/api.ts +119 -0
  39. package/src/client/components/AuditView.tsx +57 -0
  40. package/src/client/components/CompareView.tsx +186 -0
  41. package/src/client/components/ForkTreeView.tsx +123 -0
  42. package/src/client/components/ItemDetail.tsx +117 -0
  43. package/src/client/components/OverviewView.tsx +258 -0
  44. package/src/client/components/ReplayApp.tsx +205 -0
  45. package/src/client/components/ReplayModal.tsx +53 -0
  46. package/src/client/components/SessionPicker.tsx +60 -0
  47. package/src/client/components/TimelineView.tsx +296 -0
  48. package/src/client/export.ts +43 -0
  49. package/src/client/i18n.ts +360 -0
  50. package/src/client/index.ts +146 -0
  51. package/src/client/mascot-image.ts +1 -0
  52. package/src/client/mascot.tsx +192 -0
  53. package/src/client/styles.css +719 -0
  54. package/src/client/viewer-entry.tsx +66 -0
  55. package/src/core/audit.ts +227 -0
  56. package/src/core/compare.ts +0 -0
  57. package/src/core/cost.ts +61 -0
  58. package/src/core/decode.ts +125 -0
  59. package/src/core/forktree.ts +104 -0
  60. package/src/core/index.ts +7 -0
  61. package/src/core/timeline.ts +396 -0
  62. package/src/core/types.ts +90 -0
  63. package/src/index.ts +157 -0
  64. package/tsconfig.json +18 -0
  65. package/types.d.ts +15 -0
package/lib/index.js ADDED
@@ -0,0 +1,111 @@
1
+ // src/index.ts
2
+ import { readFile } from "node:fs/promises";
3
+ var name = "dsh-replay";
4
+ var inject = ["webServer", "sessionQuery"];
5
+ function json(res, status, body) {
6
+ res.writeHead(status, {
7
+ "content-type": "application/json; charset=utf-8",
8
+ "cache-control": "no-store"
9
+ });
10
+ res.end(JSON.stringify(body));
11
+ }
12
+ function get(obj, key) {
13
+ return typeof obj === "object" && obj !== null ? obj[key] : void 0;
14
+ }
15
+ function headerOf(record) {
16
+ for (const candidate of [get(record, "header"), get(record, "meta"), record]) {
17
+ if (typeof get(candidate, "id") === "string" && typeof get(candidate, "createdAt") === "number") {
18
+ return candidate;
19
+ }
20
+ }
21
+ return void 0;
22
+ }
23
+ function titleOf(record, depth = 3) {
24
+ if (typeof record !== "object" || record === null || depth < 0) return void 0;
25
+ const direct = record["title"];
26
+ if (typeof direct === "string" && direct.length > 0) return direct;
27
+ for (const value of Object.values(record)) {
28
+ if (typeof value === "object" && value !== null) {
29
+ const found = titleOf(value, depth - 1);
30
+ if (found !== void 0) return found;
31
+ }
32
+ }
33
+ return void 0;
34
+ }
35
+ function apply(ctx, config = {}) {
36
+ const prefix = config.routePrefix ?? "/replay/api";
37
+ const webServer = ctx.webServer;
38
+ const sessionQuery = ctx.sessionQuery;
39
+ const handler = async (req, res) => {
40
+ const url = new URL(req.url ?? "/", "http://localhost");
41
+ const rest = url.pathname.slice(prefix.length).replace(/^\//u, "");
42
+ try {
43
+ if (req.method !== "GET" && req.method !== "HEAD") {
44
+ json(res, 405, { error: "method not allowed" });
45
+ return;
46
+ }
47
+ if (rest === "sessions") {
48
+ const records = await sessionQuery.listSessions();
49
+ const headers = records.map(headerOf).filter((h) => h !== void 0);
50
+ const titles = /* @__PURE__ */ new Map();
51
+ if (sessionQuery.readTitleSnapshots !== void 0) {
52
+ try {
53
+ const snapshots = await sessionQuery.readTitleSnapshots(headers.map((h) => String(h["id"])));
54
+ snapshots.forEach((snapshot, i) => {
55
+ const title = titleOf(snapshot);
56
+ const id = headers[i]?.["id"];
57
+ if (title !== void 0 && typeof id === "string") titles.set(id, title);
58
+ });
59
+ } catch {
60
+ }
61
+ }
62
+ const readTitle = sessionQuery.readTitle;
63
+ if (readTitle !== void 0) {
64
+ await Promise.all(
65
+ headers.filter((h) => !titles.has(String(h["id"]))).map(async (h) => {
66
+ try {
67
+ const title = titleOf(await readTitle.call(sessionQuery, String(h["id"])));
68
+ if (title !== void 0) titles.set(String(h["id"]), title);
69
+ } catch {
70
+ }
71
+ })
72
+ );
73
+ }
74
+ json(res, 200, {
75
+ sessions: headers.map((h) => ({ header: h, title: titles.get(String(h["id"])) }))
76
+ });
77
+ return;
78
+ }
79
+ if (rest === "viewer.js") {
80
+ const source = await readFile(new URL("./viewer.js", import.meta.url), "utf8");
81
+ res.writeHead(200, { "content-type": "text/javascript; charset=utf-8" });
82
+ res.end(source);
83
+ return;
84
+ }
85
+ if (rest.startsWith("session/")) {
86
+ const id = decodeURIComponent(rest.slice("session/".length));
87
+ const snapshot = await sessionQuery.readSession(id);
88
+ const header = headerOf(snapshot) ?? headerOf(get(snapshot, "session"));
89
+ const events = get(snapshot, "events") ?? get(get(snapshot, "session"), "events") ?? [];
90
+ if (header === void 0) {
91
+ json(res, 404, { error: "session not found" });
92
+ return;
93
+ }
94
+ json(res, 200, { header, events });
95
+ return;
96
+ }
97
+ json(res, 404, { error: "unknown replay endpoint" });
98
+ } catch (error) {
99
+ json(res, 500, { error: String(error) });
100
+ }
101
+ };
102
+ ctx.effect(
103
+ () => webServer.register({ kind: "prefix", path: prefix, handler }),
104
+ "dsh-replay: http api"
105
+ );
106
+ }
107
+ export {
108
+ apply,
109
+ inject,
110
+ name
111
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Realistic sample session logs in the harness wire format (format version 0),
3
+ * used by the demo page and the test suite. Three sessions:
4
+ * A — root session with tool calls, approvals, a failing test run, packed
5
+ * chunk rows, and a couple of audit-worthy commands;
6
+ * B — a fork of A at the end of turn 1 (parentSession + seedLength +
7
+ * session/end-seed marker), continuing differently;
8
+ * C — a subagent session doing web research.
9
+ */
10
+ export declare const SESSION_A_ID = "a7f3c2d1-4b5e-4a6f-8c9d-0e1f2a3b4c5d";
11
+ export declare const SESSION_B_ID = "b8e4d3c2-5c6f-4b7a-9d0e-1f2a3b4c5d6e";
12
+ export declare const SESSION_C_ID = "c9f5e4d3-6d7a-4c8b-a0e1-2f3a4b5c6d7e";
13
+ export declare const SESSION_D_ID = "d0a6f5e4-7e8b-4d9c-b1f2-3a4b5c6d7e8f";
14
+ export declare function sessionA(): string;
15
+ export declare function sessionB(): string;
16
+ export declare function sessionC(): string;
17
+ export declare function sessionD(): string;
18
+ export declare const SAMPLE_LOGS: {
19
+ jsonl: string;
20
+ title: string;
21
+ }[];
@@ -0,0 +1,43 @@
1
+ import type { ParsedSession, SessionHeader } from '../core/types.js';
2
+ export interface SessionEntry {
3
+ header: SessionHeader;
4
+ title?: string;
5
+ }
6
+ export interface ReplayApi {
7
+ listSessions: () => Promise<SessionEntry[]>;
8
+ getSession: (id: string) => Promise<ParsedSession>;
9
+ /** Source of the standalone viewer bundle, for HTML export (optional). */
10
+ getViewerSource?: () => Promise<string>;
11
+ }
12
+ export declare class HttpReplayApi implements ReplayApi {
13
+ private readonly base;
14
+ constructor(base?: string);
15
+ private fetchJson;
16
+ listSessions(): Promise<SessionEntry[]>;
17
+ getSession(id: string): Promise<ParsedSession>;
18
+ getViewerSource(): Promise<string>;
19
+ }
20
+ /** Demo/testing implementation over raw JSONL texts keyed by session id. */
21
+ export declare class StaticReplayApi implements ReplayApi {
22
+ private readonly sessions;
23
+ private readonly titles;
24
+ /** Optional URL of the viewer bundle for HTML export (demo mode). */
25
+ viewerUrl?: string;
26
+ constructor(logs: {
27
+ jsonl: string;
28
+ title?: string;
29
+ }[]);
30
+ /** Build directly from already-decoded sessions (exported HTML, tooling). */
31
+ static fromParsed(list: {
32
+ header: ParsedSession['header'];
33
+ events: ParsedSession['events'];
34
+ title?: string;
35
+ }[]): StaticReplayApi;
36
+ getViewerSource(): Promise<string>;
37
+ listSessions(): Promise<SessionEntry[]>;
38
+ getSession(id: string): Promise<ParsedSession>;
39
+ }
40
+ export declare function formatDuration(ms: number): string;
41
+ export declare function formatTokens(n: number): string;
42
+ export declare function formatClock(time: number, startTime: number): string;
43
+ export declare function shortId(id: string): string;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import type { AuditReport } from '../../core/audit.js';
3
+ import type { TimelineModel } from '../../core/timeline.js';
4
+ export declare function AuditView({ report, model, onJump, }: {
5
+ report: AuditReport;
6
+ model: TimelineModel;
7
+ onJump: (seq: number) => void;
8
+ }): React.ReactElement;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import type { ParsedSession } from '../../core/types.js';
3
+ import { type ReplayApi, type SessionEntry } from '../api.js';
4
+ export declare function CompareView({ api, session, entries, title, }: {
5
+ api: ReplayApi;
6
+ session: ParsedSession;
7
+ entries: SessionEntry[];
8
+ title?: string;
9
+ }): React.ReactElement;
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import type { SessionEntry } from '../api.js';
3
+ export declare function ForkTreeView({ entries, currentId, onSelect, }: {
4
+ entries: SessionEntry[];
5
+ currentId?: string;
6
+ onSelect: (id: string) => void;
7
+ }): React.ReactElement;
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import type { TimelineItem, TimelineModel } from '../../core/timeline.js';
3
+ export declare function ItemDetail({ item, model, }: {
4
+ item: TimelineItem;
5
+ model: TimelineModel;
6
+ }): React.ReactElement;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import type { AuditReport } from '../../core/audit.js';
3
+ import type { TimelineModel } from '../../core/timeline.js';
4
+ export declare function OverviewView({ model, report, onOpenAudit, }: {
5
+ model: TimelineModel;
6
+ report: AuditReport;
7
+ onOpenAudit: () => void;
8
+ }): React.ReactElement;
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { type ReplayApi } from '../api.js';
3
+ import { type Lang } from '../i18n.js';
4
+ export declare function ReplayApp({ api, sessionId, initialLang, onSwitchSession, }: {
5
+ api: ReplayApi;
6
+ /** Omit to start on the session picker (modal / standalone modes). */
7
+ sessionId?: string;
8
+ /** Override the detected language ('en' | 'zh'). */
9
+ initialLang?: Lang;
10
+ /** Optional host-provided navigation; falls back to internal state. */
11
+ onSwitchSession?: (id: string) => void;
12
+ }): React.ReactElement;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import type { ReplayApi } from '../api.js';
3
+ /** Full-screen modal hosting the replay app, opened from the sidebar button. */
4
+ export declare function ReplayModal({ api, onClose, closeLabel, }: {
5
+ api: ReplayApi;
6
+ onClose: () => void;
7
+ closeLabel: string;
8
+ }): React.ReactElement;
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { type ReplayApi } from '../api.js';
3
+ export declare function SessionPicker({ api, onPick, }: {
4
+ api: ReplayApi;
5
+ onPick: (id: string) => void;
6
+ }): React.ReactElement;
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import type { TimelineModel } from '../../core/timeline.js';
3
+ export declare function TimelineView({ model, jumpToSeq, }: {
4
+ model: TimelineModel;
5
+ jumpToSeq?: number;
6
+ }): React.ReactElement;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Export a session as a single self-contained HTML file: the viewer bundle
3
+ * (react inlined) + the session data, playable offline and shareable in an
4
+ * issue, a blog post, or a chat.
5
+ */
6
+ import type { ParsedSession } from '../core/types.js';
7
+ export interface ExportSession {
8
+ header: ParsedSession['header'];
9
+ events: ParsedSession['events'];
10
+ title?: string;
11
+ }
12
+ export declare function buildReplayHtml(viewerSource: string, sessions: ExportSession[]): string;
13
+ export declare function downloadReplayHtml(html: string, filename: string): void;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Lightweight EN/中文 i18n for the replay UI. The dictionary is plain data;
3
+ * `useI18n()` provides `t(key, params)` plus the current language and setter.
4
+ * Language resolution: explicit choice (persisted) > browser language > en.
5
+ */
6
+ import React from 'react';
7
+ export type Lang = 'en' | 'zh';
8
+ export type T = (key: string, params?: Record<string, string | number>) => string;
9
+ export declare function makeT(lang: Lang): T;
10
+ export declare function detectLang(): Lang;
11
+ export declare function persistLang(lang: Lang): void;
12
+ export interface I18n {
13
+ lang: Lang;
14
+ t: T;
15
+ setLang: (lang: Lang) => void;
16
+ }
17
+ export declare const I18nContext: React.Context<I18n>;
18
+ export declare function useI18n(): I18n;
19
+ import type { AuditFinding } from '../core/audit.js';
20
+ import type { TimelineItem } from '../core/timeline.js';
21
+ /** Localized finding message; falls back to the stable English title. */
22
+ export declare function localizeFinding(t: T, finding: AuditFinding): string;
23
+ /** Localized rail label for meta items; other kinds keep their content labels. */
24
+ export declare function localizeItemLabel(t: T, item: TimelineItem): string;
@@ -0,0 +1,12 @@
1
+ export declare const name = "dsh-replay";
2
+ export declare const inject: string[];
3
+ interface SlotRegistryLike {
4
+ inject: (key: string, callback: () => unknown) => void;
5
+ register: (options: Record<string, unknown>, component: unknown) => () => void;
6
+ }
7
+ interface ClientContextLike {
8
+ slots: SlotRegistryLike;
9
+ effect: (callback: () => () => void, label?: string) => void;
10
+ }
11
+ export declare function apply(ctx: ClientContextLike): void;
12
+ export {};
@@ -0,0 +1 @@
1
+ export declare const mascotImage: string | undefined;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 鲸小深 (Jing Xiaoshen) — dsh-replay's mascot, an original whale-catgirl
3
+ * character. When `assets/jingxiaoshen.png` exists at build time it is
4
+ * embedded and used everywhere; otherwise a built-in SVG chibi stands in.
5
+ * Hide globally with: .dshr-mascot { display: none }
6
+ */
7
+ import React from 'react';
8
+ export type MascotMood = 'idle' | 'happy' | 'alert';
9
+ export declare function Mascot({ mood, size, }: {
10
+ mood?: MascotMood;
11
+ size?: number;
12
+ }): React.ReactElement;
13
+ export declare function MascotState({ mood, text, size, }: {
14
+ mood: MascotMood;
15
+ text: string;
16
+ size?: number;
17
+ }): React.ReactElement;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Security-audit pass over a decoded session: classify events into findings
3
+ * with a severity, grouped for the audit view. Rules are data, not code —
4
+ * extend `DEFAULT_RULES` or pass your own list.
5
+ *
6
+ * Severity vocabulary mirrors a standard status palette:
7
+ * 'critical' | 'serious' | 'warning' | 'info'.
8
+ */
9
+ import type { ParsedSession, SessionEvent } from './types.js';
10
+ export type Severity = 'critical' | 'serious' | 'warning' | 'info';
11
+ export interface AuditFinding {
12
+ ruleId: string;
13
+ severity: Severity;
14
+ seq: number;
15
+ time: number;
16
+ /** English title (stable fallback). */
17
+ title: string;
18
+ detail?: string;
19
+ /** Structured message id + params, so UIs can localize findings. */
20
+ messageKey?: string;
21
+ params?: Record<string, string>;
22
+ }
23
+ export interface AuditRule {
24
+ id: string;
25
+ severity: Severity | ((event: SessionEvent) => Severity);
26
+ /** Return a finding title when the event matches, undefined otherwise. */
27
+ match: (event: SessionEvent) => {
28
+ title: string;
29
+ detail?: string;
30
+ messageKey?: string;
31
+ params?: Record<string, string>;
32
+ } | undefined;
33
+ }
34
+ export interface AuditReport {
35
+ findings: AuditFinding[];
36
+ counts: Record<Severity, number>;
37
+ }
38
+ export declare const DEFAULT_RULES: AuditRule[];
39
+ export declare function auditSession(session: ParsedSession, rules?: AuditRule[]): AuditReport;
@@ -0,0 +1,31 @@
1
+ import type { ParsedSession } from './types.js';
2
+ export interface CompareStats {
3
+ id: string;
4
+ title?: string;
5
+ turns: number;
6
+ steps: number;
7
+ events: number;
8
+ toolCallCount: number;
9
+ toolErrors: number;
10
+ inputTokens: number;
11
+ billedInputTokens: number;
12
+ outputTokens: number;
13
+ cacheReadTokens: number;
14
+ durationMs: number;
15
+ models: string[];
16
+ toolMix: Record<string, number>;
17
+ }
18
+ export interface CompareResult {
19
+ a: CompareStats;
20
+ b: CompareStats;
21
+ /** All tool names across both, for a stable table order. */
22
+ toolNames: string[];
23
+ /** First seq at which the two logs differ (comparing type+data), if computable. */
24
+ divergenceSeq?: number;
25
+ /** True when one session is an ancestor/fork relative of the other. */
26
+ related: boolean;
27
+ }
28
+ export declare function compareSessions(a: ParsedSession, b: ParsedSession, titles?: {
29
+ a?: string;
30
+ b?: string;
31
+ }): CompareResult;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Cost estimation from token totals. Prices are USD per 1M tokens and are
3
+ * ESTIMATES — provider list prices change; override the table if you need
4
+ * exact numbers. Cache-write falls back to the input rate when a provider
5
+ * doesn't price it separately.
6
+ */
7
+ import type { TokenTotals } from './timeline.js';
8
+ export interface PricingRate {
9
+ /** USD per 1M uncached input tokens. */
10
+ input: number;
11
+ /** USD per 1M cache-read tokens. */
12
+ cacheRead: number;
13
+ /** USD per 1M cache-write tokens (defaults to `input`). */
14
+ cacheWrite?: number;
15
+ /** USD per 1M output tokens. */
16
+ output: number;
17
+ }
18
+ export interface PricingEntry {
19
+ match: RegExp;
20
+ rate: PricingRate;
21
+ label: string;
22
+ }
23
+ /** Editable defaults — first match wins, most specific first. */
24
+ export declare const DEFAULT_PRICING: PricingEntry[];
25
+ export interface CostEstimate {
26
+ usd: number;
27
+ label: string;
28
+ }
29
+ export declare function estimateCost(totals: TokenTotals, models: string[], pricing?: PricingEntry[]): CostEstimate | undefined;
30
+ export declare function formatUsd(usd: number): string;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Decode a DeepSeek Harness session log (JSONL text or pre-parsed records)
3
+ * into a `ParsedSession`: header + flat, seq-ordered `SessionEvent[]`.
4
+ *
5
+ * Handles the two wire realities documented in the harness:
6
+ * - the header line (`type: 'session'`) is not an event;
7
+ * - runs of streaming chunks may be packed into `*-chunks` rows
8
+ * (`seq0`/`time0`/`dt[]`) that expand back into `assistant/chunk` events.
9
+ *
10
+ * A torn final line (crash tail without newline) is tolerated and reported
11
+ * in `malformed`, mirroring the harness's own committed-bytes semantics.
12
+ */
13
+ import type { ChunkRowBase, ParsedSession, SessionEvent, SessionHeader, StorageRecord } from './types.js';
14
+ export declare function isChunkRow(record: StorageRecord): record is ChunkRowBase;
15
+ /** Expand one packed chunk row into its constituent `assistant/chunk` events. */
16
+ export declare function expandChunkRow(row: ChunkRowBase): SessionEvent[];
17
+ /** Decode one storage record (already JSON.parsed) into 0..n events. */
18
+ export declare function decodeStorageRecord(record: StorageRecord): SessionEvent[];
19
+ /**
20
+ * Parse raw JSONL text (uncompressed). The first line must be the session
21
+ * header; every further line is a `StorageRecord`.
22
+ */
23
+ export declare function parseSessionJsonl(text: string): ParsedSession;
24
+ /** Build a ParsedSession from an already-materialized header + events (e.g. an API response). */
25
+ export declare function fromInspection(header: SessionHeader, records: StorageRecord[]): ParsedSession;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Build the session lineage forest from headers (`parentSession` +
3
+ * `seedLength`), the durable fork/resume metadata the harness keeps
4
+ * out-of-log. Produces a forest plus a deterministic tidy layout
5
+ * (depth = x, DFS row = y) the SVG view renders directly.
6
+ */
7
+ import type { SessionHeader } from './types.js';
8
+ export interface ForkNode {
9
+ id: string;
10
+ header: SessionHeader;
11
+ title?: string;
12
+ children: ForkNode[];
13
+ /** Inclusive source event count inherited from the parent (undefined for roots). */
14
+ seedLength?: number;
15
+ depth: number;
16
+ /** Row index assigned by DFS layout. */
17
+ row: number;
18
+ /** True when parentSession points at a header we never saw. */
19
+ orphan?: boolean;
20
+ }
21
+ export interface ForkForest {
22
+ roots: ForkNode[];
23
+ nodes: Map<string, ForkNode>;
24
+ rowCount: number;
25
+ maxDepth: number;
26
+ }
27
+ export interface SessionListEntry {
28
+ header: SessionHeader;
29
+ title?: string;
30
+ }
31
+ export declare function buildForkForest(entries: SessionListEntry[]): ForkForest;
32
+ /** Path from root to the given session (inclusive), for breadcrumbs. */
33
+ export declare function lineageOf(forest: ForkForest, id: string): ForkNode[];
@@ -0,0 +1,7 @@
1
+ export * from './types.js';
2
+ export * from './decode.js';
3
+ export * from './timeline.js';
4
+ export * from './audit.js';
5
+ export * from './forktree.js';
6
+ export * from './compare.js';
7
+ export * from './cost.js';
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Fold a decoded session into the timeline model the UI renders:
3
+ * turns → steps → items (user / assistant / tool / meta), with durations,
4
+ * per-step token usage (last sample wins per turn/step, per the harness's
5
+ * `usageOf` replace-not-accumulate rule), and cumulative token totals.
6
+ */
7
+ import type { ParsedSession, SessionEvent, TokenUsage } from './types.js';
8
+ export type ItemKind = 'user' | 'assistant' | 'tool' | 'meta';
9
+ export interface ToolCallInfo {
10
+ callId: string;
11
+ name: string;
12
+ /** Raw argument JSON string exactly as the model produced it. */
13
+ argumentsRaw: string;
14
+ argumentsPretty?: string;
15
+ resultText?: string;
16
+ error?: {
17
+ name: string;
18
+ code: string;
19
+ };
20
+ callSeq: number;
21
+ resultSeq?: number;
22
+ callTime: number;
23
+ resultTime?: number;
24
+ durationMs?: number;
25
+ }
26
+ export interface TimelineItem {
27
+ kind: ItemKind;
28
+ seq: number;
29
+ time: number;
30
+ turn?: number;
31
+ step?: number;
32
+ type: string;
33
+ /** Short one-line label for the timeline rail. */
34
+ label: string;
35
+ /** Full text body when the item carries prose (user/assistant messages). */
36
+ text?: string;
37
+ reasoningText?: string;
38
+ toolCall?: ToolCallInfo;
39
+ usage?: TokenUsage;
40
+ event: SessionEvent;
41
+ /** True when this surface node was later shadowed by compaction. */
42
+ shadowed?: boolean;
43
+ }
44
+ export interface StepModel {
45
+ turn: number;
46
+ step: number;
47
+ startSeq: number;
48
+ endSeq?: number;
49
+ startTime: number;
50
+ endTime?: number;
51
+ usage?: TokenUsage;
52
+ items: TimelineItem[];
53
+ }
54
+ export interface TurnModel {
55
+ turn: number;
56
+ startSeq: number;
57
+ endSeq?: number;
58
+ startTime: number;
59
+ endTime?: number;
60
+ endReason?: string;
61
+ steps: StepModel[];
62
+ items: TimelineItem[];
63
+ }
64
+ export interface TokenTotals {
65
+ input: number;
66
+ output: number;
67
+ cacheRead: number;
68
+ cacheWrite: number;
69
+ reasoning: number;
70
+ /** billed input = input + cacheRead + cacheWrite (disjoint counts). */
71
+ billedInput: number;
72
+ }
73
+ export interface TimelineModel {
74
+ turns: TurnModel[];
75
+ /** Flat, seq-ordered list of everything renderable. */
76
+ items: TimelineItem[];
77
+ totals: TokenTotals;
78
+ /** Per assistant-step usage samples in order, for the token strip. */
79
+ usageSamples: {
80
+ seq: number;
81
+ turn: number;
82
+ step: number;
83
+ usage: TokenUsage;
84
+ }[];
85
+ toolCalls: ToolCallInfo[];
86
+ durationMs: number;
87
+ startTime: number;
88
+ endTime: number;
89
+ eventCount: number;
90
+ /** seq of the last `session/end-seed` marker (fork/resume boundary), if any. */
91
+ seedBoundarySeq?: number;
92
+ models: string[];
93
+ /** Latest `session/title` event payload, when the log carries one. */
94
+ title?: string;
95
+ }
96
+ export declare function buildTimeline(session: ParsedSession): TimelineModel;