@athenaintel/react 0.39.0 → 0.40.1

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 (126) hide show
  1. package/README.md +39 -3
  2. package/dist/agent-runtime/src/errors.cjs +8 -0
  3. package/dist/agent-runtime/src/errors.cjs.map +1 -1
  4. package/dist/agent-runtime/src/errors.js +8 -0
  5. package/dist/agent-runtime/src/errors.js.map +1 -1
  6. package/dist/agent-runtime/src/messages.cjs +76 -16
  7. package/dist/agent-runtime/src/messages.cjs.map +1 -1
  8. package/dist/agent-runtime/src/messages.js +76 -16
  9. package/dist/agent-runtime/src/messages.js.map +1 -1
  10. package/dist/agent-runtime/src/tool-metadata.cjs +15 -0
  11. package/dist/agent-runtime/src/tool-metadata.cjs.map +1 -1
  12. package/dist/agent-runtime/src/tool-metadata.js +15 -0
  13. package/dist/agent-runtime/src/tool-metadata.js.map +1 -1
  14. package/dist/assets/use-asset-content.cjs +127 -0
  15. package/dist/assets/use-asset-content.cjs.map +1 -0
  16. package/dist/assets/use-asset-content.d.ts +60 -0
  17. package/dist/assets/use-asset-content.js +127 -0
  18. package/dist/assets/use-asset-content.js.map +1 -0
  19. package/dist/chat/AthenaChat.cjs +24 -10
  20. package/dist/chat/AthenaChat.cjs.map +1 -1
  21. package/dist/chat/AthenaChat.js +25 -11
  22. package/dist/chat/AthenaChat.js.map +1 -1
  23. package/dist/chat/run-activity.cjs +29 -10
  24. package/dist/chat/run-activity.cjs.map +1 -1
  25. package/dist/chat/run-activity.d.ts +30 -4
  26. package/dist/chat/run-activity.js +29 -10
  27. package/dist/chat/run-activity.js.map +1 -1
  28. package/dist/chat/super-grouping/CollapsibleGroup.cjs +16 -8
  29. package/dist/chat/super-grouping/CollapsibleGroup.cjs.map +1 -1
  30. package/dist/chat/super-grouping/CollapsibleGroup.d.ts +26 -1
  31. package/dist/chat/super-grouping/CollapsibleGroup.js +16 -8
  32. package/dist/chat/super-grouping/CollapsibleGroup.js.map +1 -1
  33. package/dist/chat/super-grouping/GroupedMessageParts.cjs +59 -0
  34. package/dist/chat/super-grouping/GroupedMessageParts.cjs.map +1 -0
  35. package/dist/chat/super-grouping/GroupedMessageParts.d.ts +25 -0
  36. package/dist/chat/super-grouping/GroupedMessageParts.js +59 -0
  37. package/dist/chat/super-grouping/GroupedMessageParts.js.map +1 -0
  38. package/dist/chat/super-grouping/SuperGroupingCard.cjs +9 -5
  39. package/dist/chat/super-grouping/SuperGroupingCard.cjs.map +1 -1
  40. package/dist/chat/super-grouping/SuperGroupingCard.d.ts +2 -1
  41. package/dist/chat/super-grouping/SuperGroupingCard.js +13 -9
  42. package/dist/chat/super-grouping/SuperGroupingCard.js.map +1 -1
  43. package/dist/chat/super-grouping/SuperGroupingTranscript.cjs +10 -6
  44. package/dist/chat/super-grouping/SuperGroupingTranscript.cjs.map +1 -1
  45. package/dist/chat/super-grouping/SuperGroupingTranscript.js +14 -10
  46. package/dist/chat/super-grouping/SuperGroupingTranscript.js.map +1 -1
  47. package/dist/chat/super-grouping/grouped-part-keys.cjs +104 -0
  48. package/dist/chat/super-grouping/grouped-part-keys.cjs.map +1 -0
  49. package/dist/chat/super-grouping/grouped-part-keys.d.ts +37 -0
  50. package/dist/chat/super-grouping/grouped-part-keys.js +104 -0
  51. package/dist/chat/super-grouping/grouped-part-keys.js.map +1 -0
  52. package/dist/chat/super-grouping/index.cjs +2 -0
  53. package/dist/chat/super-grouping/index.cjs.map +1 -1
  54. package/dist/chat/super-grouping/index.d.ts +3 -1
  55. package/dist/chat/super-grouping/index.js +2 -0
  56. package/dist/chat/super-grouping/index.js.map +1 -1
  57. package/dist/chat-ui/composer/tiptap-composer.cjs +25 -19
  58. package/dist/chat-ui/composer/tiptap-composer.cjs.map +1 -1
  59. package/dist/chat-ui/composer/tiptap-composer.d.ts +5 -0
  60. package/dist/chat-ui/composer/tiptap-composer.js +26 -20
  61. package/dist/chat-ui/composer/tiptap-composer.js.map +1 -1
  62. package/dist/composer/clipboard.cjs +16 -0
  63. package/dist/composer/clipboard.cjs.map +1 -1
  64. package/dist/composer/clipboard.d.ts +9 -0
  65. package/dist/composer/clipboard.js +17 -1
  66. package/dist/composer/clipboard.js.map +1 -1
  67. package/dist/diagnostics/errors.cjs +6 -0
  68. package/dist/diagnostics/errors.cjs.map +1 -1
  69. package/dist/diagnostics/errors.d.ts +6 -0
  70. package/dist/diagnostics/errors.js +6 -0
  71. package/dist/diagnostics/errors.js.map +1 -1
  72. package/dist/index.cjs +23 -0
  73. package/dist/index.cjs.map +1 -1
  74. package/dist/index.d.ts +8 -1
  75. package/dist/index.js +23 -0
  76. package/dist/index.js.map +1 -1
  77. package/dist/lib/use-document-active.cjs +40 -0
  78. package/dist/lib/use-document-active.cjs.map +1 -0
  79. package/dist/lib/use-document-active.d.ts +9 -0
  80. package/dist/lib/use-document-active.js +40 -0
  81. package/dist/lib/use-document-active.js.map +1 -0
  82. package/dist/react/package.json.cjs +1 -1
  83. package/dist/react/package.json.js +1 -1
  84. package/dist/runtime/statewire-lifecycle.cjs +8 -0
  85. package/dist/runtime/statewire-lifecycle.cjs.map +1 -1
  86. package/dist/runtime/statewire-lifecycle.js +9 -1
  87. package/dist/runtime/statewire-lifecycle.js.map +1 -1
  88. package/dist/styles.css +1 -1
  89. package/dist/threads/Inbox.cjs +6 -2
  90. package/dist/threads/Inbox.cjs.map +1 -1
  91. package/dist/threads/Inbox.js +6 -2
  92. package/dist/threads/Inbox.js.map +1 -1
  93. package/dist/threads/mark-viewed-thread-read.cjs +97 -0
  94. package/dist/threads/mark-viewed-thread-read.cjs.map +1 -0
  95. package/dist/threads/mark-viewed-thread-read.d.ts +7 -0
  96. package/dist/threads/mark-viewed-thread-read.js +97 -0
  97. package/dist/threads/mark-viewed-thread-read.js.map +1 -0
  98. package/dist/tools/index.cjs +7 -0
  99. package/dist/tools/index.cjs.map +1 -1
  100. package/dist/tools/index.d.ts +2 -0
  101. package/dist/tools/index.js +7 -0
  102. package/dist/tools/index.js.map +1 -1
  103. package/dist/tools/inline-tools.cjs +10 -0
  104. package/dist/tools/inline-tools.cjs.map +1 -0
  105. package/dist/tools/inline-tools.d.ts +2 -0
  106. package/dist/tools/inline-tools.js +10 -0
  107. package/dist/tools/inline-tools.js.map +1 -0
  108. package/dist/tools/test-recording-report.cjs +175 -0
  109. package/dist/tools/test-recording-report.cjs.map +1 -0
  110. package/dist/tools/test-recording-report.d.ts +82 -0
  111. package/dist/tools/test-recording-report.js +175 -0
  112. package/dist/tools/test-recording-report.js.map +1 -0
  113. package/dist/tools/test-recording-tool-uis.cjs +498 -0
  114. package/dist/tools/test-recording-tool-uis.cjs.map +1 -0
  115. package/dist/tools/test-recording-tool-uis.d.ts +32 -0
  116. package/dist/tools/test-recording-tool-uis.js +498 -0
  117. package/dist/tools/test-recording-tool-uis.js.map +1 -0
  118. package/dist/tools/tool-summary.cjs +49 -1
  119. package/dist/tools/tool-summary.cjs.map +1 -1
  120. package/dist/tools/tool-summary.js +49 -1
  121. package/dist/tools/tool-summary.js.map +1 -1
  122. package/dist/tools/tool-uis.cjs +6 -1
  123. package/dist/tools/tool-uis.cjs.map +1 -1
  124. package/dist/tools/tool-uis.js +6 -1
  125. package/dist/tools/tool-uis.js.map +1 -1
  126. package/package.json +1 -1
