@atlassian/chippy 0.0.1-beta.1 → 0.0.1-beta.2

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 (54) hide show
  1. package/README.md +142 -54
  2. package/bin/chippy.mjs +53 -95
  3. package/bin/pack.mjs +20 -0
  4. package/bin/skill-dir.mjs +15 -0
  5. package/lib/avalanche.js +54 -23
  6. package/lib/avalanche.js.map +2 -2
  7. package/lib/bootstrap.js +20 -1
  8. package/lib/bootstrap.js.map +3 -3
  9. package/lib/extension/avalanche.es.js +173 -34
  10. package/lib/extension/avalanche.es.js.map +1 -1
  11. package/lib/extension/chippy-page-bridge.js +164 -44
  12. package/lib/mcp-skill/README.md +2 -4
  13. package/lib/mcp-skill/SKILL.md +21 -16
  14. package/lib/mcp-skill/extension/armed-reload-startup.js +83 -0
  15. package/lib/mcp-skill/extension/avalanche.es.js +173 -34
  16. package/lib/mcp-skill/extension/avalanche.es.js.map +1 -1
  17. package/lib/mcp-skill/extension/avalanche.iife.js +173 -34
  18. package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -1
  19. package/lib/mcp-skill/extension/background.js +2 -0
  20. package/lib/mcp-skill/extension/chippy-page-bridge.js +164 -44
  21. package/lib/mcp-skill/extension/content-script.js +23 -0
  22. package/lib/mcp-skill/extension/devtools-hook-shim.js +18 -1
  23. package/lib/mcp-skill/extension/early-bootstrap.js +3 -1
  24. package/lib/mcp-skill/extension/manifest.json +1 -1
  25. package/lib/mcp-skill/extension/panel-settings.js +2 -20
  26. package/lib/mcp-skill/extension/startup-handoff.js +30 -0
  27. package/lib/mcp-skill/references/README.md +2 -2
  28. package/lib/mcp-skill/references/avalanche-detector.md +3 -3
  29. package/lib/mcp-skill/references/browser-runtime-api.md +4 -4
  30. package/lib/mcp-skill/references/command-output-formats.md +12 -2
  31. package/lib/mcp-skill/references/command-surface.md +6 -6
  32. package/lib/mcp-skill/references/mode-a-injection.md +43 -9
  33. package/lib/mcp-skill/references/playwright-cli-setup.md +5 -5
  34. package/lib/mcp-skill/references/playwright-probes.md +1 -1
  35. package/lib/mcp-skill/references/recipes.txt +1 -1
  36. package/lib/mcp-skill/references/stability-render.md +4 -4
  37. package/lib/mcp-skill/references/test-mode-playbook.md +2 -1
  38. package/lib/mcp-skill/references/troubleshooting.md +6 -1
  39. package/lib/mcp-skill/scripts/chippy-helper-server.js +116 -58
  40. package/lib/mcp-skill/scripts/chippy.sh +41 -7
  41. package/lib/mcp-skill/scripts/helper-access.js +20 -6
  42. package/lib/mcp-skill/scripts/helper-proxy.js +87 -0
  43. package/lib/mcp-skill/scripts/page-socket-trust.js +81 -0
  44. package/lib/plugin.js.map +2 -2
  45. package/lib/storybook/connection-fallback.jsx +1 -1
  46. package/lib/storybook/manager-owner.js +42 -0
  47. package/lib/storybook/manager.jsx +22 -0
  48. package/lib/storybook/preview.js +53 -18
  49. package/lib/storybook/setup.js +15 -4
  50. package/lib/types/chippy-bridge/fiber-source.d.ts +14 -3
  51. package/lib/types/chippy-bridge/react-renderer.d.ts +30 -5
  52. package/lib/types/chippy-bridge/recorded-source.d.ts +44 -0
  53. package/lib/types/debug-render/core.d.ts +6 -0
  54. package/package.json +3 -1
@@ -8,6 +8,16 @@
8
8
  /** @type {ChippyPageRuntime} */
9
9
  const chippy = global.__CHIPPY = global.__CHIPPY || {};
10
10
  const BRIDGE_VERSION = '0.2.5';
11
+ // Returned by every command that needs __AVALANCHE__.bridge when the Avalanche
12
+ // runtime is absent. The helper reports it as an unavailable runtime rather
13
+ // than an empty result, so callers never read `count: 0` as "nothing found".
14
+ const BRIDGE_MODULE_MISSING = Object.freeze({
15
+ ok: false,
16
+ code: 'bridge-module-missing',
17
+ error: 'bridge module not loaded',
18
+ });
19
+ // Read and removed by startup-handoff.js on the next document.
20
+ const STARTUP_HANDOFF_KEY = '__chippyStartupHandoff';
11
21
  if (chippy.pageBridge) {
12
22
  // Always tear down and reinstall — version-equality skip caused stale code to win.
13
23
  try { chippy.pageBridge.destroy?.(); } catch (e) {}
@@ -37,18 +47,53 @@
37
47
  return config?.integration || global.__CHIPPY_OWNER__?.integration || null;
38
48
  }
39
49
 
50
+ // Same-origin frames share sessionStorage, so each frame keys its id by its
51
+ // position in the frame tree; the Storybook manager and its preview iframe
52
+ // must not share one client id. The top frame keeps the original key.
53
+ function frameKeySuffix() {
54
+ const path = [];
55
+ try {
56
+ /** @type {Window} */
57
+ let current = global;
58
+ while (current !== current.parent && path.length < 16) {
59
+ const siblings = current.parent.frames;
60
+ let index = -1;
61
+ for (let i = 0; i < siblings.length; i += 1) {
62
+ if (siblings[i] === current) { index = i; break; }
63
+ }
64
+ path.unshift(index);
65
+ current = current.parent;
66
+ }
67
+ } catch {
68
+ return path.length ? ':frame' : '';
69
+ }
70
+ return path.length ? ':' + path.join('.') : '';
71
+ }
72
+
40
73
  function getStoredClientId() {
74
+ const key = '__chippyClientId' + frameKeySuffix();
41
75
  try {
42
- const existing = global.sessionStorage?.getItem('__chippyClientId');
76
+ const existing = global.sessionStorage?.getItem(key);
43
77
  if (existing) return existing;
44
78
  const next = global.crypto?.randomUUID?.() || `chippy-${Date.now()}`;
45
- global.sessionStorage?.setItem('__chippyClientId', next);
79
+ global.sessionStorage?.setItem(key, next);
46
80
  return next;
47
81
  } catch {
48
82
  return `chippy-${Date.now()}`;
49
83
  }
50
84
  }
