testdriverai 7.1.4 → 7.2.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.
Files changed (70) hide show
  1. package/.github/workflows/acceptance.yaml +81 -0
  2. package/.github/workflows/publish.yaml +44 -0
  3. package/agent/index.js +18 -19
  4. package/agent/lib/commands.js +321 -121
  5. package/agent/lib/redraw.js +99 -39
  6. package/agent/lib/sandbox.js +98 -6
  7. package/agent/lib/sdk.js +25 -0
  8. package/agent/lib/system.js +2 -1
  9. package/agent/lib/validation.js +6 -6
  10. package/docs/docs.json +211 -101
  11. package/docs/snippets/tests/type-repeated-replay.mdx +1 -1
  12. package/docs/v7/_drafts/caching-selectors.mdx +24 -0
  13. package/docs/v7/api/act.mdx +1 -1
  14. package/docs/v7/api/assert.mdx +1 -1
  15. package/docs/v7/api/assertions.mdx +7 -7
  16. package/docs/v7/api/elements.mdx +78 -0
  17. package/docs/v7/api/find.mdx +38 -0
  18. package/docs/v7/api/focusApplication.mdx +2 -2
  19. package/docs/v7/api/hover.mdx +2 -2
  20. package/docs/v7/features/ai-native.mdx +57 -71
  21. package/docs/v7/features/application-logs.mdx +353 -0
  22. package/docs/v7/features/browser-logs.mdx +414 -0
  23. package/docs/v7/features/cache-management.mdx +402 -0
  24. package/docs/v7/features/continuous-testing.mdx +346 -0
  25. package/docs/v7/features/coverage.mdx +508 -0
  26. package/docs/v7/features/data-driven-testing.mdx +441 -0
  27. package/docs/v7/features/easy-to-write.mdx +2 -73
  28. package/docs/v7/features/enterprise.mdx +155 -39
  29. package/docs/v7/features/fast.mdx +63 -81
  30. package/docs/v7/features/managed-sandboxes.mdx +384 -0
  31. package/docs/v7/features/network-monitoring.mdx +568 -0
  32. package/docs/v7/features/observable.mdx +3 -22
  33. package/docs/v7/features/parallel-execution.mdx +381 -0
  34. package/docs/v7/features/powerful.mdx +1 -1
  35. package/docs/v7/features/reports.mdx +414 -0
  36. package/docs/v7/features/sandbox-customization.mdx +229 -0
  37. package/docs/v7/features/scalable.mdx +217 -2
  38. package/docs/v7/features/stable.mdx +106 -147
  39. package/docs/v7/features/system-performance.mdx +616 -0
  40. package/docs/v7/features/test-analytics.mdx +373 -0
  41. package/docs/v7/features/test-cases.mdx +393 -0
  42. package/docs/v7/features/test-replays.mdx +408 -0
  43. package/docs/v7/features/test-reports.mdx +308 -0
  44. package/docs/v7/getting-started/{running-and-debugging.mdx → debugging-tests.mdx} +12 -142
  45. package/docs/v7/getting-started/quickstart.mdx +22 -305
  46. package/docs/v7/getting-started/running-tests.mdx +173 -0
  47. package/docs/v7/overview/what-is-testdriver.mdx +2 -14
  48. package/docs/v7/presets/chrome-extension.mdx +147 -122
  49. package/interfaces/cli/commands/init.js +3 -3
  50. package/interfaces/cli/lib/base.js +3 -2
  51. package/interfaces/logger.js +0 -2
  52. package/interfaces/shared-test-state.mjs +0 -5
  53. package/interfaces/vitest-plugin.mjs +69 -42
  54. package/lib/core/Dashcam.js +65 -66
  55. package/lib/vitest/hooks.mjs +42 -50
  56. package/package.json +1 -1
  57. package/sdk-log-formatter.js +350 -175
  58. package/sdk.js +431 -116
  59. package/setup/aws/cloudformation.yaml +2 -2
  60. package/setup/aws/self-hosted.yml +1 -1
  61. package/test/testdriver/chrome-extension.test.mjs +55 -72
  62. package/test/testdriver/element-not-found.test.mjs +2 -1
  63. package/test/testdriver/hover-image.test.mjs +1 -1
  64. package/test/testdriver/scroll-until-text.test.mjs +10 -6
  65. package/test/testdriver/setup/lifecycleHelpers.mjs +19 -24
  66. package/test/testdriver/setup/testHelpers.mjs +18 -23
  67. package/vitest.config.mjs +3 -3
  68. package/.github/workflows/linux-tests.yml +0 -28
  69. package/docs/v7/getting-started/generating-tests.mdx +0 -525
  70. package/test/testdriver/auto-cache-key-demo.test.mjs +0 -56