@@ -0,0 +1,498 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { ClapperboardIcon, ExternalLinkIcon, StickyNoteIcon, XIcon, CheckIcon, FlaskConicalIcon, AlertCircleIcon, LoaderIcon, CircleDotIcon, PlayIcon, CircleDashedIcon } from "lucide-react";
3
+ import { memo, useState, useCallback, useContext, useMemo, useRef, useEffect } from "react";
4
+ import { useAthenaAssetContent, useObjectUrl } from "../assets/use-asset-content.js";
5
+ import { AthenaContext } from "../provider/AthenaContext.js";
6
+ import { ToolCard } from "./primitives.js";
7
+ import { parseTestRecordingStopResult, describeTestRecordingMark, formatRecordingClock, describeTestRecordingStart } from "./test-recording-report.js";
8
+ import { cn } from "cnfast";
9
+ function argsRecord(args) {
10
+ return typeof args === "object" && args !== null && !Array.isArray(args) ? args : void 0;
11
+ }
12
+ function argString(args, key) {
13
+ const value = args == null ? void 0 : args[key];
14
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
15
+ }
16
+ function incompleteMessage(status, fallback) {
17
+ if ((status == null ? void 0 : status.type) !== "incomplete") return null;
18
+ if (status.reason === "cancelled") return "Cancelled";
19
+ if (typeof status.error === "string" && status.error.trim() !== "") return status.error;
20
+ return fallback;
21
+ }
22
+ const IDLE_PLAYBACK = { requested: false, seek: null };
23
+ const HAVE_METADATA = 1;
24
+ function playbackErrorMessage(error) {
25
+ const status = error.status;
26
+ if (status === 401 || status === 403) return "You don't have access to this recording.";
27
+ if (status === 404) return "The recording's video was not found.";
28
+ return "The recording could not be loaded.";
29
+ }
30
+ function TestRecordingPlayer({
31
+ assetId,
32
+ title,
33
+ durationS,
34
+ url,
35
+ playback,
36
+ onPlay
37
+ }) {
38
+ var _a, _b;
39
+ const content = useAthenaAssetContent({ assetId, enabled: playback.requested });
40
+ const src = useObjectUrl(playback.requested ? content.data : void 0);
41
+ const { refetch } = content;
42
+ const retry = useCallback(() => {
43
+ void refetch();
44
+ }, [refetch]);
45
+ const videoRef = useRef(null);
46
+ const pendingSeekRef = useRef(null);
47
+ const seekTime = (_a = playback.seek) == null ? void 0 : _a.time;
48
+ const seekNonce = ((_b = playback.seek) == null ? void 0 : _b.nonce) ?? 0;
49
+ useEffect(() => {
50
+ var _a2;
51
+ const video = videoRef.current;
52
+ if (!video || !src) return;
53
+ if (seekTime !== void 0) {
54
+ video.currentTime = seekTime;
55
+ pendingSeekRef.current = video.readyState < HAVE_METADATA ? seekTime : null;
56
+ }
57
+ (_a2 = video.play()) == null ? void 0 : _a2.catch(() => {
58
+ });
59
+ }, [src, seekNonce]);
60
+ const handleLoadedMetadata = useCallback((event) => {
61
+ const pending = pendingSeekRef.current;
62
+ pendingSeekRef.current = null;
63
+ if (pending !== null) event.currentTarget.currentTime = pending;
64
+ }, []);
65
+ if (src) {
66
+ return /* @__PURE__ */ jsx(
67
+ "video",
68
+ {
69
+ ref: videoRef,
70
+ "data-testid": "test-recording-video",
71
+ src,
72
+ controls: true,
73
+ playsInline: true,
74
+ preload: "auto",
75
+ "aria-label": `Recording: ${title}`,
76
+ onLoadedMetadata: handleLoadedMetadata,
77
+ className: "block max-h-[480px] w-full bg-black"
78
+ }
79
+ );
80
+ }
81
+ if (content.isError && !content.isFetching) {
82
+ return /* @__PURE__ */ jsxs(
83
+ "div",
84
+ {
85
+ role: "alert",
86
+ "data-testid": "test-recording-video-error",
87
+ className: "flex aspect-video w-full flex-col items-center justify-center gap-2 bg-muted/40 px-6 text-center",
88
+ children: [
89
+ /* @__PURE__ */ jsx(AlertCircleIcon, { className: "size-5 text-destructive", "aria-hidden": true }),
90
+ /* @__PURE__ */ jsx("p", { className: "text-[12.5px] text-foreground", children: playbackErrorMessage(content.error) }),
91
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
92
+ /* @__PURE__ */ jsx(
93
+ "button",
94
+ {
95
+ type: "button",
96
+ onClick: retry,
97
+ className: "rounded-md border border-border bg-background px-2.5 py-1 text-[12px] font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
98
+ children: "Try again"
99
+ }
100
+ ),
101
+ url ? /* @__PURE__ */ jsx(
102
+ "a",
103
+ {
104
+ href: url,
105
+ target: "_blank",
106
+ rel: "noopener noreferrer",
107
+ className: "text-[12px] font-medium text-primary hover:underline",
108
+ children: "Watch in Athena"
109
+ }
110
+ ) : null
111
+ ] })
112
+ ]
113
+ }
114
+ );
115
+ }
116
+ if (playback.requested) {
117
+ return /* @__PURE__ */ jsxs(
118
+ "output",
119
+ {
120
+ "data-testid": "test-recording-video-loading",
121
+ className: "flex aspect-video w-full items-center justify-center gap-2 bg-zinc-950 text-[12px] text-white/80",
122
+ children: [
123
+ /* @__PURE__ */ jsx(LoaderIcon, { className: "size-4 animate-spin", "aria-hidden": true }),
124
+ "Loading recording…"
125
+ ]
126
+ }
127
+ );
128
+ }
129
+ return /* @__PURE__ */ jsxs(
130
+ "button",
131
+ {
132
+ type: "button",
133
+ onClick: onPlay,
134
+ "aria-label": `Play recording: ${title}`,
135
+ className: "group relative flex aspect-video w-full items-center justify-center bg-zinc-950 text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
136
+ children: [
137
+ /* @__PURE__ */ jsx("span", { className: "flex size-12 items-center justify-center rounded-full bg-white/15 transition-colors group-hover:bg-white/25", children: /* @__PURE__ */ jsx(PlayIcon, { className: "ml-0.5 size-5 fill-current", "aria-hidden": true }) }),
138
+ durationS !== null ? /* @__PURE__ */ jsx("span", { className: "absolute right-3 bottom-2 text-[11px] tabular-nums text-white/80", children: formatRecordingClock(durationS) }) : null
139
+ ]
140
+ }
141
+ );
142
+ }
143
+ function ChapterButton({
144
+ time,
145
+ label,
146
+ onSeek
147
+ }) {
148
+ const clock = formatRecordingClock(time);
149
+ return /* @__PURE__ */ jsx(
150
+ "button",
151
+ {
152
+ type: "button",
153
+ "data-testid": "test-recording-chapter",
154
+ onClick: () => onSeek(time),
155
+ "aria-label": `Play from ${clock}: ${label}`,
156
+ className: "shrink-0 rounded px-1.5 py-0.5 font-mono text-[11px] tabular-nums text-primary transition-colors hover:bg-primary/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
157
+ children: clock
158
+ }
159
+ );
160
+ }
161
+ const OUTCOME_LABEL = {
162
+ passed: "Passed",
163
+ failed: "Failed",
164
+ untested: "Not checked"
165
+ };
166
+ function OutcomeIcon({ result }) {
167
+ if (result === "passed") {
168
+ return /* @__PURE__ */ jsx("span", { className: "mt-px flex size-4 shrink-0 items-center justify-center rounded-full bg-emerald-500 text-white", children: /* @__PURE__ */ jsx(CheckIcon, { className: "size-2.5", strokeWidth: 3.5, "aria-hidden": true }) });
169
+ }
170
+ if (result === "failed") {
171
+ return /* @__PURE__ */ jsx("span", { className: "mt-px flex size-4 shrink-0 items-center justify-center rounded-full bg-destructive text-white", children: /* @__PURE__ */ jsx(XIcon, { className: "size-2.5", strokeWidth: 3.5, "aria-hidden": true }) });
172
+ }
173
+ return /* @__PURE__ */ jsx(CircleDashedIcon, { className: "mt-px size-4 shrink-0 text-muted-foreground", "aria-hidden": true });
174
+ }
175
+ function AssertionRow({
176
+ assertion,
177
+ onSeek
178
+ }) {
179
+ const failed = assertion.result === "failed";
180
+ const text = assertion.text || (failed ? "Failed" : "Passed");
181
+ return /* @__PURE__ */ jsxs(
182
+ "li",
183
+ {
184
+ "data-testid": "test-recording-assertion",
185
+ "data-result": assertion.result,
186
+ className: cn(
187
+ "flex items-start gap-1.5 rounded-md px-1.5 py-0.5 text-[12px] leading-snug",
188
+ failed ? "bg-destructive/10 text-destructive" : "text-muted-foreground"
189
+ ),
190
+ children: [
191
+ failed ? /* @__PURE__ */ jsx(XIcon, { className: "mt-0.5 size-3 shrink-0", strokeWidth: 3, "aria-hidden": true }) : /* @__PURE__ */ jsx(CheckIcon, { className: "mt-0.5 size-3 shrink-0 text-emerald-600", strokeWidth: 3, "aria-hidden": true }),
192
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1 break-words", children: [
193
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: failed ? "Failed: " : "Passed: " }),
194
+ text
195
+ ] }),
196
+ onSeek && assertion.editedTimeS !== null ? /* @__PURE__ */ jsx(ChapterButton, { time: assertion.editedTimeS, label: text, onSeek }) : null
197
+ ]
198
+ }
199
+ );
200
+ }
201
+ function TestRow({ test, onSeek }) {
202
+ return /* @__PURE__ */ jsxs("li", { "data-testid": "test-recording-test", "data-result": test.result, className: "px-4 py-2.5", children: [
203
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
204
+ /* @__PURE__ */ jsx(OutcomeIcon, { result: test.result }),
205
+ /* @__PURE__ */ jsxs(
206
+ "span",
207
+ {
208
+ className: cn(
209
+ "min-w-0 flex-1 break-words text-[12.5px] font-medium leading-snug",
210
+ test.result === "failed" && "text-destructive",
211
+ test.result === "passed" && "text-foreground",
212
+ test.result === "untested" && "text-muted-foreground"
213
+ ),
214
+ children: [
215
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: `${OUTCOME_LABEL[test.result]}: ` }),
216
+ test.name
217
+ ]
218
+ }
219
+ ),
220
+ test.result === "untested" ? /* @__PURE__ */ jsx("span", { className: "shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground", children: "Not checked" }) : null,
221
+ onSeek && test.startTimeS !== null ? /* @__PURE__ */ jsx(ChapterButton, { time: test.startTimeS, label: test.name, onSeek }) : null
222
+ ] }),
223
+ test.assertions.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "mt-1 flex flex-col gap-0.5 pl-6", children: test.assertions.map((assertion, index) => /* @__PURE__ */ jsx(
224
+ AssertionRow,
225
+ {
226
+ assertion,
227
+ onSeek
228
+ },
229
+ index
230
+ )) }) : null
231
+ ] });
232
+ }
233
+ function TestRecordingReportCard({ report, className }) {
234
+ const [playback, setPlayback] = useState(IDLE_PLAYBACK);
235
+ const play = useCallback(() => {
236
+ setPlayback((previous) => ({ ...previous, requested: true }));
237
+ }, []);
238
+ const seek = useCallback((time) => {
239
+ setPlayback((previous) => {
240
+ var _a;
241
+ return {
242
+ requested: true,
243
+ seek: { time, nonce: (((_a = previous.seek) == null ? void 0 : _a.nonce) ?? 0) + 1 }
244
+ };
245
+ });
246
+ }, []);
247
+ const athenaConfig = useContext(AthenaContext);
248
+ const playableAssetId = athenaConfig ? report.assetId : null;
249
+ const onSeek = playableAssetId ? seek : null;
250
+ const hasFailures = report.failed > 0;
251
+ return /* @__PURE__ */ jsxs(
252
+ "section",
253
+ {
254
+ "data-testid": "test-recording-report",
255
+ "data-has-failures": hasFailures ? "true" : "false",
256
+ "aria-label": `Test recording: ${report.title}`,
257
+ className: cn(
258
+ "my-3 w-full overflow-hidden rounded-xl border border-border/60 bg-background shadow-sm",
259
+ className
260
+ ),
261
+ children: [
262
+ /* @__PURE__ */ jsxs("header", { className: "flex items-start gap-3 px-4 py-3", children: [
263
+ /* @__PURE__ */ jsx(
264
+ "span",
265
+ {
266
+ className: cn(
267
+ "flex size-8 shrink-0 items-center justify-center rounded-lg",
268
+ hasFailures ? "bg-destructive/10 text-destructive" : "bg-emerald-50 text-emerald-600"
269
+ ),
270
+ children: /* @__PURE__ */ jsx(ClapperboardIcon, { className: "size-4", "aria-hidden": true })
271
+ }
272
+ ),
273
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
274
+ /* @__PURE__ */ jsx("h3", { className: "break-words text-[13px] font-medium leading-snug text-foreground", children: report.title }),
275
+ /* @__PURE__ */ jsxs("p", { "data-testid": "test-recording-counts", className: "text-[12px] tabular-nums text-muted-foreground", children: [
276
+ /* @__PURE__ */ jsxs("span", { className: cn(report.passed > 0 && "text-emerald-700"), children: [
277
+ report.passed,
278
+ " passed"
279
+ ] }),
280
+ " · ",
281
+ /* @__PURE__ */ jsxs("span", { className: cn(hasFailures && "font-medium text-destructive"), children: [
282
+ report.failed,
283
+ " failed"
284
+ ] }),
285
+ report.untested > 0 ? ` · ${report.untested} untested` : null
286
+ ] })
287
+ ] }),
288
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 rounded-md bg-muted/60 px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground", children: "UI test" })
289
+ ] }),
290
+ playableAssetId ? /* @__PURE__ */ jsx("div", { className: "border-t border-border/40", children: /* @__PURE__ */ jsx(
291
+ TestRecordingPlayer,
292
+ {
293
+ assetId: playableAssetId,
294
+ title: report.title,
295
+ durationS: report.editedDurationS,
296
+ url: report.url,
297
+ playback,
298
+ onPlay: play
299
+ }
300
+ ) }) : null,
301
+ report.tests.length > 0 ? /* @__PURE__ */ jsx("ol", { "aria-label": "Tests", className: "flex flex-col divide-y divide-border/40 border-t border-border/40", children: report.tests.map((test, index) => (
302
+ // Tests are an ordered log, never reordered; two may share a name.
303
+ /* @__PURE__ */ jsx(TestRow, { test, onSeek }, index)
304
+ )) }) : /* @__PURE__ */ jsx("p", { className: "border-t border-border/40 px-4 py-2.5 text-[12px] text-muted-foreground", children: "No tests were marked in this recording." }),
305
+ report.summary ? /* @__PURE__ */ jsxs("div", { className: "border-t border-border/40 px-4 py-2.5", children: [
306
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-muted-foreground", children: "Summary" }),
307
+ /* @__PURE__ */ jsx(
308
+ "p",
309
+ {
310
+ "data-testid": "test-recording-summary",
311
+ className: "mt-0.5 whitespace-pre-wrap break-words text-[12.5px] leading-relaxed text-foreground",
312
+ children: report.summary
313
+ }
314
+ )
315
+ ] }) : null,
316
+ report.url ? /* @__PURE__ */ jsx("div", { className: "flex justify-end border-t border-border/40 px-4 py-2", children: /* @__PURE__ */ jsxs(
317
+ "a",
318
+ {
319
+ href: report.url,
320
+ target: "_blank",
321
+ rel: "noopener noreferrer",
322
+ className: "flex items-center gap-1.5 rounded-md px-2 py-1 text-[12px] font-medium text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground",
323
+ children: [
324
+ /* @__PURE__ */ jsx(ExternalLinkIcon, { className: "size-3", "aria-hidden": true }),
325
+ "Watch in Athena"
326
+ ]
327
+ }
328
+ ) }) : null
329
+ ]
330
+ }
331
+ );
332
+ }
333
+ const TestRecordingStopToolUIImpl = ({
334
+ toolName,
335
+ args,
336
+ result,
337
+ status
338
+ }) => {
339
+ const typedArgs = argsRecord(args);
340
+ const titleArg = argString(typedArgs, "title");
341
+ const parsed = useMemo(
342
+ () => parseTestRecordingStopResult(result, { fallbackTitle: titleArg }),
343
+ [result, titleArg]
344
+ );
345
+ if (parsed.kind === "saved") return /* @__PURE__ */ jsx(TestRecordingReportCard, { report: parsed.report });
346
+ const isRunning = (status == null ? void 0 : status.type) === "running";
347
+ const error = incompleteMessage(status, "The test recording could not be saved.") ?? (parsed.kind === "error" ? parsed.message : null);
348
+ const cardStatus = error ? "incomplete" : isRunning ? "running" : "complete";
349
+ return /* @__PURE__ */ jsx(
350
+ ToolCard,
351
+ {
352
+ icon: ClapperboardIcon,
353
+ status: cardStatus,
354
+ title: error ? "Couldn't save the test recording" : isRunning ? "Saving test recording…" : "Test recording",
355
+ subtitle: titleArg,
356
+ toolName,
357
+ error,
358
+ args: typedArgs,
359
+ result,
360
+ children: cardStatus === "complete" ? /* @__PURE__ */ jsx("p", { className: "border-t border-border/40 px-4 py-2.5 text-[12px] text-muted-foreground", children: "The recording's report could not be read." }) : null
361
+ }
362
+ );
363
+ };
364
+ const TestRecordingStopToolUI = memo(
365
+ TestRecordingStopToolUIImpl
366
+ );
367
+ TestRecordingStopToolUI.displayName = "TestRecordingStopToolUI";
368
+ function TestRecordingRow({
369
+ testId,
370
+ kind,
371
+ icon: Icon,
372
+ iconClassName,
373
+ label,
374
+ labelClassName,
375
+ text,
376
+ time,
377
+ error
378
+ }) {
379
+ return /* @__PURE__ */ jsxs(
380
+ "div",
381
+ {
382
+ "data-testid": testId,
383
+ "data-kind": kind,
384
+ "data-failed": error ? "true" : "false",
385
+ className: cn(
386
+ "my-1 flex items-start gap-2 rounded-lg px-2 py-1 text-[12.5px] leading-snug",
387
+ error && "bg-destructive/5"
388
+ ),
389
+ children: [
390
+ /* @__PURE__ */ jsx(Icon, { className: cn("mt-0.5 size-3.5 shrink-0", iconClassName), "aria-hidden": true }),
391
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
392
+ /* @__PURE__ */ jsx("span", { className: cn("mr-1.5 font-medium", labelClassName), children: label }),
393
+ text ? /* @__PURE__ */ jsx("span", { className: "break-words text-foreground", children: text }) : null,
394
+ error ? /* @__PURE__ */ jsx("p", { className: "mt-0.5 break-words text-[12px] text-destructive", children: error }) : null
395
+ ] }),
396
+ time ? /* @__PURE__ */ jsx("span", { className: "shrink-0 font-mono text-[11px] tabular-nums text-muted-foreground", children: time }) : null
397
+ ]
398
+ }
399
+ );
400
+ }
401
+ const TestRecordingStartToolUIImpl = ({ args, result, status }) => {
402
+ const start = useMemo(
403
+ () => describeTestRecordingStart({ args: argsRecord(args), result }),
404
+ [args, result]
405
+ );
406
+ const error = incompleteMessage(status, "The recording could not start.") ?? start.error;
407
+ const isRunning = !error && (status == null ? void 0 : status.type) === "running";
408
+ return /* @__PURE__ */ jsx(
409
+ TestRecordingRow,
410
+ {
411
+ testId: "test-recording-start",
412
+ icon: error ? AlertCircleIcon : isRunning ? LoaderIcon : CircleDotIcon,
413
+ iconClassName: cn(
414
+ error && "text-destructive",
415
+ isRunning && "animate-spin text-muted-foreground",
416
+ !error && !isRunning && "text-red-500"
417
+ ),
418
+ label: error ? "Couldn't start recording" : isRunning ? "Starting recording" : "Recording started",
419
+ labelClassName: error ? "text-destructive" : "text-muted-foreground",
420
+ text: start.title,
421
+ error
422
+ }
423
+ );
424
+ };
425
+ const TestRecordingStartToolUI = memo(
426
+ TestRecordingStartToolUIImpl
427
+ );
428
+ TestRecordingStartToolUI.displayName = "TestRecordingStartToolUI";
429
+ const MARK_ROW = {
430
+ test: {
431
+ label: "Test",
432
+ icon: FlaskConicalIcon,
433
+ iconClassName: "text-primary",
434
+ labelClassName: "text-primary"
435
+ },
436
+ pass: {
437
+ label: "Pass",
438
+ icon: CheckIcon,
439
+ iconClassName: "text-emerald-600",
440
+ labelClassName: "text-emerald-700"
441
+ },
442
+ fail: {
443
+ label: "Fail",
444
+ icon: XIcon,
445
+ iconClassName: "text-destructive",
446
+ labelClassName: "text-destructive"
447
+ },
448
+ note: {
449
+ label: "Note",
450
+ icon: StickyNoteIcon,
451
+ iconClassName: "text-muted-foreground",
452
+ labelClassName: "text-muted-foreground"
453
+ }
454
+ };
455
+ const UNKNOWN_MARK_ROW = {
456
+ label: "Mark",
457
+ icon: StickyNoteIcon,
458
+ iconClassName: "text-muted-foreground",
459
+ labelClassName: "text-muted-foreground"
460
+ };
461
+ const TestRecordingMarkToolUIImpl = ({ args, result, status }) => {
462
+ const mark = useMemo(
463
+ () => describeTestRecordingMark({ args: argsRecord(args), result }),
464
+ [args, result]
465
+ );
466
+ const error = incompleteMessage(status, "The mark was not recorded.") ?? mark.error;
467
+ const isRunning = !error && (status == null ? void 0 : status.type) === "running";
468
+ const row = mark.kind ? MARK_ROW[mark.kind] : UNKNOWN_MARK_ROW;
469
+ return /* @__PURE__ */ jsx(
470
+ TestRecordingRow,
471
+ {
472
+ testId: "test-recording-mark",
473
+ kind: mark.kind ?? void 0,
474
+ icon: error ? AlertCircleIcon : isRunning ? LoaderIcon : row.icon,
475
+ iconClassName: cn(
476
+ error && "text-destructive",
477
+ isRunning && "animate-spin text-muted-foreground",
478
+ !error && !isRunning && row.iconClassName
479
+ ),
480
+ label: error ? `Couldn't mark ${row.label.toLowerCase()}` : row.label,
481
+ labelClassName: error ? "text-destructive" : row.labelClassName,
482
+ text: mark.text,
483
+ time: mark.elapsedS !== null ? formatRecordingClock(mark.elapsedS) : null,
484
+ error
485
+ }
486
+ );
487
+ };
488
+ const TestRecordingMarkToolUI = memo(
489
+ TestRecordingMarkToolUIImpl
490
+ );
491
+ TestRecordingMarkToolUI.displayName = "TestRecordingMarkToolUI";
492
+ export {
493
+ TestRecordingMarkToolUI,
494
+ TestRecordingReportCard,
495
+ TestRecordingStartToolUI,
496
+ TestRecordingStopToolUI
497
+ };
498
+ //# sourceMappingURL=test-recording-tool-uis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-recording-tool-uis.js","sources":["../../src/tools/test-recording-tool-uis.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * The UI Testing toolkit's cards: an agent records itself checking a UI\n * change on its computer and hands back an annotated video.\n *\n * - `test_recording_stop` — the report: the recording's title, \"N passed · M\n * failed\", each test with its ✓ / ✗ assertions (what the agent saw), the\n * summary, and the video, with every test's start as a chapter that plays\n * from there. It stays in the conversation instead of folding into the\n * turn's steps (`isSdkInlineTool`).\n * - `test_recording_start` / `test_recording_mark` — one compact row each.\n *\n * The video is an Athena asset the signed-in user owns. It is read with the\n * user's own credential through `useAthenaAssetContent` (a permission-checked\n * read of the stored file) and played from a page-local object URL, only when\n * the person presses play or picks a chapter; no shareable link is minted.\n */\nimport type { ToolCallMessagePartComponent } from \"@assistant-ui/react\";\nimport {\n AlertCircleIcon,\n CheckIcon,\n CircleDashedIcon,\n CircleDotIcon,\n ClapperboardIcon,\n ExternalLinkIcon,\n FlaskConicalIcon,\n LoaderIcon,\n PlayIcon,\n StickyNoteIcon,\n XIcon,\n} from \"lucide-react\";\nimport {\n type ElementType,\n memo,\n type SyntheticEvent,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useAthenaAssetContent, useObjectUrl } from \"../assets/use-asset-content\";\nimport { cn } from \"../lib/utils\";\nimport { AthenaContext } from \"../provider/AthenaContext\";\nimport { ToolCard, type ToolStatus } from \"./primitives\";\nimport {\n describeTestRecordingMark,\n describeTestRecordingStart,\n formatRecordingClock,\n parseTestRecordingStopResult,\n type TestRecordingAssertion,\n type TestRecordingMarkKind,\n type TestRecordingOutcome,\n type TestRecordingReport,\n type TestRecordingTest,\n} from \"./test-recording-report\";\n\n// ─── Helpers ─────────────────────────────────────────────────────────\n\nfunction argsRecord(args: unknown): Record<string, unknown> | undefined {\n return typeof args === \"object\" && args !== null && !Array.isArray(args)\n ? (args as Record<string, unknown>)\n : undefined;\n}\n\nfunction argString(args: Record<string, unknown> | undefined, key: string): string | undefined {\n const value = args?.[key];\n return typeof value === \"string\" && value.trim() !== \"\" ? value.trim() : undefined;\n}\n\n/** The message an `incomplete` tool status carries, or `null` for any other status. */\nfunction incompleteMessage(\n status: { type: string; reason?: string; error?: unknown } | undefined,\n fallback: string,\n): string | null {\n if (status?.type !== \"incomplete\") return null;\n if (status.reason === \"cancelled\") return \"Cancelled\";\n if (typeof status.error === \"string\" && status.error.trim() !== \"\") return status.error;\n return fallback;\n}\n\n// ─── Playback ────────────────────────────────────────────────────────\n\ninterface PlaybackRequest {\n /** The person asked to play: the video is read from then on. */\n requested: boolean;\n /** The last chapter picked; `nonce` makes picking the same chapter twice seek twice. */\n seek: { time: number; nonce: number } | null;\n}\n\nconst IDLE_PLAYBACK: PlaybackRequest = { requested: false, seek: null };\n\n/** `HTMLMediaElement.HAVE_METADATA`: below it a seek waits for the video's metadata. */\nconst HAVE_METADATA = 1;\n\nfunction playbackErrorMessage(error: Error): string {\n const status = (error as { status?: unknown }).status;\n if (status === 401 || status === 403) return \"You don't have access to this recording.\";\n if (status === 404) return \"The recording's video was not found.\";\n return \"The recording could not be loaded.\";\n}\n\nfunction TestRecordingPlayer({\n assetId,\n title,\n durationS,\n url,\n playback,\n onPlay,\n}: {\n assetId: string;\n title: string;\n durationS: number | null;\n url: string | null;\n playback: PlaybackRequest;\n onPlay: () => void;\n}) {\n const content = useAthenaAssetContent({ assetId, enabled: playback.requested });\n // Bytes a query already holds still wait for the person to press play.\n const src = useObjectUrl(playback.requested ? content.data : undefined);\n const { refetch } = content;\n const retry = useCallback(() => {\n void refetch();\n }, [refetch]);\n const videoRef = useRef<HTMLVideoElement | null>(null);\n const pendingSeekRef = useRef<number | null>(null);\n const seekTime = playback.seek?.time;\n const seekNonce = playback.seek?.nonce ?? 0;\n\n // Play once the bytes are here, and again from each chapter picked (the\n // nonce is the pick's change signal; picking one chapter twice seeks twice).\n // A seek before the metadata loads sets the start position and is applied\n // again once it has (`onLoadedMetadata`).\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useEffect(() => {\n const video = videoRef.current;\n if (!video || !src) return;\n if (seekTime !== undefined) {\n video.currentTime = seekTime;\n pendingSeekRef.current = video.readyState < HAVE_METADATA ? seekTime : null;\n }\n video.play()?.catch(() => {\n // Autoplay refused (no recent user gesture): the controls stay to press play.\n });\n }, [src, seekNonce]);\n\n const handleLoadedMetadata = useCallback((event: SyntheticEvent<HTMLVideoElement>) => {\n const pending = pendingSeekRef.current;\n pendingSeekRef.current = null;\n if (pending !== null) event.currentTarget.currentTime = pending;\n }, []);\n\n if (src) {\n return (\n <video\n ref={videoRef}\n data-testid=\"test-recording-video\"\n src={src}\n controls\n playsInline\n preload=\"auto\"\n aria-label={`Recording: ${title}`}\n onLoadedMetadata={handleLoadedMetadata}\n className=\"block max-h-[480px] w-full bg-black\"\n />\n );\n }\n\n if (content.isError && !content.isFetching) {\n return (\n <div\n role=\"alert\"\n data-testid=\"test-recording-video-error\"\n className=\"flex aspect-video w-full flex-col items-center justify-center gap-2 bg-muted/40 px-6 text-center\"\n >\n <AlertCircleIcon className=\"size-5 text-destructive\" aria-hidden />\n <p className=\"text-[12.5px] text-foreground\">{playbackErrorMessage(content.error)}</p>\n <div className=\"flex items-center gap-3\">\n {/* An explicit retry, refusals included: access may have changed since. */}\n <button\n type=\"button\"\n onClick={retry}\n className=\"rounded-md border border-border bg-background px-2.5 py-1 text-[12px] font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n Try again\n </button>\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-[12px] font-medium text-primary hover:underline\"\n >\n Watch in Athena\n </a>\n ) : null}\n </div>\n </div>\n );\n }\n\n if (playback.requested) {\n return (\n <output\n data-testid=\"test-recording-video-loading\"\n className=\"flex aspect-video w-full items-center justify-center gap-2 bg-zinc-950 text-[12px] text-white/80\"\n >\n <LoaderIcon className=\"size-4 animate-spin\" aria-hidden />\n Loading recording…\n </output>\n );\n }\n\n return (\n <button\n type=\"button\"\n onClick={onPlay}\n aria-label={`Play recording: ${title}`}\n className=\"group relative flex aspect-video w-full items-center justify-center bg-zinc-950 text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset\"\n >\n <span className=\"flex size-12 items-center justify-center rounded-full bg-white/15 transition-colors group-hover:bg-white/25\">\n <PlayIcon className=\"ml-0.5 size-5 fill-current\" aria-hidden />\n </span>\n {durationS !== null ? (\n <span className=\"absolute right-3 bottom-2 text-[11px] tabular-nums text-white/80\">\n {formatRecordingClock(durationS)}\n </span>\n ) : null}\n </button>\n );\n}\n\n/** A time that plays the recording from there: a test's chapter or an assertion's moment. */\nfunction ChapterButton({\n time,\n label,\n onSeek,\n}: {\n time: number;\n label: string;\n onSeek: (time: number) => void;\n}) {\n const clock = formatRecordingClock(time);\n return (\n <button\n type=\"button\"\n data-testid=\"test-recording-chapter\"\n onClick={() => onSeek(time)}\n aria-label={`Play from ${clock}: ${label}`}\n className=\"shrink-0 rounded px-1.5 py-0.5 font-mono text-[11px] tabular-nums text-primary transition-colors hover:bg-primary/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n {clock}\n </button>\n );\n}\n\n// ─── Report card ─────────────────────────────────────────────────────\n\nconst OUTCOME_LABEL: Record<TestRecordingOutcome, string> = {\n passed: \"Passed\",\n failed: \"Failed\",\n untested: \"Not checked\",\n};\n\nfunction OutcomeIcon({ result }: { result: TestRecordingOutcome }) {\n if (result === \"passed\") {\n return (\n <span className=\"mt-px flex size-4 shrink-0 items-center justify-center rounded-full bg-emerald-500 text-white\">\n <CheckIcon className=\"size-2.5\" strokeWidth={3.5} aria-hidden />\n </span>\n );\n }\n if (result === \"failed\") {\n return (\n <span className=\"mt-px flex size-4 shrink-0 items-center justify-center rounded-full bg-destructive text-white\">\n <XIcon className=\"size-2.5\" strokeWidth={3.5} aria-hidden />\n </span>\n );\n }\n return <CircleDashedIcon className=\"mt-px size-4 shrink-0 text-muted-foreground\" aria-hidden />;\n}\n\nfunction AssertionRow({\n assertion,\n onSeek,\n}: {\n assertion: TestRecordingAssertion;\n onSeek: ((time: number) => void) | null;\n}) {\n const failed = assertion.result === \"failed\";\n const text = assertion.text || (failed ? \"Failed\" : \"Passed\");\n return (\n <li\n data-testid=\"test-recording-assertion\"\n data-result={assertion.result}\n className={cn(\n \"flex items-start gap-1.5 rounded-md px-1.5 py-0.5 text-[12px] leading-snug\",\n failed ? \"bg-destructive/10 text-destructive\" : \"text-muted-foreground\",\n )}\n >\n {failed ? (\n <XIcon className=\"mt-0.5 size-3 shrink-0\" strokeWidth={3} aria-hidden />\n ) : (\n <CheckIcon className=\"mt-0.5 size-3 shrink-0 text-emerald-600\" strokeWidth={3} aria-hidden />\n )}\n <span className=\"min-w-0 flex-1 break-words\">\n <span className=\"sr-only\">{failed ? \"Failed: \" : \"Passed: \"}</span>\n {text}\n </span>\n {onSeek && assertion.editedTimeS !== null ? (\n <ChapterButton time={assertion.editedTimeS} label={text} onSeek={onSeek} />\n ) : null}\n </li>\n );\n}\n\nfunction TestRow({ test, onSeek }: { test: TestRecordingTest; onSeek: ((time: number) => void) | null }) {\n return (\n <li data-testid=\"test-recording-test\" data-result={test.result} className=\"px-4 py-2.5\">\n <div className=\"flex items-start gap-2\">\n <OutcomeIcon result={test.result} />\n <span\n className={cn(\n \"min-w-0 flex-1 break-words text-[12.5px] font-medium leading-snug\",\n test.result === \"failed\" && \"text-destructive\",\n test.result === \"passed\" && \"text-foreground\",\n test.result === \"untested\" && \"text-muted-foreground\",\n )}\n >\n <span className=\"sr-only\">{`${OUTCOME_LABEL[test.result]}: `}</span>\n {test.name}\n </span>\n {test.result === \"untested\" ? (\n <span className=\"shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground\">\n Not checked\n </span>\n ) : null}\n {onSeek && test.startTimeS !== null ? (\n <ChapterButton time={test.startTimeS} label={test.name} onSeek={onSeek} />\n ) : null}\n </div>\n {test.assertions.length > 0 ? (\n <ul className=\"mt-1 flex flex-col gap-0.5 pl-6\">\n {test.assertions.map((assertion, index) => (\n <AssertionRow\n // Assertions are an ordered log with no id of their own, never reordered.\n key={index}\n assertion={assertion}\n onSeek={onSeek}\n />\n ))}\n </ul>\n ) : null}\n </li>\n );\n}\n\nexport interface TestRecordingReportCardProps {\n report: TestRecordingReport;\n className?: string;\n}\n\n/**\n * A saved test recording: the title and counts, the video (read with the\n * signed-in user's credential when the person presses play), each test with\n * its assertions and chapter times, the summary, and a link to the asset in\n * Athena.\n */\nexport function TestRecordingReportCard({ report, className }: TestRecordingReportCardProps) {\n const [playback, setPlayback] = useState<PlaybackRequest>(IDLE_PLAYBACK);\n const play = useCallback(() => {\n setPlayback((previous) => ({ ...previous, requested: true }));\n }, []);\n const seek = useCallback((time: number) => {\n setPlayback((previous) => ({\n requested: true,\n seek: { time, nonce: (previous.seek?.nonce ?? 0) + 1 },\n }));\n }, []);\n // The video is read with the provider's credentials; a host that renders\n // the card outside `<AthenaProvider>` gets the report and the link only.\n const athenaConfig = useContext(AthenaContext);\n const playableAssetId = athenaConfig ? report.assetId : null;\n const onSeek = playableAssetId ? seek : null;\n const hasFailures = report.failed > 0;\n\n return (\n <section\n data-testid=\"test-recording-report\"\n data-has-failures={hasFailures ? \"true\" : \"false\"}\n aria-label={`Test recording: ${report.title}`}\n className={cn(\n \"my-3 w-full overflow-hidden rounded-xl border border-border/60 bg-background shadow-sm\",\n className,\n )}\n >\n <header className=\"flex items-start gap-3 px-4 py-3\">\n <span\n className={cn(\n \"flex size-8 shrink-0 items-center justify-center rounded-lg\",\n hasFailures ? \"bg-destructive/10 text-destructive\" : \"bg-emerald-50 text-emerald-600\",\n )}\n >\n <ClapperboardIcon className=\"size-4\" aria-hidden />\n </span>\n <div className=\"min-w-0 flex-1\">\n <h3 className=\"break-words text-[13px] font-medium leading-snug text-foreground\">\n {report.title}\n </h3>\n <p data-testid=\"test-recording-counts\" className=\"text-[12px] tabular-nums text-muted-foreground\">\n <span className={cn(report.passed > 0 && \"text-emerald-700\")}>{report.passed} passed</span>\n {\" · \"}\n <span className={cn(hasFailures && \"font-medium text-destructive\")}>{report.failed} failed</span>\n {report.untested > 0 ? ` · ${report.untested} untested` : null}\n </p>\n </div>\n <span className=\"shrink-0 rounded-md bg-muted/60 px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground\">\n UI test\n </span>\n </header>\n\n {playableAssetId ? (\n <div className=\"border-t border-border/40\">\n <TestRecordingPlayer\n assetId={playableAssetId}\n title={report.title}\n durationS={report.editedDurationS}\n url={report.url}\n playback={playback}\n onPlay={play}\n />\n </div>\n ) : null}\n\n {report.tests.length > 0 ? (\n <ol aria-label=\"Tests\" className=\"flex flex-col divide-y divide-border/40 border-t border-border/40\">\n {report.tests.map((test, index) => (\n // Tests are an ordered log, never reordered; two may share a name.\n <TestRow key={index} test={test} onSeek={onSeek} />\n ))}\n </ol>\n ) : (\n <p className=\"border-t border-border/40 px-4 py-2.5 text-[12px] text-muted-foreground\">\n No tests were marked in this recording.\n </p>\n )}\n\n {report.summary ? (\n <div className=\"border-t border-border/40 px-4 py-2.5\">\n <p className=\"text-[11px] font-medium text-muted-foreground\">Summary</p>\n <p\n data-testid=\"test-recording-summary\"\n className=\"mt-0.5 whitespace-pre-wrap break-words text-[12.5px] leading-relaxed text-foreground\"\n >\n {report.summary}\n </p>\n </div>\n ) : null}\n\n {report.url ? (\n <div className=\"flex justify-end border-t border-border/40 px-4 py-2\">\n <a\n href={report.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-1.5 rounded-md px-2 py-1 text-[12px] font-medium text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground\"\n >\n <ExternalLinkIcon className=\"size-3\" aria-hidden />\n Watch in Athena\n </a>\n </div>\n ) : null}\n </section>\n );\n}\n\n// ─── test_recording_stop ─────────────────────────────────────────────\n\nconst TestRecordingStopToolUIImpl: ToolCallMessagePartComponent = ({\n toolName,\n args,\n result,\n status,\n}) => {\n const typedArgs = argsRecord(args);\n const titleArg = argString(typedArgs, \"title\");\n const parsed = useMemo(\n () => parseTestRecordingStopResult(result, { fallbackTitle: titleArg }),\n [result, titleArg],\n );\n\n if (parsed.kind === \"saved\") return <TestRecordingReportCard report={parsed.report} />;\n\n const isRunning = status?.type === \"running\";\n const error =\n incompleteMessage(status, \"The test recording could not be saved.\") ??\n (parsed.kind === \"error\" ? parsed.message : null);\n const cardStatus: ToolStatus = error ? \"incomplete\" : isRunning ? \"running\" : \"complete\";\n return (\n <ToolCard\n icon={ClapperboardIcon}\n status={cardStatus}\n title={\n error ? \"Couldn't save the test recording\" : isRunning ? \"Saving test recording…\" : \"Test recording\"\n }\n subtitle={titleArg}\n toolName={toolName}\n error={error}\n args={typedArgs}\n result={result}\n >\n {cardStatus === \"complete\" ? (\n <p className=\"border-t border-border/40 px-4 py-2.5 text-[12px] text-muted-foreground\">\n The recording's report could not be read.\n </p>\n ) : null}\n </ToolCard>\n );\n};\n\nexport const TestRecordingStopToolUI = memo(\n TestRecordingStopToolUIImpl,\n) as unknown as ToolCallMessagePartComponent;\nTestRecordingStopToolUI.displayName = \"TestRecordingStopToolUI\";\n\n// ─── Compact rows: test_recording_start / test_recording_mark ────────\n\nfunction TestRecordingRow({\n testId,\n kind,\n icon: Icon,\n iconClassName,\n label,\n labelClassName,\n text,\n time,\n error,\n}: {\n testId: string;\n kind?: string;\n icon: ElementType;\n iconClassName?: string;\n label: string;\n labelClassName?: string;\n text?: string;\n time?: string | null;\n error?: string | null;\n}) {\n return (\n <div\n data-testid={testId}\n data-kind={kind}\n data-failed={error ? \"true\" : \"false\"}\n className={cn(\n \"my-1 flex items-start gap-2 rounded-lg px-2 py-1 text-[12.5px] leading-snug\",\n error && \"bg-destructive/5\",\n )}\n >\n <Icon className={cn(\"mt-0.5 size-3.5 shrink-0\", iconClassName)} aria-hidden />\n <div className=\"min-w-0 flex-1\">\n <span className={cn(\"mr-1.5 font-medium\", labelClassName)}>{label}</span>\n {text ? <span className=\"break-words text-foreground\">{text}</span> : null}\n {error ? <p className=\"mt-0.5 break-words text-[12px] text-destructive\">{error}</p> : null}\n </div>\n {time ? (\n <span className=\"shrink-0 font-mono text-[11px] tabular-nums text-muted-foreground\">{time}</span>\n ) : null}\n </div>\n );\n}\n\nconst TestRecordingStartToolUIImpl: ToolCallMessagePartComponent = ({ args, result, status }) => {\n const start = useMemo(\n () => describeTestRecordingStart({ args: argsRecord(args), result }),\n [args, result],\n );\n const error = incompleteMessage(status, \"The recording could not start.\") ?? start.error;\n const isRunning = !error && status?.type === \"running\";\n return (\n <TestRecordingRow\n testId=\"test-recording-start\"\n icon={error ? AlertCircleIcon : isRunning ? LoaderIcon : CircleDotIcon}\n iconClassName={cn(\n error && \"text-destructive\",\n isRunning && \"animate-spin text-muted-foreground\",\n !error && !isRunning && \"text-red-500\",\n )}\n label={error ? \"Couldn't start recording\" : isRunning ? \"Starting recording\" : \"Recording started\"}\n labelClassName={error ? \"text-destructive\" : \"text-muted-foreground\"}\n text={start.title}\n error={error}\n />\n );\n};\n\nexport const TestRecordingStartToolUI = memo(\n TestRecordingStartToolUIImpl,\n) as unknown as ToolCallMessagePartComponent;\nTestRecordingStartToolUI.displayName = \"TestRecordingStartToolUI\";\n\nconst MARK_ROW: Record<\n TestRecordingMarkKind,\n { label: string; icon: ElementType; iconClassName: string; labelClassName: string }\n> = {\n test: {\n label: \"Test\",\n icon: FlaskConicalIcon,\n iconClassName: \"text-primary\",\n labelClassName: \"text-primary\",\n },\n pass: {\n label: \"Pass\",\n icon: CheckIcon,\n iconClassName: \"text-emerald-600\",\n labelClassName: \"text-emerald-700\",\n },\n fail: {\n label: \"Fail\",\n icon: XIcon,\n iconClassName: \"text-destructive\",\n labelClassName: \"text-destructive\",\n },\n note: {\n label: \"Note\",\n icon: StickyNoteIcon,\n iconClassName: \"text-muted-foreground\",\n labelClassName: \"text-muted-foreground\",\n },\n};\n\n/** A mark whose kind has not streamed in yet (or is one this SDK does not know). */\nconst UNKNOWN_MARK_ROW = {\n label: \"Mark\",\n icon: StickyNoteIcon,\n iconClassName: \"text-muted-foreground\",\n labelClassName: \"text-muted-foreground\",\n} as const;\n\nconst TestRecordingMarkToolUIImpl: ToolCallMessagePartComponent = ({ args, result, status }) => {\n const mark = useMemo(\n () => describeTestRecordingMark({ args: argsRecord(args), result }),\n [args, result],\n );\n const error = incompleteMessage(status, \"The mark was not recorded.\") ?? mark.error;\n const isRunning = !error && status?.type === \"running\";\n const row = mark.kind ? MARK_ROW[mark.kind] : UNKNOWN_MARK_ROW;\n return (\n <TestRecordingRow\n testId=\"test-recording-mark\"\n kind={mark.kind ?? undefined}\n icon={error ? AlertCircleIcon : isRunning ? LoaderIcon : row.icon}\n iconClassName={cn(\n error && \"text-destructive\",\n isRunning && \"animate-spin text-muted-foreground\",\n !error && !isRunning && row.iconClassName,\n )}\n label={error ? `Couldn't mark ${row.label.toLowerCase()}` : row.label}\n labelClassName={error ? \"text-destructive\" : row.labelClassName}\n text={mark.text}\n time={mark.elapsedS !== null ? formatRecordingClock(mark.elapsedS) : null}\n error={error}\n />\n );\n};\n\nexport const TestRecordingMarkToolUI = memo(\n TestRecordingMarkToolUIImpl,\n) as unknown as ToolCallMessagePartComponent;\nTestRecordingMarkToolUI.displayName = \"TestRecordingMarkToolUI\";\n"],"names":["_a"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const testRecordingReport = require("./test-recording-report.cjs");
3
4
  const toolMetadata = require("../agent-runtime/src/tool-metadata.cjs");
