@tangle-network/browser-agent-driver 0.24.2 → 0.26.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 (66) hide show
  1. package/dist/brain/index.d.ts.map +1 -1
  2. package/dist/brain/index.js +1 -0
  3. package/dist/brain/index.js.map +1 -1
  4. package/dist/cli-preview.d.ts +59 -0
  5. package/dist/cli-preview.d.ts.map +1 -0
  6. package/dist/cli-preview.js +144 -0
  7. package/dist/cli-preview.js.map +1 -0
  8. package/dist/cli-share.d.ts +77 -0
  9. package/dist/cli-share.d.ts.map +1 -0
  10. package/dist/cli-share.js +141 -0
  11. package/dist/cli-share.js.map +1 -0
  12. package/dist/cli-ui.d.ts.map +1 -1
  13. package/dist/cli-ui.js +8 -4
  14. package/dist/cli-ui.js.map +1 -1
  15. package/dist/cli.js +153 -5
  16. package/dist/cli.js.map +1 -1
  17. package/dist/drivers/cursor-overlay.d.ts +18 -10
  18. package/dist/drivers/cursor-overlay.d.ts.map +1 -1
  19. package/dist/drivers/cursor-overlay.js +221 -30
  20. package/dist/drivers/cursor-overlay.js.map +1 -1
  21. package/dist/drivers/overlay-label.d.ts +33 -0
  22. package/dist/drivers/overlay-label.d.ts.map +1 -0
  23. package/dist/drivers/overlay-label.js +92 -0
  24. package/dist/drivers/overlay-label.js.map +1 -0
  25. package/dist/drivers/playwright.d.ts +22 -0
  26. package/dist/drivers/playwright.d.ts.map +1 -1
  27. package/dist/drivers/playwright.js +87 -9
  28. package/dist/drivers/playwright.js.map +1 -1
  29. package/dist/drivers/snapshot.d.ts +12 -0
  30. package/dist/drivers/snapshot.d.ts.map +1 -1
  31. package/dist/drivers/snapshot.js +17 -0
  32. package/dist/drivers/snapshot.js.map +1 -1
  33. package/dist/drivers/types.d.ts +8 -0
  34. package/dist/drivers/types.d.ts.map +1 -1
  35. package/dist/runner/fan-out.d.ts +91 -0
  36. package/dist/runner/fan-out.d.ts.map +1 -0
  37. package/dist/runner/fan-out.js +183 -0
  38. package/dist/runner/fan-out.js.map +1 -0
  39. package/dist/runner/interrupt-controller.d.ts +67 -0
  40. package/dist/runner/interrupt-controller.d.ts.map +1 -0
  41. package/dist/runner/interrupt-controller.js +142 -0
  42. package/dist/runner/interrupt-controller.js.map +1 -0
  43. package/dist/runner/overlay-narration.d.ts +83 -0
  44. package/dist/runner/overlay-narration.d.ts.map +1 -0
  45. package/dist/runner/overlay-narration.js +172 -0
  46. package/dist/runner/overlay-narration.js.map +1 -0
  47. package/dist/runner/runner.d.ts +9 -0
  48. package/dist/runner/runner.d.ts.map +1 -1
  49. package/dist/runner/runner.js +93 -0
  50. package/dist/runner/runner.js.map +1 -1
  51. package/dist/runner/stream-webhook.d.ts +70 -0
  52. package/dist/runner/stream-webhook.d.ts.map +1 -0
  53. package/dist/runner/stream-webhook.js +132 -0
  54. package/dist/runner/stream-webhook.js.map +1 -0
  55. package/dist/skills/macro-loader.d.ts +1 -1
  56. package/dist/skills/macro-loader.d.ts.map +1 -1
  57. package/dist/supervisor/policy.js +11 -0
  58. package/dist/supervisor/policy.js.map +1 -1
  59. package/dist/test-runner.d.ts +7 -0
  60. package/dist/test-runner.d.ts.map +1 -1
  61. package/dist/test-runner.js +3 -0
  62. package/dist/test-runner.js.map +1 -1
  63. package/dist/types.d.ts +36 -1
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/viewer/viewer.html +264 -16
  66. package/package.json +2 -2
@@ -658,32 +658,280 @@
658
658
  });
659
659
  }
660
660
 