@@ -114,6 +114,7 @@ const createCommands = (
114
114
  const niceSeconds = (ms) => {
115
115
  return Math.round(ms / 1000);
116
116
  };
117
+
117
118
  const delay = (t) => new Promise((resolve) => setTimeout(resolve, t));
118
119
 
119
120
  const findImageOnScreen = async (
@@ -214,12 +215,15 @@ const createCommands = (
214
215
  };
215
216
 
216
217
  const assert = async (assertion, shouldThrow = false) => {
218
+ let assertStartTimeForHandler;
217
219
  const handleAssertResponse = (response) => {
218
- emitter.emit(events.log.log, response);
219
-
220
- let valid = response.indexOf("The task passed") > -1;
220
+ const { formatter } = require("../../sdk-log-formatter.js");
221
+ const passed = response.indexOf("The task passed") > -1;
222
+ const duration = assertStartTimeForHandler ? Date.now() - assertStartTimeForHandler : undefined;
223
+
224
+ emitter.emit(events.log.narration, formatter.formatAssertResult(passed, response, duration), true);
221
225
 
222
- if (valid) {
226
+ if (passed) {
223
227
  return true;
224
228
  } else {
225
229
  if (shouldThrow) {
@@ -237,9 +241,12 @@ const createCommands = (
237
241
  const assertingMessage = formatter.formatAsserting(assertion);
238
242
  emitter.emit(events.log.log, assertingMessage);
239
243
 
240
- emitter.emit(events.log.narration, `thinking...`);
241
-
242
- const assertStartTime = Date.now();
244
+ // Capture absolute timestamp at the very start of the command
245
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
246
+ const assertTimestamp = Date.now();
247
+ const assertStartTime = assertTimestamp;
248
+ assertStartTimeForHandler = assertStartTime;
249
+
243
250
  let response = await sdk.req("assert", {
244
251
  expect: assertion,
245
252
  image: await system.captureScreenBase64(),
@@ -258,7 +265,7 @@ const createCommands = (
258
265
  interactionType: "assert",
259
266
  session: sessionId,
260
267
  prompt: assertion,
261
- timestamp: assertStartTime,
268
+ timestamp: assertTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
262
269
  duration: assertDuration,
263
270
  success: assertionPassed,
264
271
  error: assertionPassed ? undefined : response.data,
@@ -283,6 +290,10 @@ const createCommands = (
283
290
  * @param {number} [options.redraw.diffThreshold=0.1] - Screen diff threshold percentage
284
291
  */
285
292
  const scroll = async (direction = 'down', options = {}) => {
293
+ // Capture absolute timestamp at the very start of the command
294
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
295
+ const scrollTimestamp = Date.now();
296
+ const scrollStartTime = scrollTimestamp;
286
297
  // Convert number to object format
287
298
  if (typeof options === 'number') {
288
299
  options = { amount: options };
@@ -290,50 +301,121 @@ const createCommands = (
290
301
 
291
302
  let { amount = 300 } = options;
292
303
  const redrawOptions = extractRedrawOptions(options);
293
-
294
- emitter.emit(
295
- events.log.narration,
296
- theme.dim(`scrolling ${direction} ${amount}px...`),
297
- );
298
304
 
299
305
  await redraw.start(redrawOptions);
300
306
 
301
307
  amount = parseInt(amount, 10);
302
308
 
303
309
  const before = await system.captureScreenBase64();
304
- switch (direction) {
305
- case "up":
306
- await sandbox.send({
307
- type: "scroll",
308
- amount,
309
- direction,
310
- });
311
- await redraw.wait(2500, redrawOptions);
312
- break;
313
- case "down":
314
- await sandbox.send({
315
- type: "scroll",
316
- amount,
317
- direction,
318
- });
319
- await redraw.wait(2500, redrawOptions);
320
- break;
321
- case "left":
322
- console.error("Not Supported");
323
- break;
324
- case "right":
325
- console.error("Not Supported");
326
- break;
327
- default:
328
- throw new CommandError("Direction not found");
329
- }
330
- const after = await system.captureScreenBase64();
310
+ let scrollSuccess = true;
311
+ let scrollError;
312
+ let actionEndTime;
313
+
314
+ try {
315
+ switch (direction) {
316
+ case "up":
317
+ await sandbox.send({
318
+ type: "scroll",
319
+ amount,
320
+ direction,
321
+ });
322
+ actionEndTime = Date.now();
323
+ break;
324
+ case "down":
325
+ await sandbox.send({
326
+ type: "scroll",
327
+ amount,
328
+ direction,
329
+ });
330
+ actionEndTime = Date.now();
331
+ break;
332
+ case "left":
333
+ console.error("Not Supported");
334
+ scrollSuccess = false;
335
+ scrollError = "Left scroll not supported";
336
+ break;
337
+ case "right":
338
+ console.error("Not Supported");
339
+ scrollSuccess = false;
340
+ scrollError = "Right scroll not supported";
341
+ break;
342
+ default:
343
+ scrollSuccess = false;
344
+ scrollError = "Direction not found";
345
+ throw new CommandError("Direction not found");
346
+ }
347
+
348
+ const actionDuration = actionEndTime ? actionEndTime - scrollStartTime : Date.now() - scrollStartTime;
349
+
350
+ // Log nested scroll action completion
351
+ const { formatter } = require("../../sdk-log-formatter.js");
352
+ emitter.emit(
353
+ events.log.narration,
354
+ formatter.formatScrollResult(direction, amount, actionDuration),
355
+ true,
356
+ );
357
+
358
+ // Wait for redraw and track duration
359
+ const redrawStartTime = Date.now();
360
+ await redraw.wait(2500, redrawOptions);
361
+ const redrawDuration = Date.now() - redrawStartTime;
362
+
363
+ const after = await system.captureScreenBase64();
331
364
 
332
- if (before === after) {
365
+ if (before === after) {
366
+ emitter.emit(
367
+ events.log.warn,
368
+ "Attempted to scroll, but the screen did not change. You may need to click a non-interactive element to focus the scrollable area first.",
369
+ );
370
+ }
371
+
372
+ // Log nested redraw completion
333
373
  emitter.emit(
334
- events.log.warn,
335
- "Attempted to scroll, but the screen did not change. You may need to click a non-interactive element to focus the scrollable area first.",
374
+ events.log.narration,
375
+ formatter.formatRedrawComplete(redrawDuration),
376
+ true,
336
377
  );
378
+
379
+ // Track interaction success
380
+ const sessionId = sessionInstance?.get();
381
+ if (sessionId) {
382
+ try {
383
+ const scrollDuration = Date.now() - scrollStartTime;
384
+ await sandbox.send({
385
+ type: "trackInteraction",
386
+ interactionType: "scroll",
387
+ session: sessionId,
388
+ input: { direction, amount },
389
+ timestamp: scrollTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
390
+ duration: scrollDuration,
391
+ success: scrollSuccess,
392
+ error: scrollError,
393
+ });
394
+ } catch (err) {
395
+ console.warn("Failed to track scroll interaction:", err.message);
396
+ }
397
+ }
398
+ } catch (error) {
399
+ // Track interaction failure
400
+ const sessionId = sessionInstance?.get();
401
+ if (sessionId) {
402
+ try {
403
+ const scrollDuration = Date.now() - scrollStartTime;
404
+ await sandbox.send({
405
+ type: "trackInteraction",
406
+ interactionType: "scroll",
407
+ session: sessionId,
408
+ input: { direction, amount },
409
+ timestamp: scrollTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
410
+ duration: scrollDuration,
411
+ success: false,
412
+ error: error.message,
413
+ });
414
+ } catch (err) {
415
+ console.warn("Failed to track scroll interaction:", err.message);
416
+ }
417
+ }
418
+ throw error;
337
419
  }
338
420
  };
339
421
 
@@ -354,7 +436,10 @@ const createCommands = (
354
436
  * @param {number} [options.redraw.diffThreshold=0.1] - Screen diff threshold percentage
355
437
  */
356
438
  const click = async (...args) => {
357
- const clickStartTime = Date.now();
439
+ // Capture absolute timestamp at the very start of the command
440
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
441
+ const clickTimestamp = Date.now();
442
+ const clickStartTime = clickTimestamp;
358
443
  let x, y, action, elementData, redrawOptions;
359
444
 
360
445
  // Handle both object and positional argument styles
@@ -384,37 +469,35 @@ const createCommands = (
384
469
  double = true;
385
470
  }
386
471
 
387
- emitter.emit(
388
- events.log.narration,
389
- theme.dim(`${action} ${button} clicking at ${x}, ${y}...`),
390
- true,
391
- );
472
+ // Show nested action details
473
+ const actionText = action.split("-").join("");
474
+ const clickActionLogStart = Date.now();
392
475
 
393
476
  x = parseInt(x);
394
477
  y = parseInt(y);
395
478
 
396
- // Add dashcam timestamp if available
397
- if (getDashcamElapsedTime) {
398
- const elapsed = getDashcamElapsedTime();
399
- if (elapsed !== null) {
400
- elementData.timestamp = elapsed;
401
- }
402
- }
479
+ // Add absolute timestamp for sandbox events
480
+ elementData.timestamp = Date.now();
403
481
 
404
482
  await sandbox.send({ type: "moveMouse", x, y, ...elementData });
405
483
 
406
484
  emitter.emit(events.mouseMove, { x, y });
407
485
 
408
486
  await delay(2500); // wait for the mouse to move
487
+
488
+ // Update the action log with duration
489
+ const clickMoveEndTime = Date.now();
490
+ const { formatter } = require("../../sdk-log-formatter.js");
491
+ emitter.emit(
492
+ events.log.narration,
493
+ formatter.formatClickResult(button, x, y, clickMoveEndTime - clickActionLogStart),
494
+ true,
495
+ );
409
496
 
410
497
  if (action !== "hover") {
411
498
  // Update timestamp for the actual click action
412
- if (getDashcamElapsedTime) {
413
- const elapsed = getDashcamElapsedTime();
414
- if (elapsed !== null) {
415
- elementData.timestamp = elapsed;
416
- }
417
- }
499
+ elementData.timestamp = Date.now();
500
+
418
501
 
419
502
  if (action === "click" || action === "left-click") {
420
503
  await sandbox.send({ type: "leftClick", x, y, ...elementData });
@@ -438,19 +521,22 @@ const createCommands = (
438
521
 
439
522
  emitter.emit(events.mouseClick, { x, y, button, click, double });
440
523
 
524
+ // Track action duration (before redraw wait)
525
+ const actionEndTime = Date.now();
526
+ const actionDuration = actionEndTime - clickStartTime;
527
+
441
528
  // Track interaction
442
529
  const sessionId = sessionInstance?.get();
443
530
  if (sessionId && elementData.prompt) {
444
531
  try {
445
- const clickDuration = Date.now() - clickStartTime;
446
532
  await sandbox.send({
447
533
  type: "trackInteraction",
448
534
  interactionType: "click",
449
535
  session: sessionId,
450
536
  prompt: elementData.prompt,
451
537
  input: { x, y, action },
452
- timestamp: clickStartTime,
453
- duration: clickDuration,
538
+ timestamp: clickTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
539
+ duration: actionDuration,
454
540
  success: true,
455
541
  cacheHit: elementData.cacheHit,
456
542
  selector: elementData.selector,
@@ -460,10 +546,33 @@ const createCommands = (
460
546
  console.warn("Failed to track click interaction:", err.message);
461
547
  }
462
548
  }
549
+
550
+ // Wait for redraw and track duration
551
+ const redrawStartTime = Date.now();
552
+ await redraw.wait(5000, redrawOptions);
553
+ const redrawDuration = Date.now() - redrawStartTime;
554
+
555
+ // Log nested redraw completion
556
+ emitter.emit(
557
+ events.log.narration,
558
+ formatter.formatRedrawComplete(redrawDuration),
559
+ true,
560
+ );
561
+ } else {
562
+ // For hover action (within click function)
563
+ const redrawStartTime = Date.now();
564
+ await redraw.wait(5000, redrawOptions);
565
+ const redrawDuration = Date.now() - redrawStartTime;
566
+ const actionDuration = Date.now() - clickStartTime - redrawDuration;
567
+
568
+ // Log nested redraw completion
569
+ emitter.emit(
570
+ events.log.narration,
571
+ formatter.formatRedrawComplete(redrawDuration),
572
+ true,
573
+ );
463
574
  }
464
575
 
465
- await redraw.wait(5000, redrawOptions);
466
-
467
576
  return;
468
577
  } catch (error) {
469
578
  // Track interaction failure
@@ -477,7 +586,7 @@ const createCommands = (
477
586
  session: sessionId,
478
587
  prompt: elementData.prompt,
479
588
  input: { x, y, action },
480
- timestamp: clickStartTime,
589
+ timestamp: clickTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
481
590
  duration: clickDuration,
482
591
  success: false,
483
592
  error: error.message,
@@ -504,7 +613,10 @@ const createCommands = (
504
613
  * @param {boolean} [options.selectorUsed] - Whether selector was used
505
614
  */
506
615
  const hover = async (...args) => {
507
- const hoverStartTime = Date.now();
616
+ // Capture absolute timestamp at the very start of the command
617
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
618
+ const hoverTimestamp = Date.now();
619
+ const hoverStartTime = hoverTimestamp;
508
620
  let x, y, elementData, redrawOptions;
509
621
 
510
622
  // Handle both object and positional argument styles
@@ -528,29 +640,26 @@ const createCommands = (
528
640
  x = parseInt(x);
529
641
  y = parseInt(y);
530
642
 
531
- // Add dashcam timestamp if available
532
- if (getDashcamElapsedTime) {
533
- const elapsed = getDashcamElapsedTime();
534
- if (elapsed !== null) {
535
- elementData.timestamp = elapsed;
536
- }
537
- }
643
+ // Add absolute timestamp for sandbox events
644
+ elementData.timestamp = Date.now();
538
645
 
539
646
  await sandbox.send({ type: "moveMouse", x, y, ...elementData });
540
647
 
541
648
  // Track interaction
542
649
  const sessionId = sessionInstance?.get();
650
+ const actionEndTime = Date.now();
651
+ const actionDuration = actionEndTime - hoverStartTime;
652
+
543
653
  if (sessionId && elementData.prompt) {
544
654
  try {
545
- const hoverDuration = Date.now() - hoverStartTime;
546
655
  await sandbox.send({
547
656
  type: "trackInteraction",
548
657
  interactionType: "hover",
549
658
  session: sessionId,
550
659
  prompt: elementData.prompt,
551
660
  input: { x, y },
552
- timestamp: hoverStartTime,
553
- duration: hoverDuration,
661
+ timestamp: hoverTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
662
+ duration: actionDuration,
554
663
  success: true,
555
664
  cacheHit: elementData.cacheHit,
556
665
  selector: elementData.selector,
@@ -561,7 +670,19 @@ const createCommands = (
561
670
  }
562
671
  }
563
672
 
673
+ // Wait for redraw and track duration
674
+ const redrawStartTime = Date.now();
564
675
  await redraw.wait(2500, redrawOptions);
676
+ const redrawDuration = Date.now() - redrawStartTime;
677
+
678
+ // Log action completion with separate durations
679
+ const { formatter } = require("../../sdk-log-formatter.js");
680
+ const completionMessage = formatter.formatActionComplete("hover", elementData.prompt, {
681
+ actionDuration,
682
+ redrawDuration,
683
+ cacheHit: elementData.cacheHit,
684
+ });
685
+ emitter.emit(events.log.log, completionMessage);
565
686
 
566
687
  return;
567
688
  } catch (error) {
@@ -576,7 +697,7 @@ const createCommands = (
576
697
  session: sessionId,
577
698
  prompt: elementData.prompt,
578
699
  input: { x, y },
579
- timestamp: hoverStartTime,
700
+ timestamp: hoverTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
580
701
  duration: hoverDuration,
581
702
  success: false,
582
703
  error: error.message,
@@ -754,32 +875,35 @@ const createCommands = (
754
875
  * @param {number} [options.redraw.diffThreshold=0.1] - Screen diff threshold percentage
755
876
  */
756
877
  "type": async (text, options = {}) => {
757
- const typeStartTime = Date.now();
878
+ const { formatter } = require("../../sdk-log-formatter.js");
879
+ // Capture absolute timestamp at the very start of the command
880
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
881
+ const typeTimestamp = Date.now();
882
+ const typeStartTime = typeTimestamp;
758
883
  const { delay = 250, secret = false, redraw: redrawOpts, ...elementData } = options;
759
884
  const redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...options });
760
885
 
761
- // Log masked version if secret, otherwise show actual text
886
+ // Log parent action with text
762
887
  if (secret) {
763
- emitter.emit(events.log.narration, theme.dim(`typing secret "****"...`));
888
+ emitter.emit(events.log.narration, formatter.getPrefix("type") + " " + theme.yellow.bold("Type") + " " + theme.dim(`secret "****"`));
764
889
  } else {
765
- emitter.emit(events.log.narration, theme.dim(`typing "${text}"...`));
890
+ emitter.emit(events.log.narration, formatter.getPrefix("type") + " " + theme.yellow.bold("Type") + " " + theme.cyan(`"${text}"`));
766
891
  }
767
892
 
768
893
  await redraw.start(redrawOptions);
769
894
 
770
895
  text = text.toString();
771
896
 
772
- // Add dashcam timestamp if available
773
- if (getDashcamElapsedTime) {
774
- const elapsed = getDashcamElapsedTime();
775
- if (elapsed !== null) {
776
- elementData.timestamp = elapsed;
777
- }
778
- }
897
+ // Add absolute timestamp for sandbox events
898
+ elementData.timestamp = Date.now();
779
899
 
780
900
  // Actually type the text in the sandbox
781
901
  await sandbox.send({ type: "write", text, delay, ...elementData });
782
902
 
903
+ // Update the action log with duration
904
+ const typeActionEndTime = Date.now();
905
+ emitter.emit(events.log.narration, formatter.formatTypeResult(text, secret, typeActionEndTime - typeStartTime), true);
906
+
783
907
  // Track interaction
784
908
  const sessionId = sessionInstance?.get();
785
909
  if (sessionId) {
@@ -791,7 +915,7 @@ const createCommands = (
791
915
  session: sessionId,
792
916
  // Store masked text if secret, otherwise store actual text
793
917
  input: { text: secret ? "****" : text, delay },
794
- timestamp: typeStartTime,
918
+ timestamp: typeTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
795
919
  duration: typeDuration,
796
920
  success: true,
797
921
  isSecret: secret, // Flag this interaction if it contains a secret
@@ -801,7 +925,17 @@ const createCommands = (
801
925
  }
802
926
  }
803
927
 
928
+ const redrawStartTime = Date.now();
804
929
  await redraw.wait(5000, redrawOptions);
930
+ const redrawDuration = Date.now() - redrawStartTime;
931
+
932
+ // Log nested redraw completion
933
+ emitter.emit(
934
+ events.log.narration,
935
+ formatter.formatRedrawComplete(redrawDuration),
936
+ true,
937
+ );
938
+
805
939
  return;
806
940
  },
807
941
  /**
@@ -815,20 +949,36 @@ const createCommands = (
815
949
  * @param {number} [options.redraw.diffThreshold=0.1] - Screen diff threshold percentage
816
950
  */
817
951
  "press-keys": async (keys, options = {}) => {
818
- const pressKeysStartTime = Date.now();
952
+ const { formatter } = require("../../sdk-log-formatter.js");
953
+ // Capture absolute timestamp at the very start of the command
954
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
955
+ const pressKeysTimestamp = Date.now();
956
+ const pressKeysStartTime = pressKeysTimestamp;
819
957
  const redrawOptions = extractRedrawOptions(options);
958
+ const keysDisplay = Array.isArray(keys) ? keys.join(", ") : keys;
959
+
960
+ // Log parent action
820
961
  emitter.emit(
821
962
  events.log.narration,
822
- theme.dim(
823
- `pressing keys: ${Array.isArray(keys) ? keys.join(", ") : keys}...`,
824
- ),
963
+ formatter.getPrefix("pressKeys") + " " + theme.yellow.bold("PressKeys") + " " + theme.cyan(`${keysDisplay}`),
825
964
  );
826
965
 
827
966
  await redraw.start(redrawOptions);
828
967
 
968
+ // Log nested action details
969
+ const pressKeysActionLogStart = Date.now();
970
+
829
971
  // finally, press the keys
830
972
  await sandbox.send({ type: "press", keys });
831
973
 
974
+ // Update the action log with duration
975
+ const pressKeysActionEndTime = Date.now();
976
+ emitter.emit(
977
+ events.log.narration,
978
+ formatter.formatPressKeysResult(keysDisplay, pressKeysActionEndTime - pressKeysActionLogStart),
979
+ true,
980
+ );
981
+
832
982
  // Track interaction
833
983
  const sessionId = sessionInstance?.get();
834
984
  if (sessionId) {
@@ -839,7 +989,7 @@ const createCommands = (
839
989
  interactionType: "pressKeys",
840
990
  session: sessionId,
841
991
  input: { keys },
842
- timestamp: pressKeysStartTime,
992
+ timestamp: pressKeysTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
843
993
  duration: pressKeysDuration,
844
994
  success: true,
845
995
  });
@@ -848,7 +998,16 @@ const createCommands = (
848
998
  }
849
999
  }
850
1000
 
1001
+ const redrawStartTime = Date.now();
851
1002
  await redraw.wait(5000, redrawOptions);
1003
+ const redrawDuration = Date.now() - redrawStartTime;
1004
+
1005
+ // Log nested redraw completion
1006
+ emitter.emit(
1007
+ events.log.narration,
1008
+ formatter.formatRedrawComplete(redrawDuration),
1009
+ true,
1010
+ );
852
1011
 
853
1012
  return;
854
1013
  },
@@ -858,7 +1017,10 @@ const createCommands = (
858
1017
  * @param {Object} [options] - Additional options (reserved for future use)
859
1018
  */
860
1019
  "wait": async (timeout = 3000, options = {}) => {
861
- const waitStartTime = Date.now();
1020
+ // Capture absolute timestamp at the very start of the command
1021
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
1022
+ const waitTimestamp = Date.now();
1023
+ const waitStartTime = waitTimestamp;
862
1024
  emitter.emit(events.log.narration, theme.dim(`waiting ${timeout}ms...`));
863
1025
  const result = await delay(timeout);
864
1026
 
@@ -872,7 +1034,7 @@ const createCommands = (
872
1034
  interactionType: "wait",
873
1035
  session: sessionId,
874
1036
  input: { timeout },
875
- timestamp: waitStartTime,
1037
+ timestamp: waitTimestamp, // Use dashcam elapsed time instead of absolute time
876
1038
  duration: waitDuration,
877
1039
  success: true,
878
1040
  });
@@ -890,6 +1052,9 @@ const createCommands = (
890
1052
  * @param {number} [options.timeout=10000] - Timeout in milliseconds
891
1053
  */
892
1054
  "wait-for-image": async (...args) => {
1055
+ // Capture absolute timestamp at the very start of the command
1056
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
1057
+ const waitForImageTimestamp = Date.now();
893
1058
  let description, timeout;
894
1059
 
895
1060
  // Handle both object and positional argument styles
@@ -951,7 +1116,7 @@ const createCommands = (
951
1116
  session: sessionId,
952
1117
  prompt: description,
953
1118
  input: { timeout },
954
- timestamp: startTime,
1119
+ timestamp: waitForImageTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
955
1120
  duration: waitForImageDuration,
956
1121
  success: true,
957
1122
  });
@@ -974,7 +1139,7 @@ const createCommands = (
974
1139
  session: sessionId,
975
1140
  prompt: description,
976
1141
  input: { timeout },
977
- timestamp: startTime,
1142
+ timestamp: waitForImageTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
978
1143
  duration: waitForImageDuration,
979
1144
  success: false,
980
1145
  error: errorMsg,
@@ -999,6 +1164,9 @@ const createCommands = (
999
1164
  * @param {number} [options.redraw.diffThreshold=0.1] - Screen diff threshold percentage
1000
1165
  */
1001
1166
  "wait-for-text": async (...args) => {
1167
+ // Capture absolute timestamp at the very start of the command
1168
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
1169
+ const waitForTextTimestamp = Date.now();
1002
1170
  let text, timeout, redrawOptions;
1003
1171
 
1004
1172
  // Handle both object and positional argument styles
@@ -1061,7 +1229,7 @@ const createCommands = (
1061
1229
  session: sessionId,
1062
1230
  prompt: text,
1063
1231
  input: { timeout },
1064
- timestamp: startTime,
1232
+ timestamp: waitForTextTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
1065
1233
  duration: waitForTextDuration,
1066
1234
  success: true,
1067
1235
  });
@@ -1084,7 +1252,7 @@ const createCommands = (
1084
1252
  session: sessionId,
1085
1253
  prompt: text,
1086
1254
  input: { timeout },
1087
- timestamp: startTime,
1255
+ timestamp: waitForTextTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
1088
1256
  duration: waitForTextDuration,
1089
1257
  success: false,
1090
1258
  error: errorMsg,
@@ -1281,7 +1449,10 @@ const createCommands = (
1281
1449
  * @param {string} options.description - What to extract
1282
1450
  */
1283
1451
  "extract": async (...args) => {
1284
- const rememberStartTime = Date.now();
1452
+ // Capture absolute timestamp at the very start of the command
1453
+ // Frontend will calculate relative time using: timestamp - replay.clientStartDate
1454
+ const rememberTimestamp = Date.now();
1455
+ const rememberStartTime = rememberTimestamp;
1285
1456
  let description;
1286
1457
 
1287
1458
  // Handle both object and positional argument styles
@@ -1305,15 +1476,15 @@ const createCommands = (
1305
1476
  const rememberDuration = Date.now() - rememberStartTime;
1306
1477
  await sandbox.send({
1307
1478
  type: "trackInteraction",
1308
- interactionType: "remember",
1479
+ interactionType: "extract",
1309
1480
  session: sessionId,
1310
1481
  prompt: description,
1311
- timestamp: rememberStartTime,
1482
+ timestamp: rememberTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
1312
1483
  duration: rememberDuration,
1313
1484
  success: true,
1314
1485
  });
1315
1486
  } catch (err) {
1316
- console.warn("Failed to track remember interaction:", err.message);
1487
+ console.warn("Failed to track extract interaction:", err.message);
1317
1488
  }
1318
1489
  }
1319
1490
 
@@ -1326,16 +1497,16 @@ const createCommands = (
1326
1497
  const rememberDuration = Date.now() - rememberStartTime;
1327
1498
  await sandbox.send({
1328
1499
  type: "trackInteraction",
1329
- interactionType: "remember",
1500
+ interactionType: "extract",
1330
1501
  session: sessionId,
1331
1502
  prompt: description,
1332
- timestamp: rememberStartTime,
1503
+ timestamp: rememberTimestamp, // Absolute epoch timestamp - frontend calculates relative using clientStartDate
1333
1504
  duration: rememberDuration,
1334
1505
  success: false,
1335
1506
  error: error.message,
1336
1507
  });
1337
1508
  } catch (err) {
1338
- console.warn("Failed to track remember interaction:", err.message);
1509
+ console.warn("Failed to track extract interaction:", err.message);
1339
1510
  }
1340
1511
  }
1341
1512
  throw error;
@@ -1360,6 +1531,7 @@ const createCommands = (
1360
1531
  * @param {boolean} [options.silent=false] - Suppress output
1361
1532
  */
1362
1533
  "exec": async (...args) => {
1534
+ const { formatter } = require("../../sdk-log-formatter.js");
1363
1535
  let language, code, timeout, silent;
1364
1536
 
1365
1537
  // Handle both object and positional argument styles
@@ -1370,9 +1542,13 @@ const createCommands = (
1370
1542
  [language = 'pwsh', code, timeout, silent = false] = args;
1371
1543
  }
1372
1544
 
1373
- emitter.emit(events.log.narration, theme.dim(`calling exec...`), true);
1545
+ // Log parent action
1546
+ emitter.emit(events.log.narration, formatter.getPrefix("action") + " " + theme.cyan.bold("Exec") + " " + theme.magenta(`[${language}]`), true);
1374
1547
 
1375
- emitter.emit(events.log.log, code);
1548
+ // Log nested command details (truncate to first line)
1549
+ const firstLine = code.split('\n')[0];
1550
+ const codeDisplay = code.includes('\n') ? firstLine + '...' : firstLine;
1551
+ emitter.emit(events.log.log, formatter.formatCodeLine(codeDisplay));
1376
1552
 
1377
1553
  let plat = system.platform();
1378
1554
 
@@ -1399,27 +1575,48 @@ const createCommands = (
1399
1575
  language = "pwsh";
1400
1576
  }
1401
1577
 
1578
+ const execActionLogStart = Date.now();
1579
+
1402
1580
  let result = null;
1403
1581
 
1404
1582
  result = await sandbox.send({
1405
1583
  type: "commands.run",
1406
1584
  command: code,
1407
1585
  timeout,
1408
- });
1586
+ }, timeout || 300000);
1587
+
1588
+ const execActionEndTime = Date.now();
1589
+ const execDuration = execActionEndTime - execActionLogStart;
1590
+
1591
+ // const debugMode = process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
1592
+ // if (debugMode) {
1593
+ // console.log(result);
1594
+ // }
1409
1595
 
1410
1596
  if (result.out && result.out.returncode !== 0) {
1597
+ emitter.emit(
1598
+ events.log.narration,
1599
+ formatter.formatExecComplete(result.out.returncode, execDuration),
1600
+ true,
1601
+ );
1411
1602
  throw new MatchError(
1412
1603
  `Command failed with exit code ${result.out.returncode}: ${result.out.stderr}`,
1413
1604
  );
1414
1605
  } else {
1606
+ emitter.emit(
1607
+ events.log.narration,
1608
+ formatter.formatExecComplete(0, execDuration),
1609
+ true,
1610
+ );
1611
+
1415
1612
  if (!silent && result.out?.stdout) {
1416
- emitter.emit(events.log.log, theme.dim(`stdout:`), true);
1417
- emitter.emit(events.log.log, `${result.out.stdout}`, true);
1613
+ emitter.emit(events.log.log, theme.dim(` stdout:`), true);
1614
+ emitter.emit(events.log.log, theme.dim(` ${result.out.stdout}`), true);
1418
1615
  }
1419
1616
 
1420
1617
  if (!silent && result.out.stderr) {
1421
- emitter.emit(events.log.log, theme.dim(`stderr:`), true);
1422
- emitter.emit(events.log.log, `${result.out.stderr}`, true);
1618
+ emitter.emit(events.log.log, theme.dim(` stderr:`), true);
1619
+ emitter.emit(events.log.log, theme.dim(` ${result.out.stderr}`), true);
1423
1620
  }
1424
1621
 
1425
1622
  return result.out?.stdout?.trim();
@@ -1476,10 +1673,13 @@ const createCommands = (
1476
1673
  if (!stepResult) {
1477
1674
  emitter.emit(events.log.log, `No result returned from script`, true);
1478
1675
  } else {
1479
- if (!silent) {
1480
- emitter.emit(events.log.log, theme.dim(`Result:`), true);
1481
- emitter.emit(events.log.log, stepResult, true);
1482
- }
1676
+ /* The above JavaScript code is checking if the variable `silent` is falsy (not true) and if
1677
+ so, it emits log events using an emitter. The emitted log events include the
1678
+ theme.dim(`Result:`) and the value of the `stepResult` variable. */
1679
+ // if (!silent) {
1680
+ // emitter.emit(events.log.log, theme.dim(`Result:`), true);
1681
+ // emitter.emit(events.log.log, stepResult, true);
1682
+ // }
1483
1683
  }
1484
1684
 
1485
1685
  return stepResult;