@atlassian/chippy 0.0.0-stage → 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 (133) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +276 -2
  3. package/bin/chippy.mjs +92 -0
  4. package/bin/pack.mjs +204 -0
  5. package/bin/skill-dir.mjs +15 -0
  6. package/lib/avalanche.js +2601 -0
  7. package/lib/avalanche.js.map +7 -0
  8. package/lib/bootstrap.d.ts +1 -0
  9. package/lib/bootstrap.js +70 -0
  10. package/lib/bootstrap.js.map +7 -0
  11. package/lib/browser.js +15 -0
  12. package/lib/browser.js.map +7 -0
  13. package/lib/extension/avalanche.es.js +5535 -0
  14. package/lib/extension/avalanche.es.js.map +1 -0
  15. package/lib/extension/chippy-page-bridge.js +1953 -0
  16. package/lib/extension/inspect-react.js +216 -0
  17. package/lib/extension/layout-lab.js +1453 -0
  18. package/lib/extension/panel-dom.js +90 -0
  19. package/lib/extension/resolve-probes.js +342 -0
  20. package/lib/mcp-skill/LICENSE +201 -0
  21. package/lib/mcp-skill/README.md +148 -0
  22. package/lib/mcp-skill/SKILL.md +335 -0
  23. package/lib/mcp-skill/browser-scripts/playwright-probes.js +368 -0
  24. package/lib/mcp-skill/browser-scripts/playwright-session.js +114 -0
  25. package/lib/mcp-skill/extension/armed-reload-startup.js +83 -0
  26. package/lib/mcp-skill/extension/avalanche.es.js +5535 -0
  27. package/lib/mcp-skill/extension/avalanche.es.js.map +1 -0
  28. package/lib/mcp-skill/extension/avalanche.iife.js +5536 -0
  29. package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -0
  30. package/lib/mcp-skill/extension/background.js +325 -0
  31. package/lib/mcp-skill/extension/chippy-page-bridge.js +1953 -0
  32. package/lib/mcp-skill/extension/content-script.js +190 -0
  33. package/lib/mcp-skill/extension/devtools-hook-shim.js +85 -0
  34. package/lib/mcp-skill/extension/early-bootstrap.js +49 -0
  35. package/lib/mcp-skill/extension/icon-128.png +0 -0
  36. package/lib/mcp-skill/extension/icon-16.png +0 -0
  37. package/lib/mcp-skill/extension/icon-32.png +0 -0
  38. package/lib/mcp-skill/extension/icon-48.png +0 -0
  39. package/lib/mcp-skill/extension/inspect-react.js +216 -0
  40. package/lib/mcp-skill/extension/layout-lab.js +1453 -0
  41. package/lib/mcp-skill/extension/logo.svg +14 -0
  42. package/lib/mcp-skill/extension/manifest.json +69 -0
  43. package/lib/mcp-skill/extension/panel-dom.js +90 -0
  44. package/lib/mcp-skill/extension/panel-settings.js +173 -0
  45. package/lib/mcp-skill/extension/popup.html +41 -0
  46. package/lib/mcp-skill/extension/popup.js +80 -0
  47. package/lib/mcp-skill/extension/probe-descriptor.js +241 -0
  48. package/lib/mcp-skill/extension/resolve-probes.js +342 -0
  49. package/lib/mcp-skill/extension/startup-handoff.js +30 -0
  50. package/lib/mcp-skill/package.json +1 -0
  51. package/lib/mcp-skill/references/README.md +56 -0
  52. package/lib/mcp-skill/references/active-stimulus.md +85 -0
  53. package/lib/mcp-skill/references/avalanche-detector.md +731 -0
  54. package/lib/mcp-skill/references/bdd-router.md +310 -0
  55. package/lib/mcp-skill/references/browser-runtime-api.md +193 -0
  56. package/lib/mcp-skill/references/command-output-formats.md +1085 -0
  57. package/lib/mcp-skill/references/command-surface.md +183 -0
  58. package/lib/mcp-skill/references/cynefin.md +99 -0
  59. package/lib/mcp-skill/references/debug-hook.md +161 -0
  60. package/lib/mcp-skill/references/fixing-performance.md +490 -0
  61. package/lib/mcp-skill/references/hook-metrics.md +199 -0
  62. package/lib/mcp-skill/references/hooks-inspection.md +61 -0
  63. package/lib/mcp-skill/references/jira-taxonomy.md +81 -0
  64. package/lib/mcp-skill/references/mcp.md +122 -0
  65. package/lib/mcp-skill/references/mode-a-injection.md +102 -0
  66. package/lib/mcp-skill/references/operating-model.md +128 -0
  67. package/lib/mcp-skill/references/playwright-cli-setup.md +105 -0
  68. package/lib/mcp-skill/references/playwright-probes.md +205 -0
  69. package/lib/mcp-skill/references/procedures.md +545 -0
  70. package/lib/mcp-skill/references/recipes.txt +114 -0
  71. package/lib/mcp-skill/references/stability-render.md +428 -0
  72. package/lib/mcp-skill/references/test-mode-playbook.md +194 -0
  73. package/lib/mcp-skill/references/troubleshooting.md +59 -0
  74. package/lib/mcp-skill/scripts/avalanche-analyze.cjs +495 -0
  75. package/lib/mcp-skill/scripts/build-avalanche-command.js +34 -0
  76. package/lib/mcp-skill/scripts/build-fiber-state-command.js +19 -0
  77. package/lib/mcp-skill/scripts/chippy-helper-server.js +1768 -0
  78. package/lib/mcp-skill/scripts/chippy-mcp.cjs +273 -0
  79. package/lib/mcp-skill/scripts/chippy.sh +1252 -0
  80. package/lib/mcp-skill/scripts/format-probe-result.js +34 -0
  81. package/lib/mcp-skill/scripts/helper-access.js +158 -0
  82. package/lib/mcp-skill/scripts/helper-proxy.js +87 -0
  83. package/lib/mcp-skill/scripts/page-socket-trust.js +81 -0
  84. package/lib/mcp-skill/scripts/probe-annotation-channel.js +116 -0
  85. package/lib/mcp-skill/scripts/search-recipes.js +248 -0
  86. package/lib/mcp-skill/snippets/README.md +89 -0
  87. package/lib/mcp-skill/snippets/count-context-consumers.js +40 -0
  88. package/lib/mcp-skill/snippets/find-context-providers.js +38 -0
  89. package/lib/mcp-skill/snippets/find-legacy-context.js +47 -0
  90. package/lib/mcp-skill/snippets/find-prop.js +83 -0
  91. package/lib/mcp-skill/snippets/hook-metrics-sample.js +69 -0
  92. package/lib/mcp-skill/snippets/hook-metrics.js +60 -0
  93. package/lib/mcp-skill/snippets/hook-tracker.js +198 -0
  94. package/lib/mcp-skill/snippets/stability-nudge.js +101 -0
  95. package/lib/package.json +1 -0
  96. package/lib/plugin.js +119 -0
  97. package/lib/plugin.js.map +7 -0
  98. package/lib/storybook/avalanche-panel-model.js +79 -0
  99. package/lib/storybook/brand.jsx +6 -0
  100. package/lib/storybook/connection-fallback.jsx +47 -0
  101. package/lib/storybook/constants.js +15 -0
  102. package/lib/storybook/context-card.jsx +171 -0
  103. package/lib/storybook/manager-owner.js +42 -0
  104. package/lib/storybook/manager.jsx +425 -0
  105. package/lib/storybook/preset.js +7 -0
  106. package/lib/storybook/preview.js +298 -0
  107. package/lib/storybook/probe-annotation-editor.jsx +52 -0
  108. package/lib/storybook/probe-intent.js +23 -0
  109. package/lib/storybook/setup.js +104 -0
  110. package/lib/types/avalanche/commit-metrics-adapter.d.ts +76 -0
  111. package/lib/types/avalanche/commit-stats-indices.d.ts +41 -0
  112. package/lib/types/avalanche/commit-surface-ownership.d.ts +9 -0
  113. package/lib/types/avalanche/detector.d.ts +183 -0
  114. package/lib/types/avalanche/interaction-capture.d.ts +25 -0
  115. package/lib/types/avalanche/internals.d.ts +189 -0
  116. package/lib/types/avalanche/report-serializer.d.ts +283 -0
  117. package/lib/types/avalanche/reporter.d.ts +610 -0
  118. package/lib/types/chippy-bridge/fiber-source.d.ts +48 -0
  119. package/lib/types/chippy-bridge/react-renderer.d.ts +74 -0
  120. package/lib/types/chippy-bridge/recorded-source.d.ts +44 -0
  121. package/lib/types/chippy-bridge/source-map.d.ts +30 -0
  122. package/lib/types/debug-render/core.d.ts +78 -0
  123. package/lib/types/layout-lab/fiber-boundaries.d.ts +51 -0
  124. package/lib/types/layout-lab/fiber-walk.d.ts +131 -0
  125. package/lib/types/layout-lab/marker-context.d.ts +94 -0
  126. package/lib/types/package/avalanche.d.ts +6 -0
  127. package/lib/types/package/browser.d.ts +10 -0
  128. package/lib/types/package/plugin.d.ts +15 -0
  129. package/lib/types/plugins/atl/commit-metrics-adapter.d.ts +65 -0
  130. package/lib/types/plugins/atl/metrics-slot.d.ts +37 -0
  131. package/lib/types/plugins/index.d.ts +49 -0
  132. package/lib/types/plugins/jira/context-provider.d.ts +36 -0
  133. package/package.json +106 -4
