@aiscene/shared 8.0.3 → 8.0.5

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 (105) hide show
  1. package/dist/es/agent-tools/agent-behavior-init-args.mjs +44 -0
  2. package/dist/es/agent-tools/base-tools.mjs +163 -0
  3. package/dist/es/agent-tools/chrome-path.mjs +50 -0
  4. package/dist/es/agent-tools/cli-report-session.mjs +78 -0
  5. package/dist/es/agent-tools/error-formatter.mjs +106 -0
  6. package/dist/es/agent-tools/index.mjs +9 -0
  7. package/dist/es/agent-tools/init-arg-utils.mjs +38 -0
  8. package/dist/es/agent-tools/observation-artifact.mjs +5 -0
  9. package/dist/es/agent-tools/observation-record.mjs +248 -0
  10. package/dist/es/agent-tools/tool-defaults.mjs +54 -0
  11. package/dist/es/agent-tools/tool-generator.mjs +529 -0
  12. package/dist/es/agent-tools/types.mjs +3 -0
  13. package/dist/es/agent-tools/user-prompt.mjs +66 -0
  14. package/dist/es/cli/interrupt.mjs +116 -0
  15. package/dist/es/cli/record-command.mjs +130 -0
  16. package/dist/es/cli/screenshot-file.mjs +24 -0
  17. package/dist/es/cli/verbose-ai-act.mjs +230 -0
  18. package/dist/es/cli/verbose-screenshot.mjs +134 -0
  19. package/dist/es/cli/verbose.mjs +404 -0
  20. package/dist/es/env/parse-model-config.mjs +1 -1
  21. package/dist/es/env/types.mjs +18 -3
  22. package/dist/es/img/index.mjs +3 -3
  23. package/dist/es/img/info.mjs +45 -1
  24. package/dist/es/img/photon-loader.mjs +4 -0
  25. package/dist/es/img/transform.mjs +113 -3
  26. package/dist/es/recorder.mjs +249 -0
  27. package/dist/lib/agent-tools/agent-behavior-init-args.js +87 -0
  28. package/dist/lib/agent-tools/base-tools.js +197 -0
  29. package/dist/lib/agent-tools/chrome-path.js +87 -0
  30. package/dist/lib/agent-tools/cli-report-session.js +121 -0
  31. package/dist/lib/agent-tools/error-formatter.js +149 -0
  32. package/dist/lib/agent-tools/index.js +114 -0
  33. package/dist/lib/agent-tools/init-arg-utils.js +78 -0
  34. package/dist/lib/agent-tools/observation-artifact.js +42 -0
  35. package/dist/lib/agent-tools/observation-record.js +297 -0
  36. package/dist/lib/agent-tools/tool-defaults.js +97 -0
  37. package/dist/lib/agent-tools/tool-generator.js +569 -0
  38. package/dist/lib/agent-tools/types.js +40 -0
  39. package/dist/lib/agent-tools/user-prompt.js +103 -0
  40. package/dist/lib/cli/interrupt.js +156 -0
  41. package/dist/lib/cli/record-command.js +164 -0
  42. package/dist/lib/cli/screenshot-file.js +58 -0
  43. package/dist/lib/cli/verbose-ai-act.js +273 -0
  44. package/dist/lib/cli/verbose-screenshot.js +177 -0
  45. package/dist/lib/cli/verbose.js +465 -0
  46. package/dist/lib/env/parse-model-config.js +1 -1
  47. package/dist/lib/env/types.js +32 -2
  48. package/dist/lib/img/index.js +29 -5
  49. package/dist/lib/img/info.js +48 -1
  50. package/dist/lib/img/photon-loader.js +38 -0
  51. package/dist/lib/img/transform.js +135 -4
  52. package/dist/lib/recorder.js +307 -0
  53. package/dist/types/agent-tools/agent-behavior-init-args.d.ts +17 -0
  54. package/dist/types/agent-tools/base-tools.d.ts +158 -0
  55. package/dist/types/agent-tools/chrome-path.d.ts +2 -0
  56. package/dist/types/agent-tools/cli-report-session.d.ts +12 -0
  57. package/dist/types/agent-tools/error-formatter.d.ts +30 -0
  58. package/dist/types/agent-tools/index.d.ts +9 -0
  59. package/dist/types/agent-tools/init-arg-utils.d.ts +13 -0
  60. package/dist/types/agent-tools/observation-artifact.d.ts +10 -0
  61. package/dist/types/agent-tools/observation-record.d.ts +38 -0
  62. package/dist/types/agent-tools/tool-defaults.d.ts +63 -0
  63. package/dist/types/agent-tools/tool-generator.d.ts +13 -0
  64. package/dist/types/agent-tools/types.d.ts +213 -0
  65. package/dist/types/agent-tools/user-prompt.d.ts +13 -0
  66. package/dist/types/cli/interrupt.d.ts +49 -0
  67. package/dist/types/cli/record-command.d.ts +3 -0
  68. package/dist/types/cli/screenshot-file.d.ts +10 -0
  69. package/dist/types/cli/verbose-ai-act.d.ts +44 -0
  70. package/dist/types/cli/verbose-screenshot.d.ts +10 -0
  71. package/dist/types/cli/verbose.d.ts +40 -0
  72. package/dist/types/env/types.d.ts +16 -7
  73. package/dist/types/img/index.d.ts +2 -2
  74. package/dist/types/img/info.d.ts +2 -0
  75. package/dist/types/img/photon-loader.d.ts +2 -0
  76. package/dist/types/img/transform.d.ts +22 -2
  77. package/dist/types/mcp/types.d.ts +1 -0
  78. package/dist/types/recorder.d.ts +113 -0
  79. package/package.json +1 -1
  80. package/src/agent-tools/agent-behavior-init-args.ts +109 -0
  81. package/src/agent-tools/base-tools.ts +399 -0
  82. package/src/agent-tools/chrome-path.ts +74 -0
  83. package/src/agent-tools/cli-report-session.ts +130 -0
  84. package/src/agent-tools/error-formatter.ts +177 -0
  85. package/src/agent-tools/index.ts +9 -0
  86. package/src/agent-tools/init-arg-utils.ts +105 -0
  87. package/src/agent-tools/observation-artifact.ts +29 -0
  88. package/src/agent-tools/observation-record.ts +331 -0
  89. package/src/agent-tools/tool-defaults.ts +119 -0
  90. package/src/agent-tools/tool-generator.ts +866 -0
  91. package/src/agent-tools/types.ts +250 -0
  92. package/src/agent-tools/user-prompt.ts +102 -0
  93. package/src/cli/interrupt.ts +207 -0
  94. package/src/cli/record-command.ts +177 -0
  95. package/src/cli/screenshot-file.ts +61 -0
  96. package/src/cli/verbose-ai-act.ts +387 -0
  97. package/src/cli/verbose-screenshot.ts +269 -0
  98. package/src/cli/verbose.ts +753 -0
  99. package/src/env/types.ts +30 -2
  100. package/src/img/index.ts +12 -0
  101. package/src/img/info.ts +61 -0
  102. package/src/img/photon-loader.ts +5 -0
  103. package/src/img/transform.ts +262 -3
  104. package/src/mcp/types.ts +2 -0
  105. package/src/recorder.ts +625 -0
