@harness-fe/runtime 3.4.0 → 3.4.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.
package/dist/overlay.js CHANGED
@@ -249,6 +249,12 @@ export function installOverlay(client) {
249
249
  let pendingAttachment = null;
250
250
  /** Set while the picker is collecting an element for a `requiresElement` plugin. */
251
251
  let pluginAwaitingElement = null;
252
+ /**
253
+ * Purpose of the current picker session:
254
+ * - 'copy': copy element info to clipboard for use with an agent (default)
255
+ * - 'report': legacy report-a-problem flow (still used internally by plugins)
256
+ */
257
+ let pickerPurpose = 'copy';
252
258
  const setState = (next) => {
253
259
  state = next;
254
260
  infoCard.style.display = next === 'info' ? 'flex' : 'none';
@@ -322,7 +328,7 @@ export function installOverlay(client) {
322
328
  lockedEl = hoveredEl;
323
329
  setHighlight(lockedEl);
324
330
  // A plugin requested the element — hand it straight to its onClick and
325
- // skip the report/question flow entirely.
331
+ // skip all other flows.
326
332
  if (pluginAwaitingElement) {
327
333
  const plugin = pluginAwaitingElement;
328
334
  pluginAwaitingElement = null;
@@ -332,9 +338,18 @@ export function installOverlay(client) {
332
338
  void invokePlugin(plugin, el);
333
339
  return;
334
340
  }
335
- // Go straight to the question step. Screenshots are now opt-in via
336
- // the "Add screenshot" button inside the question panel — users
337
- // shouldn't have to draw on every report.
341
+ // Copy mode: build element info and copy to clipboard for agent use.
342
+ if (pickerPurpose === 'copy') {
343
+ const el = lockedEl;
344
+ lockedEl = null;
345
+ const text = buildElementCopyText(el);
346
+ void copyText(text).then(() => {
347
+ showToast('✓ Element info copied');
348
+ });
349
+ setState('idle');
350
+ return;
351
+ }
352
+ // Report mode (legacy, no longer exposed in UI but kept for plugin compatibility).
338
353
  pendingAttachment = null;
339
354
  const info = questionPanel.querySelector('[data-role=info]');
340
355
  info.textContent = describeElement(lockedEl);
@@ -358,6 +373,7 @@ export function installOverlay(client) {
358
373
  lockedEl = null;
359
374
  pendingAttachment = null;
360
375
  pluginAwaitingElement = null;
376
+ pickerPurpose = 'copy';
361
377
  setState('info');
362
378
  }
363
379
  else if (state === 'info') {
@@ -428,6 +444,31 @@ export function installOverlay(client) {
428
444
  }, 1200);
429
445
  }
430
446
  };
447
+ /**
448
+ * Build a compact element-info block for pasting into an agent prompt.
449
+ * Omits HTML (too verbose); includes source location, component name, css
450
+ * path, and session context — enough for the agent to locate and fix the
451
+ * element without any further investigation.
452
+ */
453
+ const buildElementCopyText = (el) => {
454
+ const tag = el.tagName.toLowerCase();
455
+ const comp = el.getAttribute('data-morphix-comp');
456
+ const loc = el.getAttribute('data-morphix-loc');
457
+ const css = buildCssPath(el);
458
+ const lines = [];
459
+ lines.push(`### Element context`);
460
+ lines.push('');
461
+ if (comp)
462
+ lines.push(`- component: \`${comp}\``);
463
+ if (loc)
464
+ lines.push(`- source: \`${loc}\``);
465
+ lines.push(`- tag: \`${tag}\``);
466
+ lines.push(`- css: \`${css}\``);
467
+ lines.push(`- project: \`${client.projectId}\`${client.displayName ? ` (${client.displayName})` : ''}`);
468
+ lines.push(`- session: \`${client.sessionId}\``);
469
+ lines.push(`- url: ${location.href}`);
470
+ return lines.join('\n') + '\n';
471
+ };
431
472
  const buildSnapshot = () => {
432
473
  const lines = [];
433
474
  lines.push(`### Harness-FE snapshot`);
@@ -525,6 +566,10 @@ export function installOverlay(client) {
525
566
  btn.addEventListener('click', () => {
526
567
  if (plugin.requiresElement) {
527
568
  pluginAwaitingElement = plugin;
569
+ pickerPurpose = 'report'; // plugins use the legacy element-selection flow
570
+ const label = pickerBar.querySelector('[data-role=picker-label]');
571
+ if (label)
572
+ label.textContent = '🎯 Click an element';
528
573
  setState('picker');
529
574
  }
530
575
  else {
@@ -753,16 +798,17 @@ export function installOverlay(client) {
753
798
  setState(state === 'idle' ? 'info' : 'idle');
754
799
  });
755
800
  infoCard.querySelector('[data-role=close]').addEventListener('click', () => setState('idle'));
756
- infoCard.querySelector('[data-role=report]').addEventListener('click', () => {
801
+ infoCard.querySelector('[data-role=pick-element]').addEventListener('click', () => {
802
+ pickerPurpose = 'copy';
803
+ const label = pickerBar.querySelector('[data-role=picker-label]');
804
+ if (label)
805
+ label.textContent = '🔍 Click element to copy info';
757
806
  setState('picker');
758
807
  });
759
808
  infoCard.querySelector('[data-role=copy-snapshot]').addEventListener('click', (ev) => {
760
809
  const btn = ev.currentTarget;
761
810
  void copyText(buildSnapshot(), btn);
762
811
  });
763
- infoCard.querySelector('[data-role=view-reports]').addEventListener('click', () => {
764
- setState('reports');
765
- });
766
812
  // "Open dashboard" — derive the daemon's dashboard URL from mcpUrl and
767
813
  // pop it in a new tab, deep-linked to this session. Show the button
768
814
  // only when we actually know the daemon address (mcpUrl was supplied by
@@ -815,6 +861,7 @@ export function installOverlay(client) {
815
861
  pickerBar.querySelector('[data-role=cancel]').addEventListener('click', () => {
816
862
  lockedEl = null;
817
863
  pluginAwaitingElement = null;
864
+ pickerPurpose = 'copy';
818
865
  setState('info');
819
866
  });
820
867
  questionPanel.querySelector('[data-role=cancel]').addEventListener('click', () => {
@@ -1939,16 +1986,15 @@ function buildInfoCard() {
1939
1986
  <div class="row"><span class="key">url</span><span class="pill url" data-role="url"></span></div>
1940
1987
  </div>
1941
1988
  <div class="actions">
1942
- <button class="primary" data-role="report" type="button">
1943
- <span class="icon">🎯</span>
1944
- <span class="label">Report a problem</span>
1945
- <span class="hint">Pick an element →</span>
1989
+ <button class="primary" data-role="pick-element" type="button">
1990
+ <span class="icon">🔍</span>
1991
+ <span class="label">Copy element info</span>
1992
+ <span class="hint">pick element →</span>
1946
1993
  </button>
1947
1994
  <button class="secondary" data-role="open-dashboard" type="button" style="display:none">
1948
1995
  <span class="icon">↗</span>
1949
1996
  <span>Open dashboard</span>
1950
1997
  </button>
1951
- <button class="secondary" data-role="view-reports" type="button">📁 My reports</button>
1952
1998
  <button class="secondary" data-role="copy-snapshot" type="button">📋 Copy snapshot</button>
1953
1999
  </div>
1954
2000
  <div class="plugin-actions" data-role="plugin-actions" style="display:none"></div>
@@ -1979,7 +2025,7 @@ function buildPickerBar() {
1979
2025
  const bar = document.createElement('div');
1980
2026
  bar.className = 'picker-bar';
1981
2027
  bar.innerHTML = `
1982
- <span class="label">🎯 Click an element to flag it</span>
2028
+ <span class="label" data-role="picker-label">🔍 Click element to copy info</span>
1983
2029
  <span class="hint">esc to cancel</span>
1984
2030
  <button data-role="cancel" type="button">Cancel</button>
1985
2031
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-fe/runtime",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
4
4
  "description": "Browser-side SDK injected into the dev page. Connects to the MCP server via WebSocket and executes commands.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,8 +30,8 @@
30
30
  "dependencies": {
31
31
  "@zumer/snapdom": "^2.12.0",
32
32
  "rrweb": "2.0.0-alpha.4",
33
- "@harness-fe/sandbox": "^3.2.0",
34
- "@harness-fe/protocol": "3.2.0"
33
+ "@harness-fe/protocol": "3.2.0",
34
+ "@harness-fe/sandbox": "^3.2.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "happy-dom": "^20.9.0",
@@ -73,20 +73,20 @@ describe('installOverlay', () => {
73
73
  expect(root.querySelector('[data-role=build]')!.textContent).toBe('—');
74
74
  });
75
75
 
76
- it('"Report a problem" enters picker mode (FAB turns active, info card hidden)', () => {
76
+ it('"Copy element info" enters picker mode (FAB turns active, info card hidden)', () => {
77
77
  setupDom();
78
78
  const client = makeFakeClient();
79
79
  installOverlay(client);
80
80
  const root = document.getElementById('__harness_fe_overlay__')!.shadowRoot!;
81
81
  (root.querySelector('.fab') as HTMLButtonElement).click();
82
- (root.querySelector('[data-role=report]') as HTMLButtonElement).click();
82
+ (root.querySelector('[data-role=pick-element]') as HTMLButtonElement).click();
83
83
  const fab = root.querySelector('.fab') as HTMLButtonElement;
84
84
  expect(fab.dataset.state).toBe('active');
85
85
  expect((root.querySelector('.info-card') as HTMLElement).style.display).toBe('none');
86
86
  expect((root.querySelector('.picker-bar') as HTMLElement).style.display).toBe('flex');
87
87
  });
88
88
 
89
- it('submits a task.submit event payload with selector + element on Submit', () => {
89
+ it('"Copy element info" returns to idle after element click (no task.submit fired)', () => {
90
90
  const { doc } = setupDom();
91
91
  const target = doc.createElement('button');
92
92
  target.setAttribute('data-morphix-loc', 'app/cart/CartBadge.tsx:18:5');
@@ -94,47 +94,31 @@ describe('installOverlay', () => {
94
94
  target.textContent = 'Cart (3)';
95
95
  doc.body.appendChild(target);
96
96
 
97
+ // Stub clipboard so copyText does not throw in happy-dom.
98
+ const written: string[] = [];
99
+ Object.defineProperty(globalThis.navigator, 'clipboard', {
100
+ value: { writeText: (t: string) => { written.push(t); return Promise.resolve(); } },
101
+ configurable: true,
102
+ });
103
+
97
104
  const client = makeFakeClient();
98
105
  installOverlay(client);
99
106
  const root = document.getElementById('__harness_fe_overlay__')!.shadowRoot!;
100
107
 
101
- // Open reportfake-pick submit.
108
+ // Open info cardenter pick-element picker mode.
102
109
  (root.querySelector('.fab') as HTMLButtonElement).click();
103
- (root.querySelector('[data-role=report]') as HTMLButtonElement).click();
104
-
105
- // Simulate the picker click flow by directly invoking the state we'd
106
- // be in after the user picks. We can't easily simulate
107
- // elementFromPoint in happy-dom, so reach into the question panel
108
- // and submit a payload — overlay.ts's submit handler reads lockedEl
109
- // from a closure, so we go through a synthesized click instead.
110
- // Trick: dispatch a capture-phase click on the body with the target.
111
- // overlay's onClickCapture relies on `hoveredEl` set by mousemove.
112
- // To avoid coupling to mousemove geometry, we test the submit handler
113
- // is wired by inspecting the question textarea wiring instead.
114
-
115
- // Force the panel into "question" state by clicking the target via
116
- // the document; we first set hoveredEl by dispatching mousemove with
117
- // matching screen coords.
118
- target.dispatchEvent(new MouseEvent('mousemove', {
119
- bubbles: true, clientX: 0, clientY: 0,
120
- }));
121
- // Direct click on the picker target triggers the capture handler.
110
+ (root.querySelector('[data-role=pick-element]') as HTMLButtonElement).click();
111
+ expect((root.querySelector('.picker-bar') as HTMLElement).style.display).toBe('flex');
112
+
113
+ // Simulate hover + click on the target element.
114
+ target.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: 0, clientY: 0 }));
122
115
  target.click();
123
116
 
124
- // If the picker accepted, the question panel is now visible.
125
- const question = root.querySelector('.question') as HTMLElement;
126
- if (question.style.display === 'flex') {
127
- (root.querySelector('.question textarea') as HTMLTextAreaElement).value = 'broken';
128
- (root.querySelector('.question [data-role=submit]') as HTMLButtonElement).click();
129
- expect(client.sent).toHaveLength(1);
130
- expect(client.sent[0].name).toBe('task.submit');
131
- const payload = client.sent[0].payload as { selector: { loc?: string }; question: string };
132
- expect(payload.question).toBe('broken');
133
- expect(payload.selector.loc).toBe('app/cart/CartBadge.tsx:18:5');
134
- }
135
- // If happy-dom's elementFromPoint didn't cooperate, the test still
136
- // exercises mount/open/copy paths above — submit path is asserted
137
- // separately by buildCssPath unit + bridge.test integration.
117
+ // After the pick: overlay should be idle (picker bar hidden, no question panel).
118
+ // The question panel must NOT open — copy mode skips the report flow.
119
+ expect((root.querySelector('.question') as HTMLElement).style.display).toBe('none');
120
+ // No task.submit event should be sent — copy mode never fires a report.
121
+ expect(client.sent).toHaveLength(0);
138
122
  });
139
123
 
140
124
  it('Esc closes the info card when open', () => {
package/src/overlay.ts CHANGED
@@ -309,6 +309,12 @@ export function installOverlay(client: OverlayClient): void {
309
309
  let pendingAttachment: TaskAttachment | null = null;
310
310
  /** Set while the picker is collecting an element for a `requiresElement` plugin. */
311
311
  let pluginAwaitingElement: OverlayPlugin | null = null;
312
+ /**
313
+ * Purpose of the current picker session:
314
+ * - 'copy': copy element info to clipboard for use with an agent (default)
315
+ * - 'report': legacy report-a-problem flow (still used internally by plugins)
316
+ */
317
+ let pickerPurpose: 'copy' | 'report' = 'copy';
312
318
 
313
319
  const setState = (next: State) => {
314
320
  state = next;
@@ -381,7 +387,7 @@ export function installOverlay(client: OverlayClient): void {
381
387
  lockedEl = hoveredEl;
382
388
  setHighlight(lockedEl);
383
389
  // A plugin requested the element — hand it straight to its onClick and
384
- // skip the report/question flow entirely.
390
+ // skip all other flows.
385
391
  if (pluginAwaitingElement) {
386
392
  const plugin = pluginAwaitingElement;
387
393
  pluginAwaitingElement = null;
@@ -391,9 +397,18 @@ export function installOverlay(client: OverlayClient): void {
391
397
  void invokePlugin(plugin, el);
392
398
  return;
393
399
  }
394
- // Go straight to the question step. Screenshots are now opt-in via
395
- // the "Add screenshot" button inside the question panel — users
396
- // shouldn't have to draw on every report.
400
+ // Copy mode: build element info and copy to clipboard for agent use.
401
+ if (pickerPurpose === 'copy') {
402
+ const el = lockedEl;
403
+ lockedEl = null;
404
+ const text = buildElementCopyText(el);
405
+ void copyText(text).then(() => {
406
+ showToast('✓ Element info copied');
407
+ });
408
+ setState('idle');
409
+ return;
410
+ }
411
+ // Report mode (legacy, no longer exposed in UI but kept for plugin compatibility).
397
412
  pendingAttachment = null;
398
413
  const info = questionPanel.querySelector<HTMLElement>('[data-role=info]')!;
399
414
  info.textContent = describeElement(lockedEl);
@@ -417,6 +432,7 @@ export function installOverlay(client: OverlayClient): void {
417
432
  lockedEl = null;
418
433
  pendingAttachment = null;
419
434
  pluginAwaitingElement = null;
435
+ pickerPurpose = 'copy';
420
436
  setState('info');
421
437
  } else if (state === 'info') {
422
438
  setState('idle');
@@ -486,6 +502,30 @@ export function installOverlay(client: OverlayClient): void {
486
502
  }
487
503
  };
488
504
 
505
+ /**
506
+ * Build a compact element-info block for pasting into an agent prompt.
507
+ * Omits HTML (too verbose); includes source location, component name, css
508
+ * path, and session context — enough for the agent to locate and fix the
509
+ * element without any further investigation.
510
+ */
511
+ const buildElementCopyText = (el: Element): string => {
512
+ const tag = el.tagName.toLowerCase();
513
+ const comp = el.getAttribute('data-morphix-comp');
514
+ const loc = el.getAttribute('data-morphix-loc');
515
+ const css = buildCssPath(el);
516
+ const lines: string[] = [];
517
+ lines.push(`### Element context`);
518
+ lines.push('');
519
+ if (comp) lines.push(`- component: \`${comp}\``);
520
+ if (loc) lines.push(`- source: \`${loc}\``);
521
+ lines.push(`- tag: \`${tag}\``);
522
+ lines.push(`- css: \`${css}\``);
523
+ lines.push(`- project: \`${client.projectId}\`${client.displayName ? ` (${client.displayName})` : ''}`);
524
+ lines.push(`- session: \`${client.sessionId}\``);
525
+ lines.push(`- url: ${location.href}`);
526
+ return lines.join('\n') + '\n';
527
+ };
528
+
489
529
  const buildSnapshot = (): string => {
490
530
  const lines: string[] = [];
491
531
  lines.push(`### Harness-FE snapshot`);
@@ -584,6 +624,9 @@ export function installOverlay(client: OverlayClient): void {
584
624
  btn.addEventListener('click', () => {
585
625
  if (plugin.requiresElement) {
586
626
  pluginAwaitingElement = plugin;
627
+ pickerPurpose = 'report'; // plugins use the legacy element-selection flow
628
+ const label = pickerBar.querySelector<HTMLElement>('[data-role=picker-label]');
629
+ if (label) label.textContent = '🎯 Click an element';
587
630
  setState('picker');
588
631
  } else {
589
632
  setState('idle');
@@ -803,7 +846,10 @@ export function installOverlay(client: OverlayClient): void {
803
846
 
804
847
  infoCard.querySelector('[data-role=close]')!.addEventListener('click', () => setState('idle'));
805
848
 
806
- infoCard.querySelector('[data-role=report]')!.addEventListener('click', () => {
849
+ infoCard.querySelector('[data-role=pick-element]')!.addEventListener('click', () => {
850
+ pickerPurpose = 'copy';
851
+ const label = pickerBar.querySelector<HTMLElement>('[data-role=picker-label]');
852
+ if (label) label.textContent = '🔍 Click element to copy info';
807
853
  setState('picker');
808
854
  });
809
855
 
@@ -812,10 +858,6 @@ export function installOverlay(client: OverlayClient): void {
812
858
  void copyText(buildSnapshot(), btn);
813
859
  });
814
860
 
815
- infoCard.querySelector('[data-role=view-reports]')!.addEventListener('click', () => {
816
- setState('reports');
817
- });
818
-
819
861
  // "Open dashboard" — derive the daemon's dashboard URL from mcpUrl and
820
862
  // pop it in a new tab, deep-linked to this session. Show the button
821
863
  // only when we actually know the daemon address (mcpUrl was supplied by
@@ -869,6 +911,7 @@ export function installOverlay(client: OverlayClient): void {
869
911
  pickerBar.querySelector('[data-role=cancel]')!.addEventListener('click', () => {
870
912
  lockedEl = null;
871
913
  pluginAwaitingElement = null;
914
+ pickerPurpose = 'copy';
872
915
  setState('info');
873
916
  });
874
917
 
@@ -2071,16 +2114,15 @@ function buildInfoCard(): HTMLDivElement {
2071
2114
  <div class="row"><span class="key">url</span><span class="pill url" data-role="url"></span></div>
2072
2115
  </div>
2073
2116
  <div class="actions">
2074
- <button class="primary" data-role="report" type="button">
2075
- <span class="icon">🎯</span>
2076
- <span class="label">Report a problem</span>
2077
- <span class="hint">Pick an element →</span>
2117
+ <button class="primary" data-role="pick-element" type="button">
2118
+ <span class="icon">🔍</span>
2119
+ <span class="label">Copy element info</span>
2120
+ <span class="hint">pick element →</span>
2078
2121
  </button>
2079
2122
  <button class="secondary" data-role="open-dashboard" type="button" style="display:none">
2080
2123
  <span class="icon">↗</span>
2081
2124
  <span>Open dashboard</span>
2082
2125
  </button>
2083
- <button class="secondary" data-role="view-reports" type="button">📁 My reports</button>
2084
2126
  <button class="secondary" data-role="copy-snapshot" type="button">📋 Copy snapshot</button>
2085
2127
  </div>
2086
2128
  <div class="plugin-actions" data-role="plugin-actions" style="display:none"></div>
@@ -2113,7 +2155,7 @@ function buildPickerBar(): HTMLDivElement {
2113
2155
  const bar = document.createElement('div');
2114
2156
  bar.className = 'picker-bar';
2115
2157
  bar.innerHTML = `
2116
- <span class="label">🎯 Click an element to flag it</span>
2158
+ <span class="label" data-role="picker-label">🔍 Click element to copy info</span>
2117
2159
  <span class="hint">esc to cancel</span>
2118
2160
  <button data-role="cancel" type="button">Cancel</button>
2119
2161
  `;