51
85
 
86
+ // The extension is the fallback integration. A live owner of another kind,
87
+ // such as the Storybook addon in its manager window, outranks it.
88
+ function isLiveForeignOwner(owner) {
89
+ if (!owner || !owner.integration || owner.integration.kind === 'extension') return false;
90
+ return !owner.staleAfterMs || Date.now() - (owner.heartbeatAt || 0) <= owner.staleAfterMs;
91
+ }
92
+
93
+ function isExtensionBridge() {
94
+ return config?.integration?.kind === 'extension';
95
+ }
96
+
52
97
  function getStoryId() {
53
98
  const params = new URLSearchParams(global.location.search || '');
54
99
  return params.get('id') || null;
@@ -363,6 +408,7 @@
363
408
  let onLayoutLabUpdate = null;
364
409
  let onWindowMessage = null;
365
410
  let onExtCommand = null;
411
+ let onOwnerClaimed = null;
366
412
  let publishTimer = null;
367
413
  let snapshotSequence = 0;
368
414
  let lastLocationHref = global.location.href;
@@ -542,18 +588,55 @@
542
588
  const b = getBridge();
543
589
  return b
544
590
  ? b.readAvalancheStatus(getAvalancheApi(), getAvalancheModule())
545
- : { ok: false, available: false, error: 'bridge module not loaded' };
591
+ : { ok: false, available: false, ...BRIDGE_MODULE_MISSING };
546
592
  }
547
593
 
548
594
  function evalAvalanche(command) {
549
595
  const b = getBridge();
550
596
  if (!b) {
551
597
  const action = String(command?.action || 'status');
552
- return { ok: false, available: false, action, error: 'bridge module not loaded' };
598
+ return { ok: false, available: false, action, ...BRIDGE_MODULE_MISSING };
553
599
  }
554
600
  return b.evalAvalanche(command, getAvalancheApi(), getAvalancheModule());
555
601
  }
556
602
 
603
+ // An armed reload must install Avalanche before the next document's React
604
+ // commits. The arm config travels in sessionStorage, which the next
605
+ // document's startup script reads synchronously; the extension registers that
606
+ // script for one navigation. Without the extension the wait times out and the
607
+ // page reloads anyway. Returns how early startup was prepared, for the ack.
608
+ function prepareReload(avalanche) {
609
+ if (!avalanche || !avalanche.autoStart) return Promise.resolve('not-armed');
610
+ try {
611
+ const startup = /** @type {any} */ (global).__CHIPPY_STARTUP_CONFIG || {};
612
+ global.sessionStorage.setItem(STARTUP_HANDOFF_KEY, JSON.stringify({
613
+ at: Date.now(),
614
+ helperBaseUrl: config.helperBaseUrl,
615
+ plugins: Array.isArray(startup.plugins) ? startup.plugins : undefined,
616
+ avalanche: avalanche,
617
+ }));
618
+ } catch (e) {
619
+ return Promise.resolve('storage-unavailable');
620
+ }
621
+ return new Promise(function(done) {
622
+ let settled = false;
623
+ function finish(state) {
624
+ if (settled) return;
625
+ settled = true;
626
+ global.document.removeEventListener('chippy-armed-reload-ready', onReady);
627
+ done(state);
628
+ }
629
+ function onReady(event) {
630
+ let detail = {};
631
+ try { detail = JSON.parse(String(event.detail || '{}')); } catch (e) {}
632
+ finish(detail.ok ? 'registered' : 'handoff-only');
633
+ }
634
+ global.document.addEventListener('chippy-armed-reload-ready', onReady);
635
+ setTimeout(function() { finish('handoff-only'); }, 500);
636
+ global.document.dispatchEvent(new CustomEvent('chippy-armed-reload-prepare'));
637
+ });
638
+ }
639
+
557
640
  function handleCommand(command) {
558
641
  switch (command?.type) {
559
642
  case 'activate':
@@ -581,15 +664,19 @@
581
664
  setContextUiEnabled(command.enabled !== false, 'set-context-ui');
582
665
  break;
583
666
  case 'reload':
584
- try { send({ type: 'reload-ack', clientId, requestId: command.requestId || null }); } catch (e) {}
585
- setTimeout(function() { try { window.location.reload(); } catch (e) {} }, 50);
667
+ prepareReload(command.avalanche).then(function(earlyStartup) {
668
+ try { send({ type: 'reload-ack', clientId, requestId: command.requestId || null, earlyStartup: earlyStartup }); } catch (e) {}
669
+ setTimeout(function() { try { window.location.reload(); } catch (e) {} }, 50);
670
+ });
586
671
  break;
587
672
  case 'storybook-force-remount': {
588
673
  // The Storybook addon owns the host API. The bridge only relays the
589
674
  // request so a Storybook SUT remount does not navigate its preview.
675
+ // The arm, when present, lets the addon install Avalanche before the
676
+ // remount commits.
590
677
  const remountEvent = new CustomEvent('chippy-storybook-force-remount', {
591
678
  cancelable: true,
592
- detail: { requestId: command.requestId || null },
679
+ detail: { requestId: command.requestId || null, avalanche: command.avalanche || null },
593
680
  });
594
681
  global.dispatchEvent(remountEvent);
595
682
  try {
@@ -599,6 +686,7 @@
599
686
  requestId: command.requestId || null,
600
687
  action: 'storybook-force-remount',
601
688
  ok: remountEvent.defaultPrevented,
689
+ error: remountEvent.defaultPrevented ? undefined : 'The Storybook preview did not remount: no rendered story matched the current selection',
602
690
  });
603
691
  } catch (e) {}
604
692
  break;
@@ -655,8 +743,9 @@
655
743
  }
656
744
  case 'find-fibers': {
657
745
  const requestId = command.requestId || null;
658
- const result = evalFindFibers(command.filters || {});
659
- send({ type: 'eval-probe-result', clientId, requestId, result });
746
+ Promise.resolve(evalFindFibers(command.filters || {}))
747
+ .then((result) => send({ type: 'eval-probe-result', clientId, requestId, result }))
748
+ .catch((e) => send({ type: 'eval-probe-result', clientId, requestId, result: { count: 0, items: [], error: e?.message || String(e) } }));
660
749
  break;
661
750
  }