@@ -0,0 +1,625 @@
1
+ export type MidsceneRecorderEventType =
2
+ | 'click'
3
+ | 'drag'
4
+ | 'scroll'
5
+ | 'input'
6
+ | 'navigation'
7
+ | 'setViewport'
8
+ | 'keydown';
9
+
10
+ export type MidsceneRecorderSourceKind =
11
+ | 'studio-preview'
12
+ | 'unsupported'
13
+ | (string & {});
14
+
15
+ export type MidsceneRecorderPlatformId =
16
+ | 'web'
17
+ | 'android'
18
+ | 'ios'
19
+ | 'computer'
20
+ | 'harmony'
21
+ | (string & {});
22
+
23
+ export interface MidsceneRecorderElementRect {
24
+ left?: number;
25
+ top?: number;
26
+ width?: number;
27
+ height?: number;
28
+ x?: number;
29
+ y?: number;
30
+ }
31
+
32
+ export interface MidsceneRecorderPageInfo {
33
+ width: number;
34
+ height: number;
35
+ }
36
+
37
+ /**
38
+ * A screenshot stored outside the recording event payload.
39
+ *
40
+ * Recorder events are persisted in the Studio renderer. Keeping full data
41
+ * URLs there makes long recordings retain every screenshot in the renderer
42
+ * heap, so screenshot bytes live in the Playground run directory instead.
43
+ */
44
+ export interface MidsceneRecorderScreenshotAssetRef {
45
+ id: string;
46
+ mimeType: string;
47
+ bytes: number;
48
+ }
49
+
50
+ export interface MidsceneRecorderEvent {
51
+ type: MidsceneRecorderEventType;
52
+ source?: MidsceneRecorderSourceKind;
53
+ actionType?: string;
54
+ rawPayload?: Record<string, unknown>;
55
+ url?: string;
56
+ title?: string;
57
+ value?: string;
58
+ elementRect?: MidsceneRecorderElementRect;
59
+ pageInfo: MidsceneRecorderPageInfo;
60
+ screenshotBefore?: string;
61
+ screenshotAfter?: string;
62
+ /** The single screenshot retained for AI description and Markdown export. */
63
+ screenshotAsset?: MidsceneRecorderScreenshotAssetRef;
64
+ semantic?: MidsceneRecorderSemantic;
65
+ elementDescription?: string;
66
+ descriptionLoading?: boolean;
67
+ screenshotWithBox?: string;
68
+ timestamp: number;
69
+ hashId: string;
70
+ mergedHashIds?: string[];
71
+ }
72
+
73
+ export type MidsceneRecorderSemanticSource =
74
+ | 'aiDescribe'
75
+ | 'recorderAI'
76
+ | 'heuristic';
77
+
78
+ export type MidsceneRecorderSemanticStatus = 'pending' | 'ready' | 'failed';
79
+
80
+ export type MidsceneRecorderSemanticConfidence = 'high' | 'medium' | 'low';
81
+
82
+ export interface MidsceneRecorderSemanticAiDescribe {
83
+ verifyPrompt: boolean;
84
+ verifyPassed?: boolean;
85
+ deepLocate?: boolean;
86
+ centerDistance?: number;
87
+ expectedCenter?: [number, number];
88
+ actualCenter?: [number, number];
89
+ annotatedScreenshotPath?: string;
90
+ }
91
+
92
+ export interface MidsceneRecorderSemantic {
93
+ source: MidsceneRecorderSemanticSource;
94
+ status: MidsceneRecorderSemanticStatus;
95
+ elementDescription?: string;
96
+ replayInstruction?: string;
97
+ actionSummary?: string;
98
+ confidence?: MidsceneRecorderSemanticConfidence;
99
+ error?: string;
100
+ aiDescribe?: MidsceneRecorderSemanticAiDescribe;
101
+ fallbackFrom?: MidsceneRecorderSemantic;
102
+ }
103
+
104
+ export interface MidsceneRecorderSemanticAction {
105
+ type: MidsceneRecorderEventType;
106
+ actionType?: string;
107
+ value?: string;
108
+ url?: string;
109
+ scrollDestinationDescription?: string;
110
+ }
111
+
112
+ export interface MidsceneRecorderTarget {
113
+ platformId: MidsceneRecorderPlatformId;
114
+ deviceId?: string;
115
+ label?: string;
116
+ values: Record<string, string | number | boolean>;
117
+ }
118
+
119
+ export interface MidsceneRecorderGeneratedCode {
120
+ markdown?: string;
121
+ yaml?: string;
122
+ playwright?: string;
123
+ updatedAt?: number;
124
+ }
125
+
126
+ export interface MidsceneRecorderMarkdownScreenshotAsset {
127
+ eventIndex: number;
128
+ eventHashId: string;
129
+ eventType: MidsceneRecorderEventType;
130
+ relativePath: string;
131
+ dataUrl: string;
132
+ base64Data: string;
133
+ mimeType: string;
134
+ }
135
+
136
+ export interface MidsceneRecorderMarkdownScreenshotOptions {
137
+ baseDir?: string;
138
+ maxScreenshots?: number;
139
+ }
140
+
141
+ export const DEFAULT_MIDSCENE_RECORDER_MARKDOWN_MAX_SCREENSHOTS = 20;
142
+
143
+ function isMidsceneRecorderPendingDescription(value?: string) {
144
+ return value?.trim() === 'AI is analyzing element...';
145
+ }
146
+
147
+ export function getMidsceneRecorderSemantic(
148
+ event: Pick<MidsceneRecorderEvent, 'semantic'>,
149
+ ) {
150
+ return event.semantic;
151
+ }
152
+
153
+ function getRecorderPointerActionVerb(actionType?: string) {
154
+ switch (actionType) {
155
+ case 'Tap':
156
+ return 'Tap';
157
+ case 'DoubleClick':
158
+ return 'Double click';
159
+ case 'LongPress':
160
+ return 'Long press';
161
+ case 'RightClick':
162
+ return 'Right click';
163
+ default:
164
+ return 'Click';
165
+ }
166
+ }
167
+
168
+ function getRecorderDragActionVerb(actionType?: string) {
169
+ switch (actionType) {
170
+ case 'Swipe':
171
+ return 'Swipe';
172
+ case 'DragAndDrop':
173
+ return 'Drag';
174
+ default:
175
+ return 'Drag';
176
+ }
177
+ }
178
+
179
+ export function buildMidsceneRecorderReplayInstruction(
180
+ event: MidsceneRecorderSemanticAction,
181
+ elementDescription: string,
182
+ ) {
183
+ switch (event.type) {
184
+ case 'navigation':
185
+ if (event.actionType === 'Stop') {
186
+ return 'Stop loading the current page.';
187
+ }
188
+ if (event.actionType === 'GoBack') {
189
+ return 'Go back in the browser.';
190
+ }
191
+ if (event.actionType === 'GoForward') {
192
+ return 'Go forward in the browser.';
193
+ }
194
+ if (event.actionType === 'Reload') {
195
+ return 'Reload the current page.';
196
+ }
197
+ if (event.actionType === 'NavigationChanged' && event.url) {
198
+ return `Wait for navigation to complete at \`${event.url}\`.`;
199
+ }
200
+ return event.url
201
+ ? `Navigate to \`${event.url}\`.`
202
+ : `Navigate using ${elementDescription}.`;
203
+ case 'scroll':
204
+ return event.scrollDestinationDescription
205
+ ? `Scroll the page/region with description "${elementDescription}" by value "${event.value || 'down'}" until "${event.scrollDestinationDescription}" is visible.`
206
+ : `Scroll the page/region with description "${elementDescription}" by value "${event.value || 'down'}".`;
207
+ case 'drag': {
208
+ const verb = getRecorderDragActionVerb(event.actionType);
209
+ return `${verb} through the area described as "${elementDescription}".`;
210
+ }
211
+ case 'input':
212
+ return `Input "${event.value || ''}" into the element described as "${elementDescription}".`;
213
+ case 'keydown':
214
+ return `Press "${event.value || 'the recorded key'}" on the element described as "${elementDescription}".`;
215
+ default: {
216
+ const verb = getRecorderPointerActionVerb(event.actionType);
217
+ if (verb === 'Long press') {
218
+ return `${verb} the element described as "${elementDescription}".`;
219
+ }
220
+ return `${verb} on the element described as "${elementDescription}".`;
221
+ }
222
+ }
223
+ }
224
+
225
+ export function buildMidsceneRecorderActionSummary(
226
+ event: MidsceneRecorderSemanticAction,
227
+ elementDescription: string,
228
+ ) {
229
+ switch (event.type) {
230
+ case 'navigation':
231
+ if (event.actionType === 'Stop') {
232
+ return 'Stop page loading';
233
+ }
234
+ if (event.actionType === 'GoBack') {
235
+ return 'Go back';
236
+ }
237
+ if (event.actionType === 'GoForward') {
238
+ return 'Go forward';
239
+ }
240
+ if (event.actionType === 'Reload') {
241
+ return 'Reload page';
242
+ }
243
+ if (event.actionType === 'NavigationChanged' && event.url) {
244
+ return `Wait for navigation to complete at ${event.url}`;
245
+ }
246
+ return event.url ? `Navigate to ${event.url}` : 'Navigate';
247
+ case 'scroll':
248
+ return event.scrollDestinationDescription
249
+ ? `Scroll ${elementDescription} toward ${event.scrollDestinationDescription}`
250
+ : `Scroll ${elementDescription}`;
251
+ case 'drag':
252
+ return `${getRecorderDragActionVerb(event.actionType)} ${elementDescription}`;
253
+ case 'input':
254
+ return `Input into ${elementDescription}`;
255
+ case 'keydown':
256
+ return `Press ${event.value || 'key'} on ${elementDescription}`;
257
+ default:
258
+ return `${getRecorderPointerActionVerb(event.actionType)} ${elementDescription}`;
259
+ }
260
+ }
261
+
262
+ export function getMidsceneRecorderEventDescription(
263
+ event: MidsceneRecorderEvent,
264
+ ) {
265
+ const semantic = getMidsceneRecorderSemantic(event);
266
+ if (
267
+ semantic?.actionSummary &&
268
+ !isMidsceneRecorderPendingDescription(semantic.actionSummary)
269
+ ) {
270
+ return semantic.actionSummary;
271
+ }
272
+ if (
273
+ semantic?.elementDescription &&
274
+ !isMidsceneRecorderPendingDescription(semantic.elementDescription)
275
+ ) {
276
+ return semantic.elementDescription;
277
+ }
278
+ if (
279
+ semantic?.replayInstruction &&
280
+ !isMidsceneRecorderPendingDescription(semantic.replayInstruction)
281
+ ) {
282
+ return semantic.replayInstruction;
283
+ }
284
+ if (event.type === 'navigation' && event.url) {
285
+ return `Navigate to ${event.url}`;
286
+ }
287
+ if (event.value) {
288
+ return event.actionType
289
+ ? `${event.actionType} ${event.value}`
290
+ : event.value;
291
+ }
292
+ if (
293
+ event.elementRect?.x !== undefined &&
294
+ event.elementRect?.y !== undefined
295
+ ) {
296
+ const prefix = event.actionType || event.type;
297
+ return `${prefix} (${Math.round(event.elementRect.x)}, ${Math.round(
298
+ event.elementRect.y,
299
+ )})`;
300
+ }
301
+ return event.actionType || event.type;
302
+ }
303
+
304
+ export function getMidsceneRecorderScreenshotsForLLM(
305
+ events: MidsceneRecorderEvent[],
306
+ maxScreenshots = 1,
307
+ ) {
308
+ return selectRecorderScreenshotCandidates(
309
+ getRecorderScreenshotCandidates(events),
310
+ maxScreenshots,
311
+ ).map((candidate) => candidate.screenshot);
312
+ }
313
+
314
+ export function sanitizeMidsceneRecorderFileName(value: string) {
315
+ return (
316
+ value
317
+ .trim()
318
+ .replace(/[\\/:*?"<>|]+/g, '-')
319
+ .replace(/\s+/g, '-')
320
+ .replace(/-+/g, '-')
321
+ .replace(/^-|-$/g, '')
322
+ .toLowerCase() || 'midscene-recording'
323
+ );
324
+ }
325
+
326
+ function normalizeMarkdownAssetBaseDir(baseDir?: string) {
327
+ const value = (baseDir || './screenshots').replace(/\/+$/g, '');
328
+ if (value.startsWith('./') || value.startsWith('../')) {
329
+ return value;
330
+ }
331
+ return `./${value}`;
332
+ }
333
+
334
+ function padEventIndex(index: number) {
335
+ return String(index + 1).padStart(3, '0');
336
+ }
337
+
338
+ function parseScreenshotDataUrl(value: string):
339
+ | {
340
+ dataUrl: string;
341
+ base64Data: string;
342
+ mimeType: string;
343
+ extension: string;
344
+ }
345
+ | undefined {
346
+ const dataUrlMatch = value.match(
347
+ /^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$/,
348
+ );
349
+ if (dataUrlMatch) {
350
+ const mimeType = dataUrlMatch[1];
351
+ const extension = mimeType.includes('jpeg')
352
+ ? 'jpg'
353
+ : mimeType.split('/')[1]?.replace(/[^a-zA-Z0-9]/g, '') || 'png';
354
+ return {
355
+ dataUrl: value,
356
+ base64Data: dataUrlMatch[2],
357
+ mimeType,
358
+ extension,
359
+ };
360
+ }
361
+
362
+ if (/^[a-zA-Z0-9+/=\s]+$/.test(value) && value.trim().length > 0) {
363
+ const base64Data = value.replace(/\s+/g, '');
364
+ return {
365
+ dataUrl: `data:image/png;base64,${base64Data}`,
366
+ base64Data,
367
+ mimeType: 'image/png',
368
+ extension: 'png',
369
+ };
370
+ }
371
+
372
+ return undefined;
373
+ }
374
+
375
+ function getRecorderEventScreenshot(event: MidsceneRecorderEvent) {
376
+ return (
377
+ event.screenshotWithBox || event.screenshotAfter || event.screenshotBefore
378
+ );
379
+ }
380
+
381
+ function hasCoordinateFallback(event: MidsceneRecorderEvent) {
382
+ const semantic = getMidsceneRecorderSemantic(event);
383
+ return (
384
+ !semantic?.elementDescription &&
385
+ event.elementRect?.x !== undefined &&
386
+ event.elementRect?.y !== undefined
387
+ );
388
+ }
389
+
390
+ function shouldIncludeMarkdownScreenshot(
391
+ event: MidsceneRecorderEvent,
392
+ eventIndex: number,
393
+ lastEventIndex: number,
394
+ ) {
395
+ const semantic = getMidsceneRecorderSemantic(event);
396
+ return (
397
+ eventIndex === 0 ||
398
+ eventIndex === lastEventIndex ||
399
+ event.type === 'navigation' ||
400
+ event.type === 'scroll' ||
401
+ event.type === 'input' ||
402
+ Boolean(event.screenshotWithBox) ||
403
+ !semantic?.elementDescription ||
404
+ hasCoordinateFallback(event)
405
+ );
406
+ }
407
+
408
+ interface RecorderScreenshotCandidate {
409
+ event: MidsceneRecorderEvent;
410
+ eventIndex: number;
411
+ screenshot: string;
412
+ }
413
+
414
+ function getRecorderScreenshotCandidatePriority(
415
+ candidate: RecorderScreenshotCandidate,
416
+ firstEventIndex: number,
417
+ lastEventIndex: number,
418
+ ) {
419
+ const event = candidate.event;
420
+ let priority = 0;
421
+
422
+ if (candidate.eventIndex === firstEventIndex) {
423
+ priority += 100;
424
+ }
425
+ if (candidate.eventIndex === lastEventIndex) {
426
+ priority += 95;
427
+ }
428
+ if (event.type === 'navigation') {
429
+ priority += 80;
430
+ }
431
+ if (event.screenshotWithBox) {
432
+ priority += 70;
433
+ }
434
+ const semantic = getMidsceneRecorderSemantic(event);
435
+ if (
436
+ semantic?.source === 'heuristic' ||
437
+ semantic?.confidence === 'low' ||
438
+ semantic?.error
439
+ ) {
440
+ priority += 60;
441
+ }
442
+ if (event.type === 'input' || event.type === 'scroll') {
443
+ priority += 40;
444
+ }
445
+ if (!semantic?.elementDescription || hasCoordinateFallback(event)) {
446
+ priority += 30;
447
+ }
448
+
449
+ return priority;
450
+ }
451
+
452
+ function selectEvenlyDistributedCandidates<
453
+ T extends RecorderScreenshotCandidate,
454
+ >(candidates: T[], count: number) {
455
+ if (count <= 0) {
456
+ return [];
457
+ }
458
+ if (candidates.length <= count) {
459
+ return candidates;
460
+ }
461
+ if (count === 1) {
462
+ return [candidates[Math.floor((candidates.length - 1) / 2)]];
463
+ }
464
+
465
+ return Array.from({ length: count }, (_, index) => {
466
+ const candidateIndex = Math.round(
467
+ (index * (candidates.length - 1)) / (count - 1),
468
+ );
469
+ return candidates[candidateIndex];
470
+ });
471
+ }
472
+
473
+ function selectRecorderScreenshotCandidates<
474
+ T extends RecorderScreenshotCandidate,
475
+ >(candidates: T[], maxScreenshots: number): T[] {
476
+ if (maxScreenshots <= 0 || candidates.length === 0) {
477
+ return [];
478
+ }
479
+ if (candidates.length <= maxScreenshots) {
480
+ return candidates;
481
+ }
482
+
483
+ const selected = new Map<number, T>();
484
+ const firstEventIndex = candidates[0].eventIndex;
485
+ const lastEventIndex = candidates[candidates.length - 1].eventIndex;
486
+ const addCandidate = (candidate: T | undefined) => {
487
+ if (!candidate || selected.size >= maxScreenshots) {
488
+ return;
489
+ }
490
+ selected.set(candidate.eventIndex, candidate);
491
+ };
492
+ const addEvenly = (pool: T[]) => {
493
+ const remaining = maxScreenshots - selected.size;
494
+ if (remaining <= 0) {
495
+ return;
496
+ }
497
+ const unselected = pool.filter(
498
+ (candidate) => !selected.has(candidate.eventIndex),
499
+ );
500
+ for (const candidate of selectEvenlyDistributedCandidates(
501
+ unselected,
502
+ remaining,
503
+ )) {
504
+ addCandidate(candidate);
505
+ }
506
+ };
507
+
508
+ addCandidate(candidates[0]);
509
+ addCandidate(candidates[candidates.length - 1]);
510
+
511
+ addEvenly(
512
+ candidates.filter(
513
+ (candidate) =>
514
+ getRecorderScreenshotCandidatePriority(
515
+ candidate,
516
+ firstEventIndex,
517
+ lastEventIndex,
518
+ ) >= 60,
519
+ ),
520
+ );
521
+ addEvenly(
522
+ candidates.filter(
523
+ (candidate) =>
524
+ getRecorderScreenshotCandidatePriority(
525
+ candidate,
526
+ firstEventIndex,
527
+ lastEventIndex,
528
+ ) >= 40,
529
+ ),
530
+ );
531
+ addEvenly(candidates);
532
+
533
+ return Array.from(selected.values()).sort(
534
+ (left, right) => left.eventIndex - right.eventIndex,
535
+ );
536
+ }
537
+
538
+ function getRecorderScreenshotCandidates(
539
+ events: MidsceneRecorderEvent[],
540
+ ): RecorderScreenshotCandidate[] {
541
+ const candidates: RecorderScreenshotCandidate[] = [];
542
+ const seenScreenshots = new Set<string>();
543
+ const lastEventIndex = events.length - 1;
544
+
545
+ for (let eventIndex = 0; eventIndex < events.length; eventIndex += 1) {
546
+ const event = events[eventIndex];
547
+ if (!shouldIncludeMarkdownScreenshot(event, eventIndex, lastEventIndex)) {
548
+ continue;
549
+ }
550
+
551
+ const screenshot = getRecorderEventScreenshot(event);
552
+ if (!screenshot || seenScreenshots.has(screenshot)) {
553
+ continue;
554
+ }
555
+
556
+ seenScreenshots.add(screenshot);
557
+ candidates.push({
558
+ event,
559
+ eventIndex,
560
+ screenshot,
561
+ });
562
+ }
563
+
564
+ return candidates;
565
+ }
566
+
567
+ export function createMidsceneRecorderMarkdownScreenshotAssets(
568
+ events: MidsceneRecorderEvent[],
569
+ options: MidsceneRecorderMarkdownScreenshotOptions = {},
570
+ ): MidsceneRecorderMarkdownScreenshotAsset[] {
571
+ const baseDir = normalizeMarkdownAssetBaseDir(options.baseDir);
572
+ const maxScreenshots =
573
+ options.maxScreenshots ??
574
+ DEFAULT_MIDSCENE_RECORDER_MARKDOWN_MAX_SCREENSHOTS;
575
+ const candidates: Array<
576
+ RecorderScreenshotCandidate & {
577
+ parsedScreenshot: NonNullable<ReturnType<typeof parseScreenshotDataUrl>>;
578
+ }
579
+ > = [];
580
+
581
+ for (const candidate of getRecorderScreenshotCandidates(events)) {
582
+ const parsedScreenshot = parseScreenshotDataUrl(candidate.screenshot);
583
+ if (parsedScreenshot) {
584
+ candidates.push({
585
+ ...candidate,
586
+ parsedScreenshot,
587
+ });
588
+ }
589
+ }
590
+
591
+ return selectRecorderScreenshotCandidates(candidates, maxScreenshots).map(
592
+ ({ event, eventIndex, parsedScreenshot }) => {
593
+ const safeType = event.type.replace(/[^a-zA-Z0-9-]/g, '-');
594
+ const fileName = `event-${padEventIndex(eventIndex)}-${safeType}.${parsedScreenshot.extension}`;
595
+ return {
596
+ eventIndex,
597
+ eventHashId: event.hashId,
598
+ eventType: event.type,
599
+ relativePath: `${baseDir}/${fileName}`,
600
+ dataUrl: parsedScreenshot.dataUrl,
601
+ base64Data: parsedScreenshot.base64Data,
602
+ mimeType: parsedScreenshot.mimeType,
603
+ };
604
+ },
605
+ );
606
+ }
607
+
608
+ function scalarToYaml(value: string | number | boolean) {
609
+ return JSON.stringify(value);
610
+ }
611
+
612
+ export function stringifyMidsceneRecorderTargetBlock(
613
+ target: MidsceneRecorderTarget,
614
+ ) {
615
+ const lines = [`${target.platformId}:`];
616
+ const values = Object.entries(target.values);
617
+ if (values.length === 0) {
618
+ lines.push(' {}');
619
+ return lines.join('\n');
620
+ }
621
+ for (const [key, value] of values) {
622
+ lines.push(` ${key}: ${scalarToYaml(value)}`);
623
+ }
624
+ return lines.join('\n');
625
+ }