4
5
  const TARGET_MAX = 72;
5
6
  const DETAIL_MAX = 300;
@@ -564,6 +565,50 @@ const startAsyncTaskRule = ({ args }) => {
564
565
  detail: description
565
566
  };
566
567
  };
568
+ const testRecordingStartRule = ({ args }) => {
569
+ const title = str(args.title);
570
+ return {
571
+ verb: ["Starting", "Started", "start"],
572
+ object: title ? `recording ${quoted(title)}` : "a test recording"
573
+ };
574
+ };
575
+ const testRecordingMarkRule = ({ args, result }) => {
576
+ const mark = testRecordingReport.describeTestRecordingMark({ args, result });
577
+ const text = mark.text ? clipLine(mark.text, 64) : void 0;
578
+ const at = mark.elapsedS !== null ? `at ${testRecordingReport.formatRecordingClock(mark.elapsedS)}` : void 0;
579
+ const common = { detail: mark.text || void 0, meta: [at] };
580
+ switch (mark.kind) {
581
+ case "test":
582
+ return { ...common, verb: ["Starting", "Started", "start"], object: text ? `test: ${text}` : "a test" };
583
+ case "pass":
584
+ return {
585
+ ...common,
586
+ verb: ["Marking", "Marked", "mark"],
587
+ object: text ? `pass: ${text}` : "a pass",
588
+ done: text ? `Passed: ${text}` : "Marked a pass"
589
+ };
590
+ case "fail":
591
+ return {
592
+ ...common,
593
+ verb: ["Marking", "Marked", "mark"],
594
+ object: text ? `failure: ${text}` : "a failure",
595
+ done: text ? `Failed: ${text}` : "Marked a failure"
596
+ };
597
+ case "note":
598
+ return { ...common, verb: ["Noting", "Noted", "note"], object: text ?? "a note" };
599
+ default:
600
+ return { ...common, verb: ["Marking", "Marked", "mark"], object: "the recording" };
601
+ }
602
+ };
603
+ const testRecordingStopRule = ({ args, result }) => {
604
+ const parsed = testRecordingReport.parseTestRecordingStopResult(result, { fallbackTitle: str(args.title) });
605
+ const title = parsed.kind === "saved" ? parsed.report.title : str(args.title);
606
+ return {
607
+ verb: ["Saving", "Saved", "save"],
608
+ object: title ? `test recording ${quoted(title)}` : "the test recording",
609
+ meta: [parsed.kind === "saved" ? testRecordingReport.formatTestRecordingCounts(parsed.report) : void 0]
610
+ };
611
+ };
567
612
  const RULES = {
568
613
  execute: executeRule,
569
614
  read_file: readFileRule,
@@ -599,7 +644,10 @@ const RULES = {
599
644
  read_asset: readAssetRule,
600
645
  write_todos: writeTodosRule,
601
646
  task: taskRule,
602
- start_async_task: startAsyncTaskRule
647
+ start_async_task: startAsyncTaskRule,
648
+ test_recording_start: testRecordingStartRule,
649
+ test_recording_mark: testRecordingMarkRule,
650
+ test_recording_stop: testRecordingStopRule
603
651
  };
604
652
  function registrySummary(toolName, args, status) {
605
653
  const meta = toolMetadata.getToolDisplayMeta(toolName);