662
751
  case 'capture-fiber-state': {
@@ -873,35 +962,40 @@
873
962
  if (!t) return null;
874
963
  return t.displayName || t.name || (typeof t === 'string' ? t : null);
875
964
  }
965
+ // Return the live HostRoot fiber of every React root. A FiberRoot (from
966
+ // `getFiberRoots`) exposes it as `.current`; a HostRoot fiber reached from a
967
+ // DOM container or by climbing `.return` may be the stale alternate, so it
968
+ // is re-resolved through its FiberRoot (`stateNode.current`).
876
969
  function getRootFibers() {
877
970
  const hook = devtoolsHook();
878
971
  const out = [];
879
- if (!hook?.renderers) return out;
880
972
  const seen = new Set();
881
- for (const [rendererId] of hook.renderers) {
882
- try {
883
- if (hook.getFiberRoots) {
973
+ function add(candidate) {
974
+ if (!candidate || typeof candidate !== 'object') return;
975
+ const fiberRoot = candidate.current ? candidate
976
+ : (candidate.tag === 3 && candidate.stateNode && candidate.stateNode.current ? candidate.stateNode : null);
977
+ const live = fiberRoot ? fiberRoot.current : candidate;
978
+ const key = fiberRoot || live;
979
+ if (!live || seen.has(key)) return;
980
+ seen.add(key);
981
+ out.push(live);
982
+ }
983
+ if (hook?.renderers && typeof hook.getFiberRoots === 'function') {
984
+ for (const [rendererId] of hook.renderers) {
985
+ try {
884
986
  const roots = hook.getFiberRoots(rendererId);
885
- if (roots && typeof roots.forEach === 'function') {
886
- roots.forEach((root) => {
887
- if (root && !seen.has(root)) {
888
- seen.add(root);
889
- out.push(root);
890
- }
891
- });
892
- }
893
- }
894
- } catch (_) {}
987
+ if (roots && typeof roots.forEach === 'function') roots.forEach(add);
988
+ } catch (_) {}
989
+ }
895
990
  }
896
- // Robust fallback: walk all root containers via document
991
+ // Fallback for hooks without getFiberRoots: walk root containers in the document.
897
992
  const containers = document.querySelectorAll('[id], body div, body main');
898
993
  containers.forEach((el) => {
899
994
  const key = Object.keys(el).find((k) => k.startsWith('__reactContainer$') || k.startsWith('__reactFiber$'));
900
995
  if (!key) return;
901
996
  let fiber = el[key];
902
- // climb to root
903
997
  while (fiber && fiber.return) fiber = fiber.return;
904
- if (fiber && !seen.has(fiber)) { seen.add(fiber); out.push(fiber); }
998
+ add(fiber);
905
999
  });
906
1000
  return out;
907
1001
  }
@@ -1119,7 +1213,7 @@
1119
1213
  // Delegated to avalanche.iife.js — see src/chippy-bridge/fiber-invoke.ts
1120
1214
  function evalFiberPropContract(fiberId, propName) {
1121
1215
  const b = getBridge();
1122
- if (!b || !b.evalFiberPropContract) return { ok: false, error: 'bridge module not loaded' };
1216
+ if (!b || !b.evalFiberPropContract) return { ok: false, ...BRIDGE_MODULE_MISSING };
1123
1217
  return b.evalFiberPropContract(fiberId, propName, {
1124
1218
  normalizeRememberedFiberId,
1125
1219
  getRememberedFiber,
@@ -1130,7 +1224,7 @@
1130
1224
  // Returns a Promise. The handler routes async responses through `send` once resolved.
1131
1225
  function evalFiberInvoke(fiberId, propName, args) {
1132
1226
  const b = getBridge();
1133
- if (!b || !b.evalFiberInvoke) return Promise.resolve({ ok: false, error: 'bridge module not loaded' });
1227
+ if (!b || !b.evalFiberInvoke) return Promise.resolve({ ok: false, ...BRIDGE_MODULE_MISSING });
1134
1228
  return Promise.resolve(b.evalFiberInvoke(fiberId, propName, args, {
1135
1229
  normalizeRememberedFiberId,
1136
1230
  getRememberedFiber,
@@ -1143,7 +1237,7 @@
1143
1237
  function evalFiberScheduleUpdate(fiberId, ownerIndex) {
1144
1238
  const b = getBridge();
1145
1239
  if (!b || !b.evalFiberScheduleUpdate) {
1146
- return { ok: false, reason: 'no-scheduleUpdate', error: 'bridge module not loaded' };
1240
+ return { ok: false, reason: 'no-scheduleUpdate', ...BRIDGE_MODULE_MISSING };
1147
1241
  }
1148
1242
  return b.evalFiberScheduleUpdate(fiberId, ownerIndex, {
1149
1243
  normalizeRememberedFiberId,
@@ -1154,7 +1248,7 @@
1154
1248
  // Delegated to avalanche.iife.js — see src/chippy-bridge/eval-helpers.ts
1155
1249
  function evalElementDom(element, parentLevel) {
1156
1250
  const b = getBridge();
1157
- if (!b) return { error: 'bridge module not loaded' };
1251
+ if (!b) return { ...BRIDGE_MODULE_MISSING };
1158
1252
  return b.evalElementDom(element, parentLevel, window.getComputedStyle.bind(window));
1159
1253
  }
1160
1254
 
@@ -1167,14 +1261,14 @@
1167
1261
  // Delegated to avalanche.iife.js — see src/chippy-bridge/eval-helpers.ts
1168
1262
  function evalFiberProps(baseFiber, ownerIndex) {
1169
1263
  const b = getBridge();
1170
- if (!b) return { error: 'bridge module not loaded' };
1264
+ if (!b) return { ...BRIDGE_MODULE_MISSING };
1171
1265
  return b.evalFiberProps(baseFiber, ownerIndex, chippy.layoutLab?.getHookSummary, resolveOwnerFiber);
1172
1266
  }
1173
1267
 
1174
1268
  // Delegated to avalanche.iife.js — see src/chippy-bridge/eval-helpers.ts
1175
1269
  function evalFiberPropValue(baseFiber, propName, ownerIndex) {
1176
1270
  const b = getBridge();
1177
- if (!b) return { error: 'bridge module not loaded' };
1271
+ if (!b) return { ...BRIDGE_MODULE_MISSING };
1178
1272
  return b.evalFiberPropValue(baseFiber, propName, ownerIndex, chippy.layoutLab?.getHookSummary, resolveOwnerFiber, safeSerialize);
1179
1273
  }
1180
1274
 
@@ -1188,7 +1282,7 @@
1188
1282
 
1189
1283
  function summarizeHookList(raw) {
1190
1284
  const b = getBridge();
1191
- return b ? b.summarizeHookList(raw) : (raw || { error: 'bridge module not loaded' });
1285
+ return b ? b.summarizeHookList(raw) : (raw || { ...BRIDGE_MODULE_MISSING });
1192
1286
  }
1193
1287
 
1194
1288
  function formatCounts(counts) {
@@ -1372,15 +1466,20 @@
1372
1466
  }
1373
1467
 
1374
1468
  // Delegated to avalanche.iife.js — see src/chippy-bridge/eval-helpers.ts
1375
- function evalFindFibers(filters) {
1469
+ async function evalFindFibers(filters) {
1376
1470
  const b = getBridge();
1377
- if (!b) return { count: 0, items: [], error: 'bridge module not loaded' };
1378
- return b.evalFindFibers(filters, buildScriptHelpers([]), rememberFiber);
1471
+ if (!b) return { count: 0, items: [], ...BRIDGE_MODULE_MISSING };
1472
+ const result = b.evalFindFibers(filters, buildScriptHelpers([]), rememberFiber);
1473
+ // Map each item's source through the served module, as `explain` does.
1474
+ if (b.resolveFindFiberSources && b.resolveFiberSource) {
1475
+ await b.resolveFindFiberSources(result.items, getRememberedFiber, b.resolveFiberSource);
1476
+ }
1477
+ return result;
1379
1478
  }
1380
1479
 
1381
1480
  function evalCaptureFiberState(options) {
1382
1481
  const b = getBridge();
1383
- if (!b?.captureFiberState) return { ok: false, error: 'bridge module not loaded' };
1482
+ if (!b?.captureFiberState) return { ok: false, ...BRIDGE_MODULE_MISSING };
1384
1483
  const helpers = buildScriptHelpers([]);
1385
1484
  return b.captureFiberState({
1386
1485
  ...options,
@@ -1393,7 +1492,7 @@
1393
1492
 
1394
1493
  function evalCompareFiberStates(before, after, options) {
1395
1494
  const b = getBridge();
1396
- if (!b?.compareFiberStates) return { ok: false, error: 'bridge module not loaded' };
1495
+ if (!b?.compareFiberStates) return { ok: false, ...BRIDGE_MODULE_MISSING };
1397
1496
  return b.compareFiberStates(before, after, options);
1398
1497
  }
1399
1498
 
@@ -1401,7 +1500,7 @@
1401
1500
  // extension shim stays version-coupled but implementation-free.
1402
1501
  function evalBoundaryChain(fiber, max) {
1403
1502
  const b = getBridge();
1404
- if (!b) return { ok: false, error: 'bridge module not loaded' };
1503
+ if (!b) return { ok: false, ...BRIDGE_MODULE_MISSING };
1405
1504
  return b.inspectDeclarativeBoundaryChain(fiber, max, {
1406
1505
  rememberFiber,
1407
1506
  getFiberDomNode: getAvalancheApi()?.layoutLab?.getFiberDomNode || getFiberDomNode,
@@ -1422,7 +1521,7 @@
1422
1521
 
1423
1522
  function evalPageBoundaries(max) {
1424
1523
  const b = getBridge();
1425
- if (!b) return { ok: false, error: 'bridge module not loaded' };
1524
+ if (!b) return { ok: false, ...BRIDGE_MODULE_MISSING };
1426
1525
  const helpers = buildScriptHelpers([]);
1427
1526
  return b.inspectPageDeclarativeBoundaries(max, {
1428
1527
  rememberFiber,
@@ -1470,7 +1569,7 @@
1470
1569
  const fiber = getRememberedFiber(fiberId);
1471
1570
  if (!fiber) return { error: 'Unknown fiberId: ' + fiberId };
1472
1571
  const b = getBridge();
1473
- if (!b) return { error: 'bridge module not loaded' };
1572
+ if (!b) return { ...BRIDGE_MODULE_MISSING };
1474
1573
  return b.inspectFiberStructure(fiber, fiberId, hookIndex, windowSize, replayComponentHooks, evalFiberHooks);
1475
1574
  }
1476
1575
 
@@ -1515,7 +1614,7 @@
1515
1614
 
1516
1615
  async function evalDescribeComponent(fiberIdRaw, nameRaw) {
1517
1616
  const b = getBridge();
1518
- if (!b) return { ok: false, error: 'bridge module not loaded' };
1617
+ if (!b) return { ok: false, ...BRIDGE_MODULE_MISSING };
1519
1618
  const target = resolveFiberTarget(null, fiberIdRaw, nameRaw);
1520
1619
  if (target.error) return { ok: false, error: target.error };
1521
1620
  if (b.resolveFiberSource) await b.resolveFiberSource(target.fiber);
@@ -1576,7 +1675,7 @@
1576
1675
  const fiber = getRememberedFiber(fiberId);
1577
1676
  if (!fiber) return { error: 'Unknown fiberId: ' + fiberId };
1578
1677
  const b = getBridge();
1579
- if (!b) return { error: 'bridge module not loaded' };
1678
+ if (!b) return { ...BRIDGE_MODULE_MISSING };
1580
1679
  return b.inspectFiberOwners(fiber, fiberId, buildScriptHelpers([]));
1581
1680
  }
1582
1681
 
@@ -1708,6 +1807,22 @@
1708
1807
  };
1709
1808
  global.document.addEventListener('chippy-ext-command', onExtCommand);
1710
1809
 
1810
+ // An extension bridge that is already running stands down when another
1811
+ // integration claims the page; the background's owner check covers bridges
1812
+ // that have not loaded yet.
1813
+ function standDown(owner) {
1814
+ if (destroyed) return;
1815
+ destroy();
1816
+ try { chippy.layoutLab?.clearAll?.(); } catch (e) {}
1817
+ try { chippy.layoutLab?.removeInPageUi?.(); } catch (e) {}
1818
+ chippy.standDown = { at: Date.now(), owner: owner?.integration || null };
1819
+ setConnState('yielded');
1820
+ }
1821
+ onOwnerClaimed = () => {
1822
+ if (isExtensionBridge() && isLiveForeignOwner(global.__CHIPPY_OWNER__)) standDown(global.__CHIPPY_OWNER__);
1823
+ };
1824
+ global.addEventListener('chippy-owner-claimed', onOwnerClaimed);
1825
+
1711
1826
  locationInterval = global.setInterval(() => {
1712
1827
  if (global.location.href !== lastLocationHref) {
1713
1828
  lastLocationHref = global.location.href;
@@ -1763,6 +1878,7 @@
1763
1878
  try { if (onLayoutLabUpdate) global.removeEventListener('layout-lab:update', onLayoutLabUpdate); } catch (e) {}
1764
1879
  try { if (onWindowMessage) global.removeEventListener('message', onWindowMessage); } catch (e) {}
1765
1880
  try { if (onExtCommand) global.document.removeEventListener('chippy-ext-command', onExtCommand); } catch (e) {}
1881
+ try { if (onOwnerClaimed) global.removeEventListener('chippy-owner-claimed', onOwnerClaimed); } catch (e) {}
1766
1882
  }
1767
1883
 
1768
1884
  // ─── Public API ──────────────────────────────────────────────────────────
@@ -1829,5 +1945,9 @@
1829
1945
  chippy.layoutLab?.exposeDefaultMarkers?.();
1830
1946
  }
1831
1947
 
1832
- connect();
1948
+ if (isExtensionBridge() && isLiveForeignOwner(global.__CHIPPY_OWNER__)) {
1949
+ standDown(global.__CHIPPY_OWNER__);
1950
+ } else {
1951
+ connect();
1952
+ }
1833
1953
  })(window);
@@ -73,7 +73,7 @@ Chippy supports three page-side integrations:
73
73
  | Module import | A page without the extension UI | Applies to the current document; import again after reload/navigation |
74
74
  | Storybook addon | A Storybook preview | Reconnects through preview annotations |
75
75
 
76
- On extension-owned pages, Chippy starts as a small bottom-corner launcher. Expanding it reveals one unified panel with compact `P1 — drag to source` and `P2 — drag to source` rows that act as both dock and probe detail. Dragging a probe out keeps the panel expanded; once placed, the probe is document-anchored and follows its target during page scroll. Helper and optional forward-port settings live behind its gear. Probe and connection updates patch existing controls in place, so drafts, focus, and the settings form survive ordinary refreshes. Active probes commit their annotation when the intent changes or Enter is pressed in the comment; Shift+Enter inserts a newline. The toolbar popup shows the Chippy mark and name with Enable and Disable for the current origin. Storybook is the intentional exception: its details live in the manager panel, so its preview frame retains a standalone probe dock.
76
+ On extension-owned pages, Chippy starts as a small bottom-corner launcher. Expanding it reveals one unified panel with compact `P1 — drag to source` and `P2 — drag to source` rows that act as both dock and probe detail. Dragging a probe out keeps the panel expanded; once placed, the probe is document-anchored and follows its target during page scroll. The helper URL and plugin switches live behind its gear. Probe and connection updates patch existing controls in place, so drafts, focus, and the settings form survive ordinary refreshes. Active probes commit their annotation when the intent changes or Enter is pressed in the comment; Shift+Enter inserts a newline. The toolbar popup shows the Chippy mark and name with Enable and Disable for the current origin. Storybook is the intentional exception: its details live in the manager panel, so its preview frame retains a standalone probe dock.
77
77
 
78
78
  When the extension workflow fits, load this skill's `extension` folder through **Load unpacked** in the browser's extension developer mode. The module-import and Playwright recipes live in [playwright-cli-setup](references/playwright-cli-setup.md).
79
79
 
@@ -97,7 +97,7 @@ Chippy deliberately keeps different facts separate:
97
97
  - **DOM facts:** the exact element, text, attributes, styles, and nearest test-id boundary.
98
98
  - **React facts:** the fiber, component, owners, parents, props, hooks, and source metadata for the current snapshot.
99
99
  - **Declarative boundary facts:** every Error/Suspense ancestor around a target, or the whole page reduced to a boundary-only containment forest, including current Suspense content/fallback state.
100
- - **Jira context:** optional probe enrichment for recognized ownership markers and `JSErrorBoundary` metadata; it appears in both the in-page and Storybook probe details, defaults on, and does not replace source evidence.
100
+ - **Jira context:** optional probe enrichment for recognized ownership markers and `JSErrorBoundary` metadata; it appears in both the in-page and Storybook probe details once the Jira plugin is enabled (it is off by default), and does not replace source evidence.
101
101
  - **Source facts:** what the reported file and code can do.
102
102
  - **Render facts:** which commits, sources, restarts, effects, context updates, and collateral occurred in the measured window.
103
103
  - **Action facts:** what the user or automation did immediately before a measured commit.
@@ -142,8 +142,6 @@ Performance work has three distinct entry points:
142
142
  - Probes and commit captures are evidence anchors. Preserve them until the question is answered and any edit is verified.
143
143
  - Development source resolution supports React <=18 `_debugSource` and React 19 `_debugStack` plus the dev server's source map. React 19 requires no JSX-runtime instrumentation when its development build and maps are available.
144
144
  - Production or minified pages can reduce source and component confidence. Chippy reports the boundary instead of inventing missing detail.
145
- - Browser extension file changes require an extension publish cycle; rebuilding the bundled runtime alone does not publish it.
146
- - New commands, options, fields, and behavior changes require shell, wire, type/DTO, test, and documentation parity.
147
145
 
148
146
  ## License
149
147
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: chippy
3
- description: Debug a running React page with live runtime evidence. Use to explore components (find by name, text, test id, file, or prop; explain structure and hooks), debug where an element, value, prop, or loading/error boundary comes from, compare two rendered states or pages, and investigate slow interactions, re-renders, and initial load with Avalanche commit capture. Connects through the Chippy browser extension or an injected bundle and a local helper; not for static code review without a running page.
3
+ description: Debug a running React page with live runtime evidence. Use to explore components (find by name, text, test id, file, or prop; explain structure and hooks), debug where an element, value, prop, or loading/error boundary comes from, compare two rendered states or pages, and investigate slow interactions, re-renders, and initial load with Avalanche commit capture. Connects a local helper to the page through the browser extension, an agent-driven Playwright browser, the helper's local proxy, the Storybook addon, or a console/module import; not for static code review without a running page.
4
4
  allowed-tools: Bash(chippy.sh:*)
5
5
  ---
6
6
 
@@ -21,15 +21,15 @@ Use the running page as evidence. Connect visible React behavior to exact source
21
21
 
22
22
  ## Start every task
23
23
 
24
- 1. If neither Chippy MCP tools nor this skill's scripts are installed, have the user run `npx @atlassian/chippy install` and follow its printed steps. Resolve this skill's absolute path; do not assume the current repository contains a relative copy. Prefer available Chippy MCP tools when permitted by the host; otherwise run this skill's `scripts/chippy.sh`.
24
+ 1. If neither Chippy MCP tools nor this skill's scripts are installed, have the user run `npx skills add atlassian-labs/chippy --skill chippy -g`; the skill then lives in `~/.agents/skills/chippy`, and its `extension` folder is the unpacked browser extension. Resolve this skill's absolute path; do not assume the current repository contains a relative copy. Prefer available Chippy MCP tools when permitted by the host; otherwise run this skill's `scripts/chippy.sh`.
25
25
  2. With MCP available, use `chippy_status` → `chippy_pages` → `chippy_snapshot` (refresh, then read). MCP starts/reuses the helper automatically; follow [MCP setup and tools](references/mcp.md). MCP covers inspection, Avalanche capture and analysis, interaction, and annotation waits; compare (`fiber-state-*`), `page-boundaries`, `avalanche-arm`, `reload-with-avalanche`, and `recipes` are CLI-only, so use `scripts/chippy.sh` for them. Otherwise run `health` → `pages` → `request-snapshot <clientId>`; helper-contacting shell commands must run outside the agent sandbox. The agent owns helper startup and command execution.
26
- 3. Select the page by URL, title, integration identity, and recency. Resolve a fresh `clientId` every turn; a reload or navigation can replace it. When two pages match closely, ask which page is in scope.
26
+ 3. Select the page by URL, title, integration identity, and recency. When no page is connected, choose a route in [Connection and bootstrap](#connection-and-bootstrap); the extension is one route, not a prerequisite. Resolve a fresh `clientId` every turn; a reload or navigation can replace it. When two pages match closely, ask which page is in scope.
27
27
  4. Use the first read-only signal to leave ❓ Disorder and name the Cynefin domain. Then iterate with OODA inside that domain: observe one fact, orient it to the invariant and proof gap, decide one permitted move, act, and observe again.
28
28
  5. Choose the narrowest route below. Do not edit until the proof packet names the fix layer and a repeatable verification command.
29
29
 
30
30
  `health`, `pages`, `request-snapshot`, and reads of existing probe state are orientation. Active stimuli, diagnosis, and source edits are not.
31
31
 
32
- When an extension-connected page already has the target state, work there first. Use Playwright only when automation or replay is required, the user asks for it, or the connected page cannot provide the evidence.
32
+ When a page connected in the user's own browser already has the target state, work there first. Use Playwright only when automation or replay is required, the user asks for it, or the connected page cannot provide the evidence.
33
33
 
34
34
  ## Route by question
35
35
 
@@ -45,7 +45,8 @@ Before `explain-component`, `describe-component`, or `inspect-fiber-structure`,
45
45
  | What is inside this component? | `explain-component`; use `describe-component` for the detailed hook tree | The component's structure is described, with replay status and warnings disclosed. |
46
46
  | What changed between two rendered states or pages? | `fiber-state-capture` before and after → `fiber-state-compare` | The bounded diff names matched fibers and added, removed, or changed props without returning either full capture. |
47
47
  | Why is an interaction slow or re-rendering? | `avalanche-arm` → reproduce the interaction → `avalanche-status` → hot sources / commit list / commit peek | Trigger, hotspot, collateral, and fix layer are distinguished. |
48
- | Why is initial load slow? | `reload-with-avalanche` → status / hot sources / commit analysis | The measured load window is bounded and repeatable. |
48
+ | Why is initial load slow? | Confirm the page's route records the initial load ([Connection and bootstrap](#connection-and-bootstrap)) → `reload-with-avalanche` → status / hot sources / commit analysis | The measured load window is bounded and repeatable. |
49
+ | Does the user want render evidence in tests or scripts, with no agent at run time? | Point to the standalone Avalanche detector in the [npm package documentation](https://github.com/atlassian-labs/chippy/blob/main/docs/npm-package.md#standalone-avalanche) | The test installs the detector before React DOM loads and reads `detector.report()`. |
49
50
  | Is this subtree stable? | Load [stability-render](references/stability-render.md) and run one controlled nudge | The capture yields clean, leak, leak+cascade, or trasher evidence. |
50
51
  | Which hook is suspicious? | Use `avalanche-debug-hook` only after a capture identifies `sourceId + hookIndex` | Primitive/custom-hook ownership and next remediation branch are named. |
51
52
  | Are state dispatches or memo/effect recomputations driving the cost? | Load [hook-metrics](references/hook-metrics.md) | A before/after counter or tracker result is joined to the same interaction. |
@@ -62,7 +63,7 @@ Use [bdd-router](references/bdd-router.md) when the situation needs the complete
62
63
 
63
64
  - A probe identifies one exact DOM element rendered by one bundle. Its source location, fiber, owner chain, and marker coordinates are facts for that snapshot.
64
65
  - Open `source.fileName` at `lineNumber` directly once it is an authored path. On React 19, an initial `http(s)` module URL is the generated fallback; read the following `*-source-resolved` snapshot or run `describe-component` before treating its line as a repository position. Do not grep for a file the probe already identified. Search is the last resort after the runtime-to-source chain is exhausted.
65
- - Development source metadata is version-dependent: React <=18 supplies `_debugSource`; React 19 supplies `_debugStack`. Chippy filters dependency frames, fetches the served module in the page realm, decodes its inline or external source map, and republishes the authored file/line. Absence of `_debugSource` alone is not production evidence.
66
+ - Development source metadata is version-dependent: React <=18 supplies `_debugSource`; React 19 supplies `_debugStack`. Chippy filters dependency frames, fetches the served module in the page realm, decodes its inline or external source map, and republishes the authored file/line. It also corrects `_debugSource` lines that a dev-server transform shifted before JSX compilation, such as Vite's Fast Refresh header. `find-fibers`, `describe-component`, and `explain-component` return the mapped position. Absence of `_debugSource` alone is not production evidence.
66
67
  - Read every returned field. Props, owner indices, keys, test IDs, hook counts, and source locations can each change the next move.
67
68
  - Static source proves what code can do. Runtime evidence proves what happened in this run. Never promote code shape, a global's presence, or a hot component into a runtime cause.
68
69
  - Screenshots confirm appearance; they do not replace probe, fiber, action, or commit evidence.
@@ -118,13 +119,17 @@ Chippy has a local helper and one page-side integration. `chippy_pages` or
118
119
  helper startup without a shell command; it needs host access to the browser's
119
120
  network environment. Closing an MCP session preserves the shared helper.
120
121
 
121
- | Integration | Owner | Reload behavior |
122
- |---|---|---|
123
- | Browser extension | Chippy extension | Re-injects automatically for enabled origins. |
124
- | Console/module import | The current document | Full reload/navigation removes Chippy; run the import again. |
125
- | Storybook addon | Storybook preview annotations | `page-reload` uses Storybook `FORCE_REMOUNT` to remount the SUT without reloading the preview iframe. |
122
+ Each route below loads the same page runtime. The extension is one delivery route; choose by who owns the browser and whether the task needs initial-load capture.
123
+
124
+ | Route | Who sets it up | Reload behavior | Initial-load capture |
125
+ |---|---|---|---|
126
+ | Browser extension | User, once per browser and origin | Re-injects automatically for enabled origins. | Yes, with `reload-with-avalanche`: the armed reload runs the early scripts at `document_start`. |
127
+ | Agent-driven Playwright browser | Agent; no extension | Init scripts load again on every navigation; the ad-hoc import must be repeated. | Only with the [pre-page start-up](references/mode-a-injection.md#pre-page-start-up) variant, which installs the detector before React on every document. The packaged loader and the ad-hoc import start the detector after connection. |
128
+ | Local proxy (`proxy-start <targetPort> [originalUrl]`) | Agent; the user or Playwright opens the page on the helper port | The proxy adds Chippy to every load of the `originalUrl` path, so pass `originalUrl`. | Yes, with `reload-with-avalanche`: the shim and Avalanche run before page scripts. Targets on this machine only. |
129
+ | Storybook addon (`addons: ['@atlassian/chippy/storybook']`, Storybook 10.3+) | User, in the Storybook configuration | `page-reload` uses Storybook `FORCE_REMOUNT` to remount the SUT without reloading the preview iframe. | The story's mount, through the remount. |
130
+ | Console/module import, including `connectChippy()` from `@atlassian/chippy/browser` | User or page code, in the current document | Full reload/navigation removes Chippy; run the import again. | No: use mid-page capture. |
126
131
 
127
- The extension toolbar popup shows the Chippy mark and name with Enable and Disable for the current origin. The in-page UI starts as one small bottom-corner Chippy button. Expand it to reveal compact `P1 — drag to source` and `P2 — drag to source` rows; each row is the probe's dock and expands into its details once placed. Dragging a probe out keeps the panel expanded, and a placed probe is document-anchored so it moves with its target during page scroll. The gear contains the helper URL, the optional forward port, and the Jira and ATL plugin switches. Active probes commit their annotation when the intent changes or Enter is pressed in the comment; Shift+Enter inserts a newline. Storybook intentionally keeps its probe dock in the preview because its details panel lives in the manager frame.
132
+ The extension toolbar popup shows the Chippy mark and name with Enable and Disable for the current origin. The in-page UI starts as one small bottom-corner Chippy button. Expand it to reveal compact `P1 — drag to source` and `P2 — drag to source` rows; each row is the probe's dock and expands into its details once placed. Dragging a probe out keeps the panel expanded, and a placed probe is document-anchored so it moves with its target during page scroll. The gear contains the helper URL and the Jira and ATL plugin switches. Active probes commit their annotation when the intent changes or Enter is pressed in the comment; Shift+Enter inserts a newline. Storybook intentionally keeps its probe dock in the preview because its details panel lives in the manager frame.
128
133
 
129
134
  Each connected page reports `integration.kind`, `who`, `source`, `reinjectsOnReload`, and `reloadAction`. Use those fields instead of inferring how Chippy arrived.
130
135
 
@@ -136,14 +141,14 @@ For a user-controlled page without the extension, run this in the browser consol
136
141
  import('http://127.0.0.1:7823/__chippy/inject.mjs')
137
142
  ```
138
143
 
139
- The single `import(...)` is all that is needed: the ESM bootstrap installs Chippy, loads the Avalanche bridge runtime (so fiber-backed reads and captures work immediately — no second asset), and activates probes for the current document. It renders no in-page control panel/overlay. After reload or navigation, import again.
144
+ The single `import(...)` is all that is needed: the ESM bootstrap installs Chippy, loads the Avalanche bridge runtime (so fiber-backed reads and captures work immediately — no second asset), and activates probes for the current document. It renders no in-page control panel/overlay. After reload or navigation, import again. A strict Content Security Policy blocks the import; on a local dev server, use the proxy instead, which removes the page's CSP headers.
140
145
 
141
- For extension-free pages where reload destroys Chippy (e.g. a production page using console import), **do not use `reload-with-avalanche`**. The correct workflow is mid-page capture: import Chippy once, install/arm Avalanche on the live page (`avalanche-install` or `avalanche-arm`), then trigger a controlled render stimulus such as `fiber-schedule-update` / the stability nudge. `reload-with-avalanche` is only for reload-surviving integrations such as the extension or Storybook addon. On Storybook, its reload step is a `FORCE_REMOUNT` of the current SUT, not an iframe navigation.
146
+ For pages where reload destroys Chippy (console import or `connectChippy()`), **do not use `reload-with-avalanche`**. The correct workflow is mid-page capture: import Chippy once, install/arm Avalanche on the live page (`avalanche-install` or `avalanche-arm`), then trigger a controlled render stimulus such as `fiber-schedule-update` / the stability nudge. `reload-with-avalanche` is for routes that load Chippy before page scripts on the reloaded document: the extension, the local proxy, and the Storybook addon. On Storybook, its reload step is a `FORCE_REMOUNT` of the current SUT, not an iframe navigation. A Playwright page with the pre-page start-up variant installs the detector on every document itself, so a plain `page.reload()` captures the load.
142
147
 
143
148
  The helper serves matching source maps alongside its Avalanche bundles; browser
144
149
  developer tools can resolve the runtime frames without missing-map requests.
145
150
 
146
- For Playwright-controlled pages, use [playwright-cli-setup](references/playwright-cli-setup.md). The classic fallback requires `inject.js` followed by `avalanche.iife.js`; `bridge module not loaded` means the second asset is missing.
151
+ For an agent-driven Playwright browser, follow [mode-a-injection](references/mode-a-injection.md) for the loader variants and their capture limits, and [playwright-cli-setup](references/playwright-cli-setup.md) for ad-hoc `playwright-cli` sessions. The classic fallback requires `inject.js` followed by `avalanche.iife.js`; `bridge module not loaded` means the second asset is missing.
147
152
 
148
153
  ### Explicit wait mode
149
154
 
@@ -276,7 +281,7 @@ Arm before the user or harness performs the slow interaction. Probes drill into
276
281
 
277
282
  `latestAction` is a clue, not causality. A confirmed root cause needs a source-to-collateral chain or source code that proves the update origin. Unmount and Suspense fields are branch signals; report DOM regions before wrapper names when the question is about DOM teardown.
278
283
 
279
- Use `detectionMode=full` when `commit-peek include=rendered` or per-component render evidence is required. `sources` and `stats` do not populate `renderedByCommit`. If broad triage began in the default `sources` mode, run `avalanche-recapture <clientId> full`, reproduce the identical interaction, and only then read rendered fields. Load [avalanche-detector](references/avalanche-detector.md) for exact schemas and [fixing-performance](references/fixing-performance.md) for fix-layer selection.
284
+ Every mode records which components rendered and their prop changes (`commit-peek include=rendered`). `stats` leaves `isSelfSource` false, and only `full` or `amplifiers` fill `trasherCounts`. When amplifier evidence is required and triage began in the default `sources` mode, run `avalanche-recapture <clientId> full`, reproduce the identical interaction, and only then read `trasherCounts`. Load [avalanche-detector](references/avalanche-detector.md) for exact schemas and [fixing-performance](references/fixing-performance.md) for fix-layer selection.
280
285
 
281
286
  ## Snippets and active stimuli
282
287
 
@@ -0,0 +1,83 @@
1
+ /* eslint-disable */
2
+ // One-shot early startup for an armed reload, loaded into the background
3
+ // service worker with importScripts().
4
+ //
5
+ // The background's normal startup injection waits on several asynchronous
6
+ // lookups after navigation commits, so a fast page can commit React before
7
+ // Avalanche installs. Before an armed reload, the page bridge stores the arm in
8
+ // sessionStorage and asks for this registration. Chrome then runs the scripts
9
+ // below synchronously at document_start of the next document, and
10
+ // startup-handoff.js turns the stored arm into the startup config.
11
+ //
12
+ // The registration covers one navigation of one frame: it is removed when that
13
+ // document finishes parsing, when its navigation fails, or when it is older
14
+ // than PENDING_MAX_AGE_MS at the next navigation event.
15
+ (function chippyArmedReloadStartup(global) {
16
+ const SCRIPT_ID = 'chippy-armed-reload-startup';
17
+ const PENDING_KEY = 'chippyArmedReloadPending';
18
+ const PENDING_MAX_AGE_MS = 60000;
19
+ const FILES = ['devtools-hook-shim.js', 'startup-handoff.js', 'avalanche.iife.js', 'early-bootstrap.js'];
20
+
21
+ function originPattern(url) {
22
+ const parsed = new URL(url);
23
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') throw new Error(`Unsupported page URL: ${url}`);
24
+ return `${parsed.origin}/*`;
25
+ }
26
+
27
+ async function unregister() {
28
+ try { await chrome.scripting.unregisterContentScripts({ ids: [SCRIPT_ID] }); } catch {}
29
+ try { await chrome.storage.session.remove(PENDING_KEY); } catch {}
30
+ }
31
+
32
+ // Registration calls run one at a time; Chrome rejects a concurrent
33
+ // registration of the same id.
34
+ let queue = Promise.resolve();
35
+ function serialized(task) {
36
+ const run = queue.then(task, task);
37
+ queue = run.catch(() => {});
38
+ return run;
39
+ }
40
+
41
+ async function register({ tabId, frameId, url }) {
42
+ const pattern = originPattern(url);
43
+ await unregister();
44
+ await chrome.scripting.registerContentScripts([{
45
+ id: SCRIPT_ID,
46
+ matches: [pattern],
47
+ js: FILES,
48
+ runAt: 'document_start',
49
+ world: 'MAIN',
50
+ allFrames: frameId !== 0,
51
+ persistAcrossSessions: false,
52
+ }]);
53
+ await chrome.storage.session.set({ [PENDING_KEY]: { tabId, frameId, pattern, at: Date.now() } });
54
+ return { ok: true, pattern };
55
+ }
56
+
57
+ async function settle(details) {
58
+ let pending;
59
+ try { pending = (await chrome.storage.session.get(PENDING_KEY))[PENDING_KEY]; } catch { return; }
60
+ if (!pending) return;
61
+ const ours = pending.tabId === details.tabId && pending.frameId === details.frameId;
62
+ if (ours || Date.now() - pending.at > PENDING_MAX_AGE_MS) await serialized(unregister);
63
+ }
64
+
65
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
66
+ if (message?.type !== 'chippy-register-armed-reload') return false;
67
+ const tabId = sender.tab?.id;
68
+ if (tabId == null || !sender.url) {
69
+ sendResponse({ ok: false, error: 'No sender tab for the armed reload' });
70
+ return false;
71
+ }
72
+ serialized(() => register({ tabId, frameId: sender.frameId ?? 0, url: sender.url }))
73
+ .then(sendResponse)
74
+ .catch((e) => sendResponse({ ok: false, error: e.message }));
75
+ return true;
76
+ });
77
+
78
+ // document_start injection has happened by DOMContentLoaded.
79
+ chrome.webNavigation.onDOMContentLoaded.addListener((details) => { settle(details); });
80
+ chrome.webNavigation.onErrorOccurred.addListener((details) => { settle(details); });
81
+
82
+ global.chippyArmedReloadStartup = { SCRIPT_ID, FILES, originPattern };
83
+ })(globalThis);