@@ -0,0 +1,325 @@
1
+ /* eslint-disable */
2
+ // Chippy background service worker.
3
+ // Ensures the content script + bundle scripts are injected into tabs even when
4
+ // Chrome's declarative content_scripts injection fails (e.g. after extension
5
+ // reload, on service-worker-served pages, or on fast-loading pages).
6
+ //
7
+ // Injection is split into EARLY_STARTUP_SCRIPTS and LATE_BUNDLE_SCRIPTS. The
8
+ // first group must beat react-dom evaluation; the second group can wait until
9
+ // the page owner is known and only installs the bridge/probe UI.
10
+
11
+ const DEFAULT_HELPER = 'http://127.0.0.1:7823';
12
+ // A packed build (`chippy pack`) prepends globalThis.__CHIPPY_BUILD to this file
13
+ // with the plugins it enables until the user saves their own selection.
14
+ const BUILD_DEFAULT_PLUGINS = Array.isArray(globalThis.__CHIPPY_BUILD?.defaultPlugins) ? globalThis.__CHIPPY_BUILD.defaultPlugins : [];
15
+ const CONTENT_SCRIPTS = ['panel-settings.js', 'content-script.js'];
16
+ // Registers early startup for the navigation after an armed reload.
17
+ importScripts('armed-reload-startup.js');
18
+ const EXTENSION_INTEGRATION = Object.freeze({
19
+ kind: 'extension',
20
+ who: 'Chippy browser extension',
21
+ source: '.agents/skills/chippy/extension/background.js',
22
+ reinjectsOnReload: true,
23
+ reloadAction: 'No user action required; the enabled extension re-injects Chippy after navigation.',
24
+ });
25
+ // IIFE bundle of Chippy's avalanche adapter. Loads in MAIN world before
26
+ // the document parses so __AVALANCHE__ is published before any React mounts.
27
+ const EARLY_STARTUP_SCRIPTS = [
28
+ // MUST be first. Installs the React DevTools hook shim BEFORE react-dom or
29
+ // any page-React module evaluates. Without it, react-dom does not register
30
+ // its renderer into hook.renderers, and Avalanche debug-render cannot find
31
+ // ReactCurrentDispatcher to capture hook call stacks. That collapses
32
+ // debugHook() to a flat fallback (no custom-hook folders, no callerLoc).
33
+ 'devtools-hook-shim.js',
34
+ 'avalanche.iife.js',
35
+ 'early-bootstrap.js',
36
+ ];
37
+
38
+ const LATE_BUNDLE_SCRIPTS = [
39
+ 'inspect-react.js',
40
+ 'panel-dom.js',
41
+ 'layout-lab.js',
42
+ 'resolve-probes.js',
43
+ 'chippy-page-bridge.js',
44
+ ];
45
+ // Late bundle is bridge/probes only. Avalanche lifecycle is owned by
46
+ // EARLY_STARTUP_SCRIPTS; re-injecting avalanche code here causes duplicate
47
+ // globals and races bootstrap ordering.
48
+
49
+ // Inject content script into a tab, ignoring errors (already injected, restricted page, etc.)
50
+ async function ensureContentScript(tabId) {
51
+ try {
52
+ await chrome.scripting.executeScript({
53
+ target: { tabId },
54
+ files: CONTENT_SCRIPTS,
55
+ });
56
+ } catch {
57
+ // Ignore — tab may be restricted (chrome://, edge://, etc.)
58
+ }
59
+ }
60
+
61
+
62
+ async function getStartupConfig(origin, helperBaseUrl, clientId, plugins) {
63
+ const helperBase = (helperBaseUrl || DEFAULT_HELPER).replace(/\/+$/, '');
64
+ const config = {
65
+ enabled: true,
66
+ helperBaseUrl: helperBaseUrl || DEFAULT_HELPER,
67
+ avalanche: null,
68
+ // Plugins the user enabled in the panel settings, or the build's defaults.
69
+ plugins: Array.isArray(plugins) ? plugins : [],
70
+ contextUiEnabled: true,
71
+ integration: EXTENSION_INTEGRATION,
72
+ };
73
+ try {
74
+ if (clientId) {
75
+ const rc = await fetch(`${helperBase}/__chippy/api/avalanche-arm?clientId=${encodeURIComponent(clientId)}`, { signal: AbortSignal.timeout(1000) });
76
+ if (rc.ok) {
77
+ const data = await rc.json();
78
+ if (data.armed && data.config) {
79
+ config.avalanche = data.config;
80
+ return config;
81
+ }
82
+ }
83
+ }
84
+ const ro = await fetch(`${helperBase}/__chippy/api/avalanche-arm?origin=${encodeURIComponent(origin)}`, { signal: AbortSignal.timeout(1000) });
85
+ if (ro.ok) {
86
+ const data = await ro.json();
87
+ if (data.armed && data.config) config.avalanche = data.config;
88
+ }
89
+ } catch {}
90
+ return config;
91
+ }
92
+
93
+ async function lookupClientIdForTab(tabId, helperBaseUrl) {
94
+ const helperBase = (helperBaseUrl || DEFAULT_HELPER).replace(/\/+$/, '');
95
+ try {
96
+ const r = await fetch(`${helperBase}/__chippy/api/pages`, { signal: AbortSignal.timeout(1000) });
97
+ if (!r.ok) return null;
98
+ const data = await r.json();
99
+ const pages = Array.isArray(data?.pages) ? data.pages : [];
100
+ const hit = pages.find((p) => Number(p?.tabId) === Number(tabId));
101
+ return hit?.clientId || null;
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ // Extension is the DEFAULT/fallback integration. Any explicitly user-driven
108
+ // owner (Storybook addon, direct import, etc.) outranks it: when a live owner
109
+ // with integration.kind !== 'extension' already holds window.__CHIPPY_OWNER__,
110
+ // the extension stands down instead of stomping the lock. Runs in the page
111
+ // (MAIN) world because __CHIPPY_OWNER__ only exists there.
112
+ async function detectLiveForeignOwner(tabId) {
113
+ try {
114
+ const results = await chrome.scripting.executeScript({
115
+ target: { tabId },
116
+ world: 'MAIN',
117
+ injectImmediately: true,
118
+ func: () => {
119
+ const owner = window.__CHIPPY_OWNER__;
120
+ if (!owner || !owner.integration) return null;
121
+ // Extension never yields to another extension owner (that is itself).
122
+ if (owner.integration.kind === 'extension') return null;
123
+ // Respect the owner's own liveness contract; a stale owner is ignored.
124
+ const staleAfterMs = owner.staleAfterMs;
125
+ const heartbeatAt = owner.heartbeatAt || 0;
126
+ if (staleAfterMs && Date.now() - heartbeatAt > staleAfterMs) return null;
127
+ return {
128
+ ownerId: owner.ownerId || null,
129
+ kind: owner.integration.kind || null,
130
+ who: owner.integration.who || null,
131
+ };
132
+ },
133
+ });
134
+ return results?.[0]?.result || null;
135
+ } catch {
136
+ // If we cannot probe the page world, do not block injection.
137
+ return null;
138
+ }
139
+ }
140
+
141
+ async function injectEarlyStartup(tabId, startupConfig) {
142
+ // Timing-critical path: do NOT run an owner-detection preflight before these
143
+ // startup scripts. reload-with-avalanche relies on devtools-hook-shim +
144
+ // avalanche.iife.js landing as early as possible on navigation. Owner/yield
145
+ // checks happen later in ensureBundleScripts before bridge/probes activation.
146
+ try {
147
+ await chrome.scripting.executeScript({
148
+ target: { tabId },
149
+ world: 'MAIN',
150
+ injectImmediately: true,
151
+ func: (cfg, integration) => {
152
+ window.__CHIPPY_STARTUP_CONFIG = cfg;
153
+ // Stamp the extension as the page owner so its identity is stable no
154
+ // matter which config-write path (early startup, bundle, or content
155
+ // script apply-config) wins. getIntegrationIdentity() falls back to
156
+ // __CHIPPY_OWNER__.integration, so this guarantees integration is never
157
+ // null on an extension-owned page. We reach here only when no live
158
+ // foreign owner exists (checked above), so we never overwrite the
159
+ // Storybook addon / import owner.
160
+ const existing = window.__CHIPPY_OWNER__;
161
+ const existingLive =
162
+ existing &&
163
+ existing.integration &&
164
+ existing.integration.kind !== 'extension' &&
165
+ (!existing.staleAfterMs || Date.now() - (existing.heartbeatAt || 0) <= existing.staleAfterMs);
166
+ if (!existingLive) {
167
+ window.__CHIPPY_OWNER__ = {
168
+ ownerId: 'chippy-extension',
169
+ integration,
170
+ heartbeatAt: Date.now(),
171
+ staleAfterMs: 15000,
172
+ };
173
+ }
174
+ },
175
+ args: [startupConfig, EXTENSION_INTEGRATION],
176
+ });
177
+ } catch (e) {
178
+ return { ok: false, step: 'startup-config', error: e.message };
179
+ }
180
+
181
+ for (const file of EARLY_STARTUP_SCRIPTS) {
182
+ try {
183
+ await chrome.scripting.executeScript({
184
+ target: { tabId },
185
+ files: [file],
186
+ world: 'MAIN',
187
+ injectImmediately: true,
188
+ });
189
+ } catch (e) {
190
+ return { ok: false, step: `startup:${file}`, error: e.message };
191
+ }
192
+ }
193
+
194
+ return { ok: true, detail: `${EARLY_STARTUP_SCRIPTS.length} early scripts injected` };
195
+ }
196
+
197
+ // Write Chippy config into the page world and inject non-startup bundle scripts.
198
+ async function ensureBundleScripts(tabId, helperBaseUrl, options = {}) {
199
+ const foreignOwner = await detectLiveForeignOwner(tabId);
200
+ if (foreignOwner) {
201
+ return { ok: true, yielded: true, detail: `extension yielded to ${foreignOwner.who || foreignOwner.kind}` };
202
+ }
203
+
204
+ const config = {
205
+ helperBaseUrl: helperBaseUrl || DEFAULT_HELPER,
206
+ activated: true,
207
+ contextUiEnabled: true,
208
+ integration: EXTENSION_INTEGRATION,
209
+ };
210
+
211
+ if (options.avalanche) config.avalanche = options.avalanche;
212
+
213
+ try {
214
+ await chrome.scripting.executeScript({
215
+ target: { tabId },
216
+ func: (nextConfig) => {
217
+ window.__CHIPPY = window.__CHIPPY || {};
218
+ window.__CHIPPY.config = {
219
+ ...(window.__CHIPPY.config || {}),
220
+ ...nextConfig,
221
+ };
222
+ try {
223
+ window.__CHIPPY?.pageBridge?.applyConfig?.(window.__CHIPPY.config);
224
+ } catch {}
225
+ },
226
+ args: [config],
227
+ world: 'MAIN',
228
+ });
229
+ } catch (e) {
230
+ return { ok: false, step: 'set-config', error: e.message };
231
+ }
232
+
233
+ for (const file of LATE_BUNDLE_SCRIPTS) {
234
+ try {
235
+ await chrome.scripting.executeScript({
236
+ target: { tabId },
237
+ files: [file],
238
+ world: 'MAIN',
239
+ });
240
+ } catch (e) {
241
+ return { ok: false, step: `inject:${file}`, error: e.message };
242
+ }
243
+ }
244
+
245
+ return { ok: true, detail: `${LATE_BUNDLE_SCRIPTS.length} late scripts injected via background` };
246
+ }
247
+
248
+ // Full injection: content script + bundle
249
+ async function ensureAll(tabId, helperBaseUrl, options = {}) {
250
+ await ensureContentScript(tabId);
251
+ return ensureBundleScripts(tabId, helperBaseUrl, options);
252
+ }
253
+
254
+ // When popup/content-script ask us to ensure injection
255
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
256
+ const messageType = message?.type;
257
+
258
+ if (messageType === 'chippy-ensure-content-script' && message.tabId) {
259
+ ensureContentScript(message.tabId).then(() => sendResponse({ ok: true })).catch((e) => {
260
+ sendResponse({ ok: false, step: 'ensure-content-script', error: e.message });
261
+ });
262
+ return true;
263
+ }
264
+
265
+ if (messageType === 'chippy-inject-bundle') {
266
+ const tabId = message.tabId || sender.tab?.id;
267
+ if (!tabId) {
268
+ sendResponse({ ok: false, step: 'resolve-tab', error: 'No tabId available for bundle injection' });
269
+ return false;
270
+ }
271
+
272
+ ensureBundleScripts(tabId, message.helperBaseUrl || DEFAULT_HELPER, message.options || {})
273
+ .then((result) => sendResponse(result))
274
+ .catch((e) => sendResponse({ ok: false, step: 'inject-bundle', error: e.message }));
275
+ return true;
276
+ }
277
+
278
+ return false;
279
+ });
280
+
281
+ // Early startup-critical injection on navigation commit.
282
+ chrome.webNavigation.onCommitted.addListener(async (details) => {
283
+ if (details.frameId !== 0) return;
284
+ const tabId = details.tabId;
285
+ try {
286
+ const tab = await chrome.tabs.get(tabId);
287
+ if (!tab?.url) return;
288
+ const origin = new URL(tab.url).origin;
289
+ const stored = await chrome.storage.local.get({ helperBaseUrl: DEFAULT_HELPER, enabledOrigins: {}, plugins: BUILD_DEFAULT_PLUGINS });
290
+ if (!stored.enabledOrigins?.[origin]) return;
291
+ const clientId = await lookupClientIdForTab(tabId, stored.helperBaseUrl);
292
+ const startupConfig = await getStartupConfig(origin, stored.helperBaseUrl, clientId, stored.plugins);
293
+ await injectEarlyStartup(tabId, startupConfig);
294
+ if (startupConfig && startupConfig.avalanche && startupConfig.avalanche.autoStart) {
295
+ try {
296
+ await fetch(`${(stored.helperBaseUrl || DEFAULT_HELPER).replace(/\/+$/, '')}/__chippy/api/avalanche-arm`, {
297
+ method: 'POST',
298
+ headers: { 'Content-Type': 'application/json' },
299
+ body: JSON.stringify({ origin, clientId, autoStart: false }),
300
+ signal: AbortSignal.timeout(1000),
301
+ });
302
+ } catch {}
303
+ }
304
+ } catch (e) {
305
+ console.warn(`[chippy-bg] early-inject failed tab=${tabId}: ${e.message}`);
306
+ }
307
+ });
308
+
309
+ // Re-inject non-startup bundle on navigation complete for enabled origins.
310
+ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
311
+ if (changeInfo.status !== 'complete' || !tab.url) return;
312
+
313
+ chrome.storage.local.get({ helperBaseUrl: DEFAULT_HELPER, enabledOrigins: {}, plugins: BUILD_DEFAULT_PLUGINS }, async (stored) => {
314
+ try {
315
+ const origin = new URL(tab.url).origin;
316
+ if (!stored.enabledOrigins?.[origin]) return;
317
+ const clientId = await lookupClientIdForTab(tabId, stored.helperBaseUrl);
318
+ const startupConfig = await getStartupConfig(origin, stored.helperBaseUrl, clientId, stored.plugins);
319
+ const options = startupConfig.avalanche ? { avalanche: startupConfig.avalanche } : {};
320
+ ensureAll(tabId, stored.helperBaseUrl, options).catch((e) => {
321
+ console.warn(`[chippy-bg] late-inject failed for ${origin} in tab ${tabId}: ${e.message}`);
322
+ });
323
+ } catch {}
324
+ });
325
+ });