661
- /** Render the recording.webm video player for an agent test. */
661
+ /** Render the recording.webm video player for an agent test.
662
+ * Gen 32: frame-accurate timeline scrubber — video time is synced
663
+ * with per-turn markers; clicking a turn jumps to that moment;
664
+ * the agent's reasoning for the selected turn renders below the
665
+ * video. Turn times are computed from cumulative turn.durationMs,
666
+ * which is a close proxy for wall-clock video time because the
667
+ * recording and the turn loop run on the same clock. */
662
668
  function showRecording(test) {
663
669
  const main = el('main-content');
664
670
  if (!test?.recording) {
665
671
  main.innerHTML = '<div class="empty-state"><h2>No recording</h2><p>This test did not produce a recording.webm.</p></div>';
666
672
  return;
667
673
  }
674
+ const turns = test.turns || [];
675
+ // Build cumulative start times per turn in seconds. The first turn
676
+ // starts at t=0; subsequent turns start at the sum of prior durations.
677
+ // Turns without durationMs (legacy runs) get evenly distributed.
678
+ const totalMs = test.durationMs || turns.reduce((s, t) => s + (t.durationMs || 0), 0);
679
+ let cursor = 0;
680
+ const turnMarkers = turns.map((t) => {
681
+ const start = cursor;
682
+ cursor += t.durationMs || (totalMs / Math.max(1, turns.length));
683
+ return { start, dur: t.durationMs || (totalMs / Math.max(1, turns.length)), turn: t };
684
+ });
685
+ const durationSec = Math.max(1, Math.round((totalMs || 1) / 1000));
686
+ const stepCount = turns.length;
687
+
688
+ // Gen 33: two-column layout — video + timeline + reasoning on the
689
+ // left, decision-list sidebar on the right. The decision list is
690
+ // the primary scrub interface: each row shows the turn number,
691
+ // the verb, the target, a status chip, and (on click) fork-from-
692
+ // this-turn affordance. Click any row = video seek + reasoning update.
668
693
  main.innerHTML = `
669
- <div style="width: 100%; max-width: 1200px;">
670
- <div style="margin-bottom: 16px;">
671
- <h2 style="margin: 0; font-size: 18px;">${escapeHtml(test.name || test.id)} — Recording</h2>
672
- <div style="color: var(--text-muted); font-size: 12px; margin-top: 4px;">
673
- Frame-by-frame screencast of the agent session. Cursor + clicks are visible inline.
694
+ <div style="width: 100%; max-width: 1200px; display: grid; grid-template-columns: 1fr 340px; gap: 20px; align-items: start;">
695
+ <div>
696
+ <div style="margin-bottom: 16px;">
697
+ <h2 style="margin: 0; font-size: 18px;">${escapeHtml(test.name || test.id)} — Recording</h2>
698
+ <div style="color: var(--text-muted); font-size: 12px; margin-top: 4px;">
699
+ ${stepCount} turns · ${durationSec}s · click any decision on the right to scrub the video to that moment.
700
+ </div>
701
+ </div>
702
+ <div class="screenshot-wrap" style="background: #000; position: relative;">
703
+ <video id="rec-video" controls autoplay muted loop style="max-width: 100%; max-height: calc(100vh - 340px); display: block;" src="${escapeHtml(test.recording)}">
704
+ Your browser doesn't support inline video playback.
705
+ </video>
706
+ </div>
707
+ <!-- Timeline scrubber: thin track with a marker per turn -->
708
+ <div id="rec-timeline" style="margin-top: 14px; position: relative; height: 46px; padding: 0 4px;">
709
+ <div style="position: absolute; top: 18px; left: 4px; right: 4px; height: 6px; background: var(--border); border-radius: 3px;"></div>
710
+ <div id="rec-playhead" style="position: absolute; top: 15px; left: 4px; width: 2px; height: 12px; background: var(--accent, #7aa2ff); border-radius: 1px; box-shadow: 0 0 4px var(--accent, #7aa2ff); pointer-events: none;"></div>
711
+ <div id="rec-markers" style="position: relative; height: 46px;"></div>
712
+ </div>
713
+ <!-- Reasoning panel synced to the playhead -->
714
+ <div id="rec-reasoning" style="margin-top: 14px; padding: 14px 16px; background: var(--bg-panel, #141927); border: 1px solid var(--border, #2a3145); border-radius: 8px; min-height: 84px;">
715
+ <div style="font-size: 10px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent, #7aa2ff); margin-bottom: 8px;">
716
+ Agent reasoning · <span id="rec-turn-label">turn —</span>
717
+ </div>
718
+ <div id="rec-reasoning-body" style="font-size: 13px; line-height: 1.55; color: var(--text, #e6ebf5); white-space: pre-wrap;">
719
+ Press play to follow the agent's reasoning turn-by-turn.
720
+ </div>
721
+ <div id="rec-fork-panel" style="margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border, #2a3145); display: none;">
722
+ <div style="font-size: 10px; color: var(--text-muted); margin-bottom: 4px;">
723
+ Fork a new run from this decision with a different goal:
724
+ </div>
725
+ <div style="display: flex; gap: 6px; align-items: center;">
726
+ <input id="rec-fork-goal" type="text" placeholder="new goal…" style="flex: 1; padding: 6px 10px; background: var(--bg, #0b0f19); border: 1px solid var(--border, #2a3145); border-radius: 4px; color: var(--text, #e6ebf5); font-family: inherit; font-size: 12px;" />
727
+ <button id="rec-fork-button" type="button" style="padding: 6px 14px; background: var(--accent, #7aa2ff); color: #0b1220; border: none; border-radius: 4px; font-size: 12px; font-weight: 700; cursor: pointer;">
728
+ Copy fork cmd
729
+ </button>
730
+ </div>
731
+ <div id="rec-fork-hint" style="margin-top: 6px; font-size: 11px; color: var(--text-muted); font-family: 'SF Mono', Menlo, monospace; word-break: break-all;"></div>
732
+ </div>
674
733
  </div>
675
734
  </div>
676
- <div class="screenshot-wrap" style="background: #000;">
677
- <video controls autoplay muted loop style="max-width: 100%; max-height: calc(100vh - 220px); display: block;" src="${escapeHtml(test.recording)}">
678
- Your browser doesn't support inline video playback.
679
- </video>
680
- </div>
681
- <div style="margin-top: 12px; font-size: 12px; color: var(--text-muted);">
682
- Tip: scrub the video to see the agent's cursor move to each click target.
683
- Each click pulses a ring at the contact point when the run was captured with <code>--show-cursor</code>.
684
- </div>
735
+ <!-- Decision sidebar -->
736
+ <aside id="rec-sidebar" style="background: var(--bg-panel, #141927); border: 1px solid var(--border, #2a3145); border-radius: 8px; padding: 12px; max-height: calc(100vh - 220px); overflow-y: auto;">
737
+ <div style="font-size: 10px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent, #7aa2ff); margin-bottom: 10px;">
738
+ ${stepCount} decisions
739
+ </div>
740
+ <div id="rec-decision-list"></div>
741
+ </aside>
685
742
  </div>
686
743
  `;
744
+
745
+ const video = document.getElementById('rec-video');
746
+ const markers = document.getElementById('rec-markers');
747
+ const playhead = document.getElementById('rec-playhead');
748
+ const reasoningBody = document.getElementById('rec-reasoning-body');
749
+ const turnLabel = document.getElementById('rec-turn-label');
750
+ const decisionList = document.getElementById('rec-decision-list');
751
+ const forkPanel = document.getElementById('rec-fork-panel');
752
+ const forkGoalInput = document.getElementById('rec-fork-goal');
753
+ const forkButton = document.getElementById('rec-fork-button');
754
+ const forkHint = document.getElementById('rec-fork-hint');
755
+
756
+ // Render markers. Each marker is an absolutely-positioned dot with
757
+ // a thin stem so the viewer sees segment boundaries without clutter.
758
+ turnMarkers.forEach((m, i) => {
759
+ const pct = (m.start / Math.max(1, totalMs)) * 100;
760
+ const action = m.turn.action?.action ?? '?';
761
+ const el = document.createElement('button');
762
+ el.type = 'button';
763
+ el.className = 'rec-marker';
764
+ el.dataset.index = String(i);
765
+ el.title = `Turn ${i + 1} — ${action}`;
766
+ Object.assign(el.style, {
767
+ position: 'absolute',
768
+ left: `calc(${pct}% + 4px)`,
769
+ top: '8px',
770
+ width: '12px',
771
+ height: '28px',
772
+ marginLeft: '-6px',
773
+ background: 'transparent',
774
+ border: 'none',
775
+ padding: '0',
776
+ cursor: 'pointer',
777
+ display: 'flex',
778
+ flexDirection: 'column',
779
+ alignItems: 'center',
780
+ });
781
+ el.innerHTML = `
782
+ <div style="width: 10px; height: 10px; border-radius: 9999px; background: var(--bg-panel, #141927); border: 2px solid var(--accent, #7aa2ff); box-shadow: 0 0 4px rgba(122,162,255,0.5);"></div>
783
+ <div style="width: 1px; height: 10px; background: var(--border, #2a3145); margin-top: 2px;"></div>
784
+ <div style="font-size: 9px; color: var(--text-muted); margin-top: 1px;">${i + 1}</div>
785
+ `;
786
+ el.addEventListener('click', () => {
787
+ video.currentTime = m.start / 1000;
788
+ selectTurn(i);
789
+ });
790
+ markers.appendChild(el);
791
+ });
792
+
793
+ // Verdict classifier mirrors the overlay narration logic — keep in
794
+ // sync with src/runner/overlay-narration.ts so the sidebar shows
795
+ // the same verdict colors as the video's baked-in badges.
796
+ function classifyTurn(t) {
797
+ const r = (t.reasoning || '') + ' ' + (t.action?.result || '');
798
+ if (/POSITIVE\s+MATCH/i.test(r)) return 'positive';
799
+ if (/\bCLEARED\b/i.test(r)) return 'cleared';
800
+ if (/NEEDS\s+REVIEW/i.test(r)) return 'review';
801
+ if (t.result?.success === false || t.verificationFailure) return 'error';
802
+ return 'neutral';
803
+ }
804
+
805
+ const VERDICT_COLOR = {
806
+ positive: '#fb7185', cleared: '#4ade80', review: '#fbbf24',
807
+ error: '#ef4444', neutral: 'var(--accent, #7aa2ff)'
808
+ };
809
+
810
+ // Build the decision sidebar. Each row carries turn index, action
811
+ // verb, target preview, a verdict chip, and a one-line reasoning
812
+ // summary. Click = seek video + update active state.
813
+ turns.forEach((t, i) => {
814
+ const act = t.action?.action || '?';
815
+ const targetRaw = t.action?.selector ?? t.action?.text ?? t.action?.key ?? t.action?.url ?? '';
816
+ const target = String(targetRaw).slice(0, 48);
817
+ const verdict = classifyTurn(t);
818
+ const color = VERDICT_COLOR[verdict] || VERDICT_COLOR.neutral;
819
+ const summary = (t.reasoning || '').replace(/\s+/g, ' ').trim().slice(0, 80);
820
+ const row = document.createElement('button');
821
+ row.type = 'button';
822
+ row.className = 'rec-decision';
823
+ row.dataset.index = String(i);
824
+ Object.assign(row.style, {
825
+ display: 'block', width: '100%', textAlign: 'left',
826
+ padding: '10px 12px', margin: '0 0 6px 0',
827
+ background: 'transparent',
828
+ border: '1px solid var(--border, #2a3145)', borderLeft: `3px solid ${color}`,
829
+ borderRadius: '6px', color: 'var(--text, #e6ebf5)',
830
+ cursor: 'pointer', fontFamily: 'inherit',
831
+ transition: 'background 120ms ease, border-color 120ms ease',
832
+ });
833
+ row.innerHTML = `
834
+ <div style="display: flex; justify-content: space-between; align-items: baseline; gap: 8px; font-size: 12px;">
835
+ <div>
836
+ <span style="color: var(--text-muted); font-family: 'SF Mono', Menlo, monospace;">${i + 1}.</span>
837
+ <span style="color: ${color}; font-weight: 600; margin-left: 4px;">${escapeHtml(act)}</span>
838
+ ${target ? `<span style="color: var(--text-muted); margin-left: 6px;">${escapeHtml(target)}</span>` : ''}
839
+ </div>
840
+ </div>
841
+ ${summary ? `<div style="margin-top: 4px; font-size: 11px; color: var(--text-muted); line-height: 1.4;">${escapeHtml(summary)}</div>` : ''}
842
+ `;
843
+ row.addEventListener('mouseenter', () => { row.style.background = 'rgba(122,162,255,0.04)'; });
844
+ row.addEventListener('mouseleave', () => { if (!row.classList.contains('active')) row.style.background = 'transparent'; });
845
+ row.addEventListener('click', () => {
846
+ const m = turnMarkers[i];
847
+ if (m) video.currentTime = m.start / 1000;
848
+ selectTurn(i);
849
+ });
850
+ decisionList.appendChild(row);
851
+ });
852
+
853
+ function selectTurn(i) {
854
+ const t = turns[i];
855
+ if (!t) return;
856
+ const action = t.action?.action ?? '?';
857
+ const target = t.action?.selector ?? t.action?.text ?? t.action?.key ?? t.action?.url ?? '';
858
+ turnLabel.textContent = `turn ${i + 1} / ${turns.length} · ${action}${target ? ' · ' + String(target).slice(0, 80) : ''}`;
859
+ const r = (t.reasoning || '').trim();
860
+ reasoningBody.textContent = r || '(no reasoning captured)';
861
+ // Highlight the active marker
862
+ markers.querySelectorAll('.rec-marker').forEach((n, j) => {
863
+ const dot = n.firstElementChild;
864
+ if (!dot) return;
865
+ dot.style.background = j === i ? 'var(--accent, #7aa2ff)' : 'var(--bg-panel, #141927)';
866
+ });
867
+ // Highlight the active sidebar row
868
+ decisionList.querySelectorAll('.rec-decision').forEach((n, j) => {
869
+ if (j === i) {
870
+ n.classList.add('active');
871
+ n.style.background = 'rgba(122,162,255,0.10)';
872
+ n.style.borderColor = 'var(--accent, #7aa2ff)';
873
+ // Keep the active row visible when auto-advanced by video time
874
+ n.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
875
+ } else {
876
+ n.classList.remove('active');
877
+ n.style.background = 'transparent';
878
+ n.style.borderColor = 'var(--border, #2a3145)';
879
+ }
880
+ });
881
+ // Show the fork panel. Pre-fill a templated goal the user can edit.
882
+ const runId = test.id || test.runId || '';
883
+ const defaultGoal = (t.reasoning || '').split(/\.\s/)[0].slice(0, 80).replace(/^\s*["']?/, '').replace(/["']?\s*$/, '');
884
+ forkPanel.style.display = 'block';
885
+ forkGoalInput.value = defaultGoal;
886
+ const updateHint = () => {
887
+ const g = forkGoalInput.value.trim() || '<new goal>';
888
+ forkHint.textContent = runId
889
+ ? `bad run --fork-run ${runId} --goal "${g.replace(/"/g, '\\"')}"`
890
+ : '(no run id available — fork requires memory-enabled runs)';
891
+ };
892
+ updateHint();
893
+ forkGoalInput.oninput = updateHint;
894
+ forkButton.onclick = async () => {
895
+ const cmd = forkHint.textContent;
896
+ try {
897
+ await navigator.clipboard.writeText(cmd);
898
+ const prev = forkButton.textContent;
899
+ forkButton.textContent = 'Copied ✓';
900
+ setTimeout(() => { forkButton.textContent = prev; }, 1800);
901
+ } catch {
902
+ // Clipboard blocked — select the hint text so the user can Cmd-C.
903
+ const range = document.createRange();
904
+ range.selectNodeContents(forkHint);
905
+ const sel = window.getSelection();
906
+ sel.removeAllRanges();
907
+ sel.addRange(range);
908
+ }
909
+ };
910
+ }
911
+
912
+ function findTurnAt(timeMs) {
913
+ // Binary-search not needed for typical N≤100
914
+ let idx = -1;
915
+ for (let i = 0; i < turnMarkers.length; i++) {
916
+ if (timeMs >= turnMarkers[i].start) idx = i;
917
+ else break;
918
+ }
919
+ return idx;
920
+ }
921
+
922
+ video.addEventListener('timeupdate', () => {
923
+ const timeMs = video.currentTime * 1000;
924
+ const pct = Math.min(100, (timeMs / Math.max(1, totalMs)) * 100);
925
+ playhead.style.left = `calc(${pct}% + 4px)`;
926
+ const idx = findTurnAt(timeMs);
927
+ if (idx >= 0 && idx !== selectTurn._lastIdx) {
928
+ selectTurn._lastIdx = idx;
929
+ selectTurn(idx);
930
+ }
931
+ });
932
+ // Initial selection
933
+ if (turns.length > 0) selectTurn(0);
934
+
687
935
  const aside = el('aside-panel');
688
936
  aside.innerHTML = `
689
937
  <div class="panel">
@@ -702,7 +950,7 @@
702
950
  <div class="panel">
703
951
  <h3>About this view</h3>
704
952
  <div class="panel-body" style="font-size: 12px;">
705
- The recording is a Playwright screencast captured during the run. The cursor sprite, click pulses, and element highlights are baked into the video frames when <code>--show-cursor</code> was passed to <code>bad run</code>.
953
+ Click a timeline marker to jump to that turn. The reasoning panel below the video updates as the video plays each timestamp maps to the agent's own words at that moment. Cursor, click pulses, and verdict badges are baked into the video frames when the run used <code>--show-cursor</code>.
706
954
  </div>
707
955
  </div>
708
956
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/browser-agent-driver",
3
- "version": "0.24.2",
3
+ "version": "0.26.0",
4
4
  "description": "LLM-driven browser agent for UI automation, testing, and evaluation",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -133,4 +133,4 @@
133
133
  "typescript": "^5.3.0",
134
134
  "vitest": "^4.0.18"
135
135
  }
136
- }
136
+ }