agent-session-replayer 0.1.0

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.
package/dist/index.cjs ADDED
@@ -0,0 +1,573 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AgentSessionReplayer: () => AgentSessionReplayer
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/AgentSessionReplayer.tsx
28
+ var import_react2 = require("react");
29
+
30
+ // src/playback.ts
31
+ var initialPlaybackState = () => ({ eventIndex: 0, revealOffset: 0, phase: "typing" });
32
+ var segmentGraphemes = (value) => {
33
+ if (typeof Intl.Segmenter === "function") return [...new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(value)].map(({ segment }) => segment);
34
+ return Array.from(value);
35
+ };
36
+ var getEventText = (event) => event.blocks.map((block) => `${block.title ? `${block.title}
37
+ ` : ""}${block.content}`).join("\n");
38
+ var getEventLength = (event) => segmentGraphemes(getEventText(event)).length;
39
+ var revealEvent = (event, offset) => {
40
+ let remaining = offset;
41
+ const blocks = [];
42
+ for (const block of event.blocks) {
43
+ if (remaining <= 0) break;
44
+ const titleParts = segmentGraphemes(block.title ?? "");
45
+ const title = titleParts.slice(0, remaining).join("");
46
+ remaining = Math.max(0, remaining - titleParts.length - (block.title ? 1 : 0));
47
+ const contentParts = segmentGraphemes(block.content);
48
+ const content = contentParts.slice(0, remaining).join("");
49
+ remaining = Math.max(0, remaining - contentParts.length - 1);
50
+ blocks.push({ ...block, title: block.title ? title : void 0, content });
51
+ }
52
+ return { ...event, blocks };
53
+ };
54
+ function playbackReducer(state, action) {
55
+ if (action.type === "RESTART") return initialPlaybackState();
56
+ if (action.eventIndex !== state.eventIndex) return state;
57
+ if (action.type === "FINISH_COLLAPSE") return state.phase === "collapsing" ? { ...state, phase: "between-events" } : state;
58
+ if (action.type === "ADVANCE_EVENT") return state.phase === "between-events" ? { eventIndex: state.eventIndex + 1, revealOffset: 0, phase: "typing" } : state;
59
+ if (state.phase !== "typing") return state;
60
+ const event = action.item.events[state.eventIndex];
61
+ const length = getEventLength(event);
62
+ const revealOffset = action.type === "COMPLETE_EVENT" ? length : Math.min(length, state.revealOffset + action.amount);
63
+ if (revealOffset < length) return { ...state, revealOffset };
64
+ return { ...state, revealOffset, phase: state.eventIndex === action.item.events.length - 1 ? "case-complete" : "collapsing" };
65
+ }
66
+
67
+ // src/validation.ts
68
+ var import_zod = require("zod");
69
+ var nonEmpty = import_zod.z.string().min(1);
70
+ var actorSchema = import_zod.z.enum(["implementer", "reviewer"]);
71
+ var eventTypeSchema = import_zod.z.enum([
72
+ "task_received",
73
+ "plan",
74
+ "patch",
75
+ "review_request",
76
+ "review_start",
77
+ "blocking_finding",
78
+ "revision",
79
+ "verification",
80
+ "approval"
81
+ ]);
82
+ var blockKindSchema = import_zod.z.enum([
83
+ "message",
84
+ "code",
85
+ "tool_call",
86
+ "tool_output",
87
+ "finding",
88
+ "patch",
89
+ "git_diff",
90
+ "status",
91
+ "result"
92
+ ]);
93
+ var agentSchema = import_zod.z.object({
94
+ id: nonEmpty,
95
+ name: nonEmpty,
96
+ role: nonEmpty,
97
+ context: nonEmpty
98
+ }).strict();
99
+ var blockSchema = import_zod.z.object({
100
+ id: nonEmpty,
101
+ kind: blockKindSchema,
102
+ title: nonEmpty.optional(),
103
+ content: nonEmpty,
104
+ language: nonEmpty.optional()
105
+ }).strict();
106
+ var eventSchema = import_zod.z.object({
107
+ id: nonEmpty,
108
+ type: eventTypeSchema,
109
+ actor: actorSchema,
110
+ title: nonEmpty,
111
+ summary: nonEmpty,
112
+ blocks: import_zod.z.array(blockSchema).min(1).describe("Block IDs must be unique within each event. The runtime parser enforces this constraint.")
113
+ }).strict().superRefine((event, context) => {
114
+ if (new Set(event.blocks.map(({ id }) => id)).size !== event.blocks.length) {
115
+ context.addIssue({
116
+ code: "custom",
117
+ path: ["blocks"],
118
+ message: "Block IDs must be unique within an event"
119
+ });
120
+ }
121
+ });
122
+ var sessionSchema = import_zod.z.object({
123
+ id: nonEmpty,
124
+ title: nonEmpty,
125
+ summary: nonEmpty,
126
+ repository: nonEmpty,
127
+ branch: nonEmpty,
128
+ events: import_zod.z.array(eventSchema).min(1).describe("Event IDs must be unique within each case. The runtime parser enforces this constraint.")
129
+ }).strict().superRefine((session, context) => {
130
+ if (new Set(session.events.map(({ id }) => id)).size !== session.events.length) {
131
+ context.addIssue({
132
+ code: "custom",
133
+ path: ["events"],
134
+ message: "Event IDs must be unique within a case"
135
+ });
136
+ }
137
+ });
138
+ var colorsSchema = import_zod.z.object({
139
+ background: import_zod.z.string().optional(),
140
+ surface: import_zod.z.string().optional(),
141
+ border: import_zod.z.string().optional(),
142
+ text: import_zod.z.string().optional(),
143
+ muted: import_zod.z.string().optional(),
144
+ implementer: import_zod.z.string().optional(),
145
+ reviewer: import_zod.z.string().optional(),
146
+ success: import_zod.z.string().optional(),
147
+ danger: import_zod.z.string().optional(),
148
+ focus: import_zod.z.string().optional()
149
+ }).strict();
150
+ var replayContentShape = {
151
+ agents: import_zod.z.object({ implementer: agentSchema, reviewer: agentSchema }).strict(),
152
+ cases: import_zod.z.array(sessionSchema).min(1).describe("Case IDs must be unique across this array. The runtime parser enforces this constraint.")
153
+ };
154
+ function addReplayContentIssues(value, context) {
155
+ if (new Set(value.cases.map(({ id }) => id)).size !== value.cases.length) {
156
+ context.addIssue({ code: "custom", path: ["cases"], message: "Case IDs must be unique" });
157
+ }
158
+ }
159
+ var replayContentSchema = import_zod.z.object(replayContentShape).strict().superRefine(addReplayContentIssues);
160
+ var propsSchema = import_zod.z.object({
161
+ ...replayContentShape,
162
+ typingSpeed: import_zod.z.number().finite().positive(),
163
+ eventDelayMs: import_zod.z.number().finite().nonnegative(),
164
+ height: import_zod.z.number({ error: "height must be greater than zero." }).refine((value) => Number.isFinite(value) && value > 0, "height must be greater than zero."),
165
+ colors: colorsSchema.optional(),
166
+ caseIndex: import_zod.z.number().int().nonnegative().optional(),
167
+ initialCaseIndex: import_zod.z.number().int().nonnegative(),
168
+ className: import_zod.z.string().optional(),
169
+ onCaseChange: import_zod.z.function().optional(),
170
+ onEventStart: import_zod.z.function().optional(),
171
+ onEventComplete: import_zod.z.function().optional(),
172
+ onCaseComplete: import_zod.z.function().optional()
173
+ }).strict().superRefine((props, context) => {
174
+ addReplayContentIssues(props, context);
175
+ for (const key of ["caseIndex", "initialCaseIndex"]) {
176
+ const value = props[key];
177
+ if (value !== void 0 && value >= props.cases.length) {
178
+ context.addIssue({
179
+ code: "custom",
180
+ path: [key],
181
+ message: `${key} must reference an available case`
182
+ });
183
+ }
184
+ }
185
+ });
186
+ function formatPath(path) {
187
+ const formatted = path.reduce((result, part) => typeof part === "number" ? `${result}[${part}]` : result ? `${result}.${String(part)}` : String(part), "");
188
+ return formatted || "$";
189
+ }
190
+ function parseReplayerProps(value) {
191
+ const result = propsSchema.safeParse(value);
192
+ if (result.success) return result.data;
193
+ const details = result.error.issues.map((issue) => `${formatPath(issue.path)}: ${issue.message}`).join("; ");
194
+ throw new Error(`AgentSessionReplayer received invalid props: ${details}`);
195
+ }
196
+
197
+ // src/GitDiffBlock.tsx
198
+ var import_react = require("react");
199
+ var import_jsx_runtime = require("react/jsx-runtime");
200
+ var METADATA_PREFIXES = [
201
+ "diff --git ",
202
+ "index ",
203
+ "new file mode ",
204
+ "deleted file mode ",
205
+ "similarity index ",
206
+ "rename from ",
207
+ "rename to ",
208
+ "--- ",
209
+ "+++ ",
210
+ "\"
211
+ ];
212
+ function classifyGitDiffLine(line) {
213
+ if (METADATA_PREFIXES.some((prefix) => line.startsWith(prefix))) return "metadata";
214
+ if (line.startsWith("@@")) return "hunk";
215
+ if (line.startsWith("+")) return "addition";
216
+ if (line.startsWith("-")) return "removal";
217
+ return "context";
218
+ }
219
+ function GitDiffBlock({ content }) {
220
+ const lines = content.split("\n");
221
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { className: "asr-git-diff", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: lines.map((line, index) => {
222
+ const kind = classifyGitDiffLine(line);
223
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react.Fragment, { children: [
224
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `asr-git-diff-line asr-git-diff-line--${kind}`, children: line }),
225
+ index < lines.length - 1 ? "\n" : null
226
+ ] }, index);
227
+ }) }) });
228
+ }
229
+
230
+ // src/AgentSessionReplayer.tsx
231
+ var import_jsx_runtime2 = require("react/jsx-runtime");
232
+ var DEFAULT_TYPING_SPEED = 110;
233
+ var DEFAULT_EVENT_DELAY = 500;
234
+ var COLLAPSE_DURATION_MS = 220;
235
+ var COLLAPSE_EASING = "cubic-bezier(0.23, 1, 0.32, 1)";
236
+ var colorVariables = {
237
+ background: "--asr-background",
238
+ surface: "--asr-surface",
239
+ border: "--asr-border",
240
+ text: "--asr-text",
241
+ muted: "--asr-muted",
242
+ implementer: "--asr-implementer",
243
+ reviewer: "--asr-reviewer",
244
+ success: "--asr-success",
245
+ danger: "--asr-danger",
246
+ focus: "--asr-focus"
247
+ };
248
+ function usePrefersReducedMotion() {
249
+ const [reduced, setReduced] = (0, import_react2.useState)(false);
250
+ (0, import_react2.useEffect)(() => {
251
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
252
+ const query = window.matchMedia("(prefers-reduced-motion: reduce)");
253
+ const update = () => setReduced(query.matches);
254
+ update();
255
+ query.addEventListener?.("change", update);
256
+ return () => query.removeEventListener?.("change", update);
257
+ }, []);
258
+ return reduced;
259
+ }
260
+ function Agent({ actor, active, agent }) {
261
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `asr-agent asr-agent--${actor} ${active ? "asr-is-active" : ""}`, children: [
262
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "asr-agent-avatar", "aria-hidden": "true", children: "\u2723" }),
263
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
264
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: agent.name }),
265
+ " ",
266
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: agent.role }),
267
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
268
+ "its context: ",
269
+ agent.context
270
+ ] })
271
+ ] })
272
+ ] });
273
+ }
274
+ function Block({ block }) {
275
+ const codeLike = block.kind === "code" || block.kind === "patch" || block.kind === "tool_output";
276
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: `asr-chat-block asr-chat-block--${block.kind}`, children: [
277
+ block.title && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("header", { children: [
278
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: block.kind.replace("_", " ") }),
279
+ block.title
280
+ ] }),
281
+ block.kind === "git_diff" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(GitDiffBlock, { content: block.content }) : codeLike ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { children: block.content }) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: block.content })
282
+ ] });
283
+ }
284
+ function ExpandedEvent({ event, visibleEvent, typing }) {
285
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("article", { className: `asr-expanded-event asr-expanded-event--${event.actor}`, "aria-label": event.title, children: [
286
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "asr-event-heading", children: [
287
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: event.actor === "implementer" ? "\u2723 claude" : "adversarial reviewer \u2723" }),
288
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: event.title })
289
+ ] }),
290
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "asr-blocks", children: visibleEvent.blocks.map((block) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Block, { block }, block.id)) }),
291
+ typing && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "asr-typing-caret", "aria-hidden": "true" })
292
+ ] });
293
+ }
294
+ function EventSummaryContent({ event, index }) {
295
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
296
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: String(index + 1).padStart(2, "0") }),
297
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: event.title }),
298
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: event.summary }),
299
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "asr-disclosure", "aria-hidden": "true", children: "\u2304" })
300
+ ] });
301
+ }
302
+ function EventSummaryRow({ event, index, contentId, onToggle, className = "" }) {
303
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
304
+ "button",
305
+ {
306
+ type: "button",
307
+ className: `asr-event-summary-row asr-event-summary-row--${event.actor}${className ? ` ${className}` : ""}`,
308
+ "aria-expanded": "false",
309
+ "aria-controls": contentId,
310
+ "aria-label": `Expand ${event.title}`,
311
+ onClick: onToggle,
312
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EventSummaryContent, { event, index })
313
+ }
314
+ );
315
+ }
316
+ function CompletedEvent({ event, index, expanded, onToggle }) {
317
+ const contentId = `asr-event-${event.id}`;
318
+ if (!expanded) {
319
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "asr-completed-event", children: [
320
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EventSummaryRow, { event, index, contentId, onToggle }),
321
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { id: contentId, hidden: true })
322
+ ] });
323
+ }
324
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("article", { className: `asr-expanded-event asr-expanded-event--${event.actor}`, "aria-label": event.title, children: [
325
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
326
+ "button",
327
+ {
328
+ type: "button",
329
+ className: "asr-expanded-toggle",
330
+ "aria-expanded": "true",
331
+ "aria-controls": contentId,
332
+ "aria-label": `Collapse ${event.title}`,
333
+ onClick: onToggle,
334
+ children: [
335
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: event.actor === "implementer" ? "\u2723 claude" : "adversarial reviewer \u2723" }),
336
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: event.title }),
337
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "asr-disclosure", "aria-hidden": "true", children: "\u2303" })
338
+ ]
339
+ }
340
+ ),
341
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "asr-blocks", id: contentId, children: event.blocks.map((block) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Block, { block }, block.id)) })
342
+ ] });
343
+ }
344
+ function CollapsingEvent({
345
+ event,
346
+ index,
347
+ reduceMotion,
348
+ onComplete
349
+ }) {
350
+ const shellRef = (0, import_react2.useRef)(null);
351
+ const expandedRef = (0, import_react2.useRef)(null);
352
+ const summaryRef = (0, import_react2.useRef)(null);
353
+ (0, import_react2.useEffect)(() => {
354
+ const shell = shellRef.current;
355
+ const expanded = expandedRef.current;
356
+ const summary = summaryRef.current;
357
+ if (!shell || !expanded || !summary) return;
358
+ if (reduceMotion || typeof shell.animate !== "function") {
359
+ onComplete();
360
+ return;
361
+ }
362
+ let cancelled = false;
363
+ const options = {
364
+ duration: COLLAPSE_DURATION_MS,
365
+ easing: COLLAPSE_EASING,
366
+ fill: "forwards"
367
+ };
368
+ const shellAnimation = shell.animate([
369
+ { height: `${shell.scrollHeight}px` },
370
+ { height: "46px" }
371
+ ], options);
372
+ const expandedAnimation = expanded.animate([
373
+ { opacity: 1, transform: "translateY(0)" },
374
+ { opacity: 0, transform: "translateY(-6px)" }
375
+ ], options);
376
+ const summaryAnimation = summary.animate([
377
+ { opacity: 0, transform: "translateY(6px)" },
378
+ { opacity: 1, transform: "translateY(0)" }
379
+ ], options);
380
+ void shellAnimation.finished.then(() => {
381
+ if (!cancelled) onComplete();
382
+ }).catch(() => void 0);
383
+ return () => {
384
+ cancelled = true;
385
+ shellAnimation.cancel();
386
+ expandedAnimation.cancel();
387
+ summaryAnimation.cancel();
388
+ };
389
+ }, [event.id, onComplete, reduceMotion]);
390
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "asr-collapse-shell", ref: shellRef, children: [
391
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "asr-collapse-expanded", ref: expandedRef, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ExpandedEvent, { event, visibleEvent: event, typing: false }) }),
392
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "asr-collapse-summary", ref: summaryRef, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: `asr-event-summary-row asr-event-summary-row--${event.actor}`, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EventSummaryContent, { event, index }) }) })
393
+ ] });
394
+ }
395
+ function AgentSessionReplayer(props) {
396
+ const {
397
+ agents,
398
+ cases,
399
+ typingSpeed,
400
+ eventDelayMs,
401
+ height,
402
+ colors,
403
+ caseIndex,
404
+ initialCaseIndex,
405
+ className,
406
+ onCaseChange,
407
+ onEventStart,
408
+ onEventComplete,
409
+ onCaseComplete
410
+ } = parseReplayerProps({
411
+ ...props,
412
+ typingSpeed: props.typingSpeed === void 0 ? DEFAULT_TYPING_SPEED : props.typingSpeed,
413
+ eventDelayMs: props.eventDelayMs === void 0 ? DEFAULT_EVENT_DELAY : props.eventDelayMs,
414
+ height: props.height === void 0 ? 720 : props.height,
415
+ initialCaseIndex: props.initialCaseIndex === void 0 ? 0 : props.initialCaseIndex
416
+ });
417
+ const controlled = caseIndex !== void 0;
418
+ const [internalCaseIndex, setInternalCaseIndex] = (0, import_react2.useState)(initialCaseIndex);
419
+ const resolvedCaseIndex = controlled ? caseIndex : internalCaseIndex;
420
+ const [state, dispatch] = (0, import_react2.useReducer)(playbackReducer, void 0, initialPlaybackState);
421
+ const [generation, bumpGeneration] = (0, import_react2.useReducer)((value) => value + 1, 0);
422
+ const [expandedEventIds, setExpandedEventIds] = (0, import_react2.useState)(() => /* @__PURE__ */ new Set());
423
+ const reduceMotion = usePrefersReducedMotion();
424
+ const transcriptRef = (0, import_react2.useRef)(null);
425
+ const started = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
426
+ const completed = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
427
+ const completedCases = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
428
+ const previousCase = (0, import_react2.useRef)(resolvedCaseIndex);
429
+ const activeCase = cases[resolvedCaseIndex];
430
+ const caseChanged = previousCase.current !== resolvedCaseIndex;
431
+ const eventIndex = caseChanged ? 0 : state.eventIndex;
432
+ const revealOffset = caseChanged ? 0 : state.revealOffset;
433
+ const phase = caseChanged ? "typing" : state.phase;
434
+ const event = activeCase.events[eventIndex];
435
+ const visibleEvent = (0, import_react2.useMemo)(() => revealEvent(event, revealOffset), [event, revealOffset]);
436
+ const finishCollapse = (0, import_react2.useCallback)(() => {
437
+ dispatch({ type: "FINISH_COLLAPSE", eventIndex });
438
+ }, [eventIndex]);
439
+ (0, import_react2.useEffect)(() => {
440
+ if (previousCase.current === resolvedCaseIndex) return;
441
+ previousCase.current = resolvedCaseIndex;
442
+ setExpandedEventIds(/* @__PURE__ */ new Set());
443
+ bumpGeneration();
444
+ dispatch({ type: "RESTART" });
445
+ }, [resolvedCaseIndex]);
446
+ (0, import_react2.useEffect)(() => {
447
+ if (caseChanged) return;
448
+ const key = `${generation}:${activeCase.id}:${event.id}`;
449
+ if (!started.current.has(key)) {
450
+ started.current.add(key);
451
+ onEventStart?.(event, activeCase);
452
+ }
453
+ }, [activeCase, caseChanged, event, generation, onEventStart]);
454
+ (0, import_react2.useEffect)(() => {
455
+ if (caseChanged) return;
456
+ if (phase === "typing") {
457
+ if (reduceMotion) {
458
+ dispatch({ type: "COMPLETE_EVENT", item: activeCase, eventIndex });
459
+ return;
460
+ }
461
+ const timer = window.setTimeout(() => dispatch({ type: "TICK", item: activeCase, eventIndex, amount: 1 }), 1e3 / typingSpeed);
462
+ return () => window.clearTimeout(timer);
463
+ }
464
+ if (phase === "between-events") {
465
+ const timer = window.setTimeout(() => dispatch({ type: "ADVANCE_EVENT", eventIndex }), eventDelayMs);
466
+ return () => window.clearTimeout(timer);
467
+ }
468
+ }, [activeCase, caseChanged, eventDelayMs, eventIndex, phase, reduceMotion, revealOffset, typingSpeed]);
469
+ (0, import_react2.useEffect)(() => {
470
+ if (phase === "typing") return;
471
+ const key = `${generation}:${activeCase.id}:${event.id}`;
472
+ if (!completed.current.has(key)) {
473
+ completed.current.add(key);
474
+ onEventComplete?.(event, activeCase);
475
+ }
476
+ if (phase === "case-complete") {
477
+ const caseKey = `${generation}:${activeCase.id}`;
478
+ if (!completedCases.current.has(caseKey)) {
479
+ completedCases.current.add(caseKey);
480
+ onCaseComplete?.(activeCase);
481
+ }
482
+ }
483
+ }, [activeCase, event, generation, onCaseComplete, onEventComplete, phase]);
484
+ (0, import_react2.useEffect)(() => {
485
+ const transcript = transcriptRef.current;
486
+ if (!transcript || transcript.scrollHeight <= transcript.clientHeight) return;
487
+ transcript.scrollTo({ top: transcript.scrollHeight, behavior: reduceMotion ? "auto" : "smooth" });
488
+ const bottomGap = transcript.scrollHeight - transcript.clientHeight - transcript.scrollTop;
489
+ if (bottomGap > 1) transcript.scrollTop = transcript.scrollHeight;
490
+ }, [eventIndex, reduceMotion, resolvedCaseIndex, revealOffset, visibleEvent.blocks.length]);
491
+ const requestCase = (nextIndex) => {
492
+ if (nextIndex === resolvedCaseIndex || nextIndex < 0 || nextIndex >= cases.length) return;
493
+ if (!controlled) {
494
+ setInternalCaseIndex(nextIndex);
495
+ dispatch({ type: "RESTART" });
496
+ }
497
+ onCaseChange?.(nextIndex, cases[nextIndex]);
498
+ };
499
+ const restart = () => {
500
+ setExpandedEventIds(/* @__PURE__ */ new Set());
501
+ bumpGeneration();
502
+ dispatch({ type: "RESTART" });
503
+ };
504
+ const toggleEvent = (eventId) => {
505
+ setExpandedEventIds((current) => {
506
+ const next = new Set(current);
507
+ if (next.has(eventId)) next.delete(eventId);
508
+ else next.add(eventId);
509
+ return next;
510
+ });
511
+ };
512
+ const style = {
513
+ ...Object.fromEntries(Object.entries(colors ?? {}).map(([key, value]) => [colorVariables[key], value])),
514
+ "--asr-height": `${height}px`,
515
+ height: `${height}px`
516
+ };
517
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { "data-agent-session-replayer": true, className: `asr-root${className ? ` ${className}` : ""}`, style, "aria-label": "Two-agent review conversation", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "asr-chat-stage", children: [
518
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("header", { className: "asr-frame-header", children: [
519
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "asr-workflow-line", children: [
520
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "asr-brand", children: [
521
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "\u2723" }),
522
+ " Claude Code \xB7 Dynamic workflow"
523
+ ] }),
524
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "asr-review-label", children: "Adversarial review" })
525
+ ] }),
526
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "asr-case-summary", children: [
527
+ cases.length,
528
+ " bugs caught by adversarial review before merge"
529
+ ] })
530
+ ] }),
531
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "asr-case-nav", children: [
532
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
533
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "asr-case-progress", children: [
534
+ "Case ",
535
+ resolvedCaseIndex + 1,
536
+ " of ",
537
+ cases.length
538
+ ] }),
539
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { className: "asr-case-title", children: activeCase.title }),
540
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "asr-repository", children: [
541
+ activeCase.repository,
542
+ " \xB7 ",
543
+ activeCase.branch,
544
+ " \xB7 ",
545
+ phase === "case-complete" ? "Case complete" : `Message ${eventIndex + 1} of ${activeCase.events.length}`
546
+ ] })
547
+ ] }),
548
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "asr-controls", "aria-label": "Playback controls", children: [
549
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", onClick: () => requestCase(resolvedCaseIndex - 1), disabled: resolvedCaseIndex === 0, "aria-label": "Previous case", children: "\u2190" }),
550
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", onClick: restart, "aria-label": "Restart case", children: "Restart" }),
551
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "asr-next-button", onClick: () => requestCase(resolvedCaseIndex + 1), disabled: resolvedCaseIndex === cases.length - 1, children: "Next case \u2192" })
552
+ ] })
553
+ ] }),
554
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "asr-agent-row", children: [
555
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Agent, { actor: "implementer", active: event.actor === "implementer", agent: agents.implementer }),
556
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Agent, { actor: "reviewer", active: event.actor === "reviewer", agent: agents.reviewer })
557
+ ] }),
558
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "asr-transcript", ref: transcriptRef, children: [
559
+ activeCase.events.slice(0, eventIndex).map((past, index) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CompletedEvent, { event: past, index, expanded: expandedEventIds.has(past.id), onToggle: () => toggleEvent(past.id) }, past.id)),
560
+ phase === "collapsing" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CollapsingEvent, { event, index: eventIndex, reduceMotion, onComplete: finishCollapse }) : phase === "between-events" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CompletedEvent, { event, index: eventIndex, expanded: expandedEventIds.has(event.id), onToggle: () => toggleEvent(event.id) }, event.id) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ExpandedEvent, { event, visibleEvent, typing: phase === "typing" }) })
561
+ ] }),
562
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "asr-live-status", "aria-live": "polite", children: phase === "case-complete" ? `${activeCase.title} complete.` : `Autoplaying ${event.title}.` }),
563
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("footer", { className: "asr-footer", children: [
564
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Scripted playback: each reviewer receives only the diff and is told to find how it is wrong. No live model is running." }),
565
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("a", { className: "asr-watermark", href: "https://devos.ing", target: "_blank", rel: "noopener noreferrer", children: "devos" })
566
+ ] })
567
+ ] }) });
568
+ }
569
+ // Annotate the CommonJS export names for ESM import in node:
570
+ 0 && (module.exports = {
571
+ AgentSessionReplayer
572
+ });
573
+ //# sourceMappingURL=index.cjs.map