@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,298 @@
1
+ import { addons } from 'storybook/preview-api';
2
+ import { FORCE_REMOUNT, STORY_CHANGED, STORY_HOT_UPDATED } from 'storybook/internal/core-events';
3
+ import { EVENTS, PREVIEW_MESSAGE_SOURCE } from './constants.js';
4
+ import { OWNER_ID, avalancheArmReady, ownerResult } from './setup.js';
5
+
6
+ import '../extension/inspect-react.js';
7
+ import '../extension/panel-dom.js';
8
+ import '../extension/layout-lab.js';
9
+ import '../extension/resolve-probes.js';
10
+ import avalanche from '../extension/avalanche.es.js';
11
+ import '../extension/chippy-page-bridge.js';
12
+
13
+ if (ownerResult.ok) {
14
+ globalThis.__AVALANCHE__ = globalThis.__AVALANCHE__ || avalanche;
15
+ }
16
+
17
+ function emit(channel, event, payload) {
18
+ try {
19
+ channel.emit(event, payload);
20
+ } catch {
21
+ // Storybook channel can be briefly unavailable during preview reloads.
22
+ }
23
+ }
24
+
25
+ function getBridge() {
26
+ return globalThis.__CHIPPY?.pageBridge || null;
27
+ }
28
+
29
+ function getAvalancheApi() {
30
+ return globalThis.__AVALANCHE__ || avalanche || null;
31
+ }
32
+
33
+ function runAvalanche(action, command = {}) {
34
+ const api = getAvalancheApi();
35
+ const dispatcher = api?.bridge?.evalAvalanche;
36
+ if (typeof dispatcher !== 'function') {
37
+ return { ok: false, available: !!api, action, error: 'avalanche dispatcher unavailable' };
38
+ }
39
+ return dispatcher({ action, ...command }, api, api);
40
+ }
41
+
42
+ let avalancheStoryId = null;
43
+ let avalancheAutoCache = null;
44
+
45
+ function currentStoryId() {
46
+ return globalThis.__STORYBOOK_PREVIEW__?.selectionStore?.selection?.storyId
47
+ || new URLSearchParams(globalThis.location.search || '').get('id')
48
+ || globalThis.__STORYBOOK_STORY_STORE__?.selection?.storyId
49
+ || null;
50
+ }
51
+
52
+ // Storybook remounts only the render whose id matches FORCE_REMOUNT's storyId.
53
+ // Return that id, or null when no rendered story matches.
54
+ function remountTargetId() {
55
+ const storyId = currentStoryId();
56
+ const renders = globalThis.__STORYBOOK_PREVIEW__?.storyRenders;
57
+ if (!Array.isArray(renders)) return storyId;
58
+ return renders.some((render) => render.id === storyId) ? storyId : null;
59
+ }
60
+
61
+ // Install Avalanche with the options of a reload-with-avalanche arm. Returns
62
+ // true when this call installed it.
63
+ function installFromArm(arm) {
64
+ const api = getAvalancheApi();
65
+ if (!ownerResult.ok || !arm?.autoStart || typeof api?.install !== 'function' || api.status?.().installed) return false;
66
+ try {
67
+ api.install({
68
+ detectionMode: arm.options?.detectionMode || 'sources',
69
+ historySize: arm.max || 1000,
70
+ captureFibers: arm.options?.captureFibers !== false,
71
+ });
72
+ } catch (_) {
73
+ return false;
74
+ }
75
+ avalancheStoryId = currentStoryId();
76
+ avalancheAutoCache = null;
77
+ return true;
78
+ }
79
+
80
+ // The arm lookup in setup.js resolves after module evaluation; the global
81
+ // loader below holds the first story render until it has been applied.
82
+ const armApplied = avalancheArmReady.then(installFromArm, () => false);
83
+
84
+ function ensureAvalancheInstalled({ resetForStory = false } = {}) {
85
+ const storyId = currentStoryId();
86
+ const status = runAvalanche('status');
87
+ const installed = status?.status?.installed || status?.result?.status?.installed;
88
+ if (!installed) {
89
+ runAvalanche('install', { options: { detectionMode: 'sources', historySize: 5000, captureFibers: true } });
90
+ avalancheStoryId = storyId;
91
+ avalancheAutoCache = null;
92
+ return;
93
+ }
94
+ if (resetForStory && storyId && storyId !== avalancheStoryId) {
95
+ runAvalanche('clear');
96
+ avalancheStoryId = storyId;
97
+ avalancheAutoCache = null;
98
+ }
99
+ }
100
+
101
+ function refreshAvalancheAuto() {
102
+ ensureAvalancheInstalled({ resetForStory: true });
103
+ avalancheAutoCache = {
104
+ storyId: currentStoryId(),
105
+ status: runAvalanche('status'),
106
+ hotSources: runAvalanche('hot-sources', { limit: 50 }),
107
+ commitList: runAvalanche('commit-list', { top: 1000, by: 'collateral' }),
108
+ };
109
+ return avalancheAutoCache;
110
+ }
111
+
112
+ function statePayload(extra = {}) {
113
+ const bridge = getBridge();
114
+ const mode = globalThis.__CHIPPY_OWNER__?.mode || bridge?.getState?.()?.config?.mode;
115
+ const avalancheAuto = mode === 'avalanche' ? refreshAvalancheAuto() : avalancheAutoCache;
116
+ return {
117
+ ok: !!bridge,
118
+ owner: globalThis.__CHIPPY_OWNER__ || null,
119
+ blocked: globalThis.__CHIPPY_BLOCKED__ || null,
120
+ state: bridge?.getState?.() || null,
121
+ avalancheStatus: avalancheAuto?.status || runAvalanche('status'),
122
+ avalancheAuto,
123
+ ...extra,
124
+ };
125
+ }
126
+
127
+ function activeMode() {
128
+ return globalThis.__CHIPPY_OWNER__?.mode || globalThis.__CHIPPY?.pageBridge?.getState?.()?.config?.mode;
129
+ }
130
+
131
+ function resetAvalancheForBoundary(reason) {
132
+ runAvalanche('clear');
133
+ avalancheStoryId = currentStoryId();
134
+ avalancheAutoCache = null;
135
+ if (activeMode() === 'avalanche') {
136
+ ensureAvalancheInstalled();
137
+ refreshAvalancheAuto();
138
+ publishState(channel, `avalanche-reset:${reason}`);
139
+ }
140
+ }
141
+
142
+ function syncActiveLifecycle() {
143
+ if (activeMode() === 'avalanche') ensureAvalancheInstalled({ resetForStory: true });
144
+ }
145
+
146
+ function publishState(channel, reason = 'state') {
147
+ syncActiveLifecycle();
148
+ emit(channel, EVENTS.STATE, statePayload({ reason }));
149
+ }
150
+
151
+ function executeCommand(command) {
152
+ const bridge = getBridge();
153
+ if (!bridge) return { ok: false, error: 'bridge-not-ready' };
154
+ switch (command?.type) {
155
+ case 'activate':
156
+ bridge.activate?.();
157
+ return { ok: true };
158
+ case 'deactivate':
159
+ bridge.deactivate?.();
160
+ return { ok: true };
161
+ case 'ensure-probes':
162
+ bridge.activate?.();
163
+ bridge.ensureProbes?.();
164
+ return { ok: true };
165
+ case 'clear-probes':
166
+ bridge.clearProbes?.();
167
+ return { ok: true };
168
+ case 'set-probe-annotation': {
169
+ const annotation = bridge.setProbeAnnotation?.(command.markerId, {
170
+ intent: command.intent ?? null,
171
+ comment: command.comment ?? '',
172
+ });
173
+ return annotation
174
+ ? { ok: true, markerId: command.markerId, annotation }
175
+ : { ok: false, error: `probe-not-found:${command.markerId || 'missing'}` };
176
+ }
177
+ case 'avalanche-install': {
178
+ globalThis.__CHIPPY_LOCK__?.heartbeat?.(OWNER_ID, { mode: 'avalanche' });
179
+ bridge.applyConfig?.({ mode: 'avalanche', activated: false });
180
+ ensureAvalancheInstalled({ resetForStory: true });
181
+ return runAvalanche('status');
182
+ }
183
+ case 'avalanche-stop':
184
+ return runAvalanche('uninstall');
185
+ case 'avalanche-clear':
186
+ ensureAvalancheInstalled();
187
+ return runAvalanche('clear');
188
+ case 'avalanche-status':
189
+ ensureAvalancheInstalled();
190
+ return runAvalanche('status');
191
+ case 'avalanche-export':
192
+ ensureAvalancheInstalled();
193
+ return runAvalanche('export');
194
+ case 'avalanche-hot-sources':
195
+ ensureAvalancheInstalled();
196
+ return runAvalanche('hot-sources', { limit: command.limit || 50 });
197
+ case 'avalanche-commit-list':
198
+ ensureAvalancheInstalled();
199
+ return runAvalanche('commit-list', { top: command.top || 1000, by: command.by || 'collateral' });
200
+ case 'avalanche-commit-peek':
201
+ ensureAvalancheInstalled();
202
+ return runAvalanche('commit-peek', {
203
+ commitId: command.commitId,
204
+ include: command.include || ['stats', 'sources', 'timing', 'metrics', 'actions'],
205
+ maxItems: command.maxItems || 20,
206
+ });
207
+ case 'inspect-boundary':
208
+ // Report the selected collateral boundary (owner/error/suspense) + its
209
+ // fiber-hop distance back to the backend. The click already carries the
210
+ // resolved descriptor; the addon acknowledges it as a first-class result
211
+ // so downstream consumers can act on the collateral boundary.
212
+ return {
213
+ ok: true,
214
+ markerId: command.markerId ?? null,
215
+ boundaryKind: command.boundaryKind ?? null,
216
+ boundary: command.boundary ?? null,
217
+ distance: command.distance ?? command.boundary?.distance ?? null,
218
+ };
219
+ default:
220
+ return { ok: false, error: `unknown-command:${command?.type || 'missing'}` };
221
+ }
222
+ }
223
+
224
+ const channel = addons.getChannel();
225
+
226
+ function handleMode(mode) {
227
+ globalThis.__CHIPPY_LOCK__?.heartbeat?.(OWNER_ID, { mode });
228
+ const bridge = getBridge();
229
+ if (bridge?.applyConfig) bridge.applyConfig(mode === 'chippy' ? { mode } : { mode, activated: false });
230
+ if (mode !== 'chippy') {
231
+ bridge?.clearProbes?.();
232
+ }
233
+ if (mode === 'avalanche') ensureAvalancheInstalled({ resetForStory: true });
234
+ publishState(channel, 'mode');
235
+ }
236
+
237
+ function handleCommand(command) {
238
+ const result = executeCommand(command);
239
+ emit(channel, EVENTS.RESULT, { command, result, state: statePayload({ reason: 'command' }) });
240
+ publishState(channel, `command:${command?.type || 'unknown'}`);
241
+ }
242
+
243
+ channel.on(EVENTS.REQUEST_STATE, () => publishState(channel, 'request'));
244
+ channel.on(EVENTS.SET_MODE, handleMode);
245
+ channel.on(EVENTS.COMMAND, handleCommand);
246
+ channel.on(STORY_CHANGED, () => resetAvalancheForBoundary('story-changed'));
247
+ channel.on(FORCE_REMOUNT, () => resetAvalancheForBoundary('force-remount'));
248
+ channel.on(STORY_HOT_UPDATED, () => resetAvalancheForBoundary('story-hot-updated'));
249
+ window.addEventListener('message', (event) => {
250
+ const data = event.data || {};
251
+ if (data.source !== PREVIEW_MESSAGE_SOURCE) return;
252
+ if (data.event === EVENTS.SET_MODE) handleMode(data.payload);
253
+ if (data.event === EVENTS.COMMAND) handleCommand(data.payload);
254
+ });
255
+
256
+ window.addEventListener('chippy-storybook-force-remount', (event) => {
257
+ // Keep Storybook's manager and preview alive. FORCE_REMOUNT replaces only
258
+ // the rendered SUT, unlike reloading the preview iframe document. The event
259
+ // stays unhandled, and the helper reports the remount as failed, when no
260
+ // rendered story can be remounted.
261
+ const storyId = remountTargetId();
262
+ if (!storyId) return;
263
+ // An armed remount installs Avalanche first; the FORCE_REMOUNT listener
264
+ // above then clears history, so the capture starts with the remount.
265
+ installFromArm(event.detail?.avalanche);
266
+ emit(channel, FORCE_REMOUNT, { storyId });
267
+ event.preventDefault();
268
+ });
269
+
270
+ window.addEventListener('chippy-conn-state', () => publishState(channel, 'conn-state'));
271
+ window.addEventListener('layout-lab:update', () => publishState(channel, 'layout-lab'));
272
+
273
+ // A marker starts life after a manager-panel button was focused. Because the
274
+ // shared drag handler prevents pointerdown's default, Storybook would otherwise
275
+ // keep keyboard focus in the manager document and arrow navigation would never
276
+ // reach the preview iframe.
277
+ document.addEventListener('pointerdown', (event) => {
278
+ const marker = event.target?.closest?.('[data-layout-lab-marker]');
279
+ marker?.focus?.({ preventScroll: true });
280
+ }, true);
281
+
282
+ setInterval(() => {
283
+ globalThis.__CHIPPY_LOCK__?.heartbeat?.(OWNER_ID);
284
+ publishState(channel, 'heartbeat');
285
+ }, 5000);
286
+
287
+ publishState(channel, ownerResult.ok ? 'ready' : 'blocked');
288
+
289
+ export const decorators = [
290
+ (Story) => Story(),
291
+ ];
292
+
293
+ export const loaders = [
294
+ async () => {
295
+ await armApplied;
296
+ return {};
297
+ },
298
+ ];
@@ -0,0 +1,52 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { PROBE_INTENTS, normalizeProbeAnnotation } from './probe-intent.js';
3
+
4
+ const INTENT_LABELS = {
5
+ explain: 'Explain',
6
+ investigate: 'Investigate',
7
+ change: 'Change',
8
+ };
9
+
10
+ export function ProbeAnnotationEditor({ markerId, intent, comment, onAttach }) {
11
+ const [draft, setDraft] = useState(() => normalizeProbeAnnotation({ intent, comment }));
12
+
13
+ useEffect(() => {
14
+ setDraft(normalizeProbeAnnotation({ intent, comment }));
15
+ }, [intent, comment]);
16
+
17
+ const changeIntent = (event) => {
18
+ const next = normalizeProbeAnnotation({ ...draft, intent: event.target.value || null });
19
+ setDraft(next);
20
+ onAttach?.(next);
21
+ };
22
+
23
+ const commitComment = (event) => {
24
+ if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent?.isComposing) return;
25
+ event.preventDefault();
26
+ event.stopPropagation();
27
+ onAttach?.(normalizeProbeAnnotation({ ...draft, comment: event.currentTarget.value }));
28
+ };
29
+
30
+ return (
31
+ <div style={{ display: 'grid', gap: 6, marginTop: 8 }}>
32
+ <select
33
+ aria-label={`P${markerId} intent`}
34
+ value={draft.intent || ''}
35
+ onChange={changeIntent}
36
+ style={{ width: '100%', padding: '5px 7px', borderRadius: 6 }}
37
+ >
38
+ <option value="">Choose intent</option>
39
+ {PROBE_INTENTS.map((value) => <option key={value} value={value}>{INTENT_LABELS[value]}</option>)}
40
+ </select>
41
+ <textarea
42
+ aria-label={`P${markerId} comment`}
43
+ maxLength={2000}
44
+ placeholder="What should Chippy do with this probe?"
45
+ value={draft.comment}
46
+ onChange={(event) => setDraft((current) => ({ ...current, comment: event.target.value }))}
47
+ onKeyDown={commitComment}
48
+ style={{ boxSizing: 'border-box', width: '100%', minHeight: 58, resize: 'vertical', padding: '6px 7px', borderRadius: 6 }}
49
+ />
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,23 @@
1
+ // src/layout-lab/probe-intent.ts
2
+ var PROBE_INTENTS = ["explain", "investigate", "change"];
3
+ var MAX_COMMENT_LENGTH = 2e3;
4
+ var INTENT_SET = new Set(PROBE_INTENTS);
5
+ function normalizeProbeAnnotation(value) {
6
+ const input = value && typeof value === "object" ? value : {};
7
+ const rawIntent = typeof input.intent === "string" ? input.intent.trim().toLowerCase() : "";
8
+ return {
9
+ intent: INTENT_SET.has(rawIntent) ? rawIntent : null,
10
+ comment: typeof input.comment === "string" ? input.comment.slice(0, MAX_COMMENT_LENGTH) : ""
11
+ };
12
+ }
13
+ function updateProbeAnnotation(current, patch) {
14
+ return normalizeProbeAnnotation({
15
+ intent: patch.intent === void 0 ? current.intent : patch.intent,
16
+ comment: patch.comment === void 0 ? current.comment : patch.comment
17
+ });
18
+ }
19
+ export {
20
+ PROBE_INTENTS,
21
+ normalizeProbeAnnotation,
22
+ updateProbeAnnotation
23
+ };
@@ -0,0 +1,104 @@
1
+ const OWNER_ID = 'storybook-addon';
2
+ const OWNER_TYPE = 'storybook-addon';
3
+ const HELPER_BASE_URL = 'http://127.0.0.1:7823';
4
+ const STALE_AFTER_MS = 15_000;
5
+ const INTEGRATION = Object.freeze({
6
+ kind: 'storybook',
7
+ who: 'Chippy Storybook addon',
8
+ source: '.storybook/chippy-addon/setup.js',
9
+ reinjectsOnReload: true,
10
+ reloadAction: 'Chippy remounts the current SUT through Storybook FORCE_REMOUNT without reloading the preview iframe.',
11
+ });
12
+
13
+ async function lookupAvalancheArm() {
14
+ try {
15
+ const url = `${HELPER_BASE_URL.replace(/\/+$/, '')}/__chippy/api/avalanche-arm?origin=${encodeURIComponent(location.href)}`;
16
+ const response = await fetch(url, { signal: AbortSignal.timeout(1000) });
17
+ if (!response.ok) return null;
18
+ const data = await response.json();
19
+ return data && data.armed && data.config ? data.config : null;
20
+ } catch (_) {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function acquireOwner(mode = 'chippy') {
26
+ const now = Date.now();
27
+ const current = globalThis.__CHIPPY_OWNER__ || null;
28
+ if (
29
+ !current ||
30
+ current.ownerId === OWNER_ID ||
31
+ (current.heartbeatAt && now - current.heartbeatAt > STALE_AFTER_MS)
32
+ ) {
33
+ const owner = {
34
+ ...(current?.ownerId === OWNER_ID ? current : {}),
35
+ ownerId: OWNER_ID,
36
+ ownerType: OWNER_TYPE,
37
+ transport: 'storybook-static',
38
+ ui: 'storybook-panel',
39
+ mode,
40
+ helperBaseUrl: HELPER_BASE_URL,
41
+ integration: INTEGRATION,
42
+ staleAfterMs: STALE_AFTER_MS,
43
+ startedAt: current?.ownerId === OWNER_ID ? current.startedAt || now : now,
44
+ heartbeatAt: now,
45
+ };
46
+ globalThis.__CHIPPY_OWNER__ = owner;
47
+ globalThis.__CHIPPY_LOCK__ = globalThis.__CHIPPY_LOCK__ || {};
48
+ globalThis.__CHIPPY_LOCK__.get = () => globalThis.__CHIPPY_OWNER__ || null;
49
+ globalThis.__CHIPPY_LOCK__.heartbeat = (ownerId, patch = {}) => {
50
+ if (globalThis.__CHIPPY_OWNER__?.ownerId !== ownerId) {
51
+ return { ok: false, reason: 'not-owner', owner: globalThis.__CHIPPY_OWNER__ || null };
52
+ }
53
+ globalThis.__CHIPPY_OWNER__ = { ...globalThis.__CHIPPY_OWNER__, ...patch, heartbeatAt: Date.now() };
54
+ return { ok: true, owner: globalThis.__CHIPPY_OWNER__ };
55
+ };
56
+ globalThis.__CHIPPY_LOCK__.release = (ownerId) => {
57
+ if (globalThis.__CHIPPY_OWNER__?.ownerId !== ownerId) {
58
+ return { ok: false, reason: 'not-owner', owner: globalThis.__CHIPPY_OWNER__ || null };
59
+ }
60
+ const owner = globalThis.__CHIPPY_OWNER__;
61
+ delete globalThis.__CHIPPY_OWNER__;
62
+ return { ok: true, owner };
63
+ };
64
+ return { ok: true, owner, reused: current?.ownerId === OWNER_ID, replacedStale: !!(current && current.ownerId !== OWNER_ID) };
65
+ }
66
+ return { ok: false, reason: 'owned-by-other-chippy', owner: current };
67
+ }
68
+
69
+ const ownerResult = acquireOwner('chippy');
70
+ globalThis.__CHIPPY_OWNER_RESULT__ = ownerResult;
71
+ if (!ownerResult.ok) {
72
+ globalThis.__CHIPPY_BLOCKED__ = ownerResult;
73
+ } else {
74
+ globalThis.__CHIPPY_STARTUP_CONFIG = {
75
+ ...(globalThis.__CHIPPY_STARTUP_CONFIG || {}),
76
+ helperBaseUrl: HELPER_BASE_URL,
77
+ activated: false,
78
+ ui: 'storybook-panel',
79
+ ownerType: OWNER_TYPE,
80
+ ownerId: OWNER_ID,
81
+ transport: 'storybook-static',
82
+ mode: 'chippy',
83
+ integration: INTEGRATION,
84
+ avalanche: null,
85
+ staleAfterMs: STALE_AFTER_MS,
86
+ };
87
+ globalThis.__CHIPPY = globalThis.__CHIPPY || {};
88
+ globalThis.__CHIPPY.config = { ...(globalThis.__CHIPPY.config || {}), ...globalThis.__CHIPPY_STARTUP_CONFIG };
89
+ }
90
+
91
+ // The arm lookup must not block module evaluation: top-level await fails
92
+ // `storybook build` under Vite's default browser targets. preview.js waits on
93
+ // this promise in a global loader, so the first story renders after the arm is
94
+ // known and its mount is captured.
95
+ function applyAvalancheArm(arm) {
96
+ if (!arm || !ownerResult.ok) return null;
97
+ globalThis.__CHIPPY_STARTUP_CONFIG = { ...(globalThis.__CHIPPY_STARTUP_CONFIG || {}), avalanche: arm };
98
+ globalThis.__CHIPPY.config = { ...(globalThis.__CHIPPY.config || {}), avalanche: arm };
99
+ return arm;
100
+ }
101
+
102
+ const avalancheArmReady = ownerResult.ok ? lookupAvalancheArm().then(applyAvalancheArm) : Promise.resolve(null);
103
+
104
+ export { HELPER_BASE_URL, INTEGRATION, OWNER_ID, OWNER_TYPE, STALE_AFTER_MS, avalancheArmReady, ownerResult };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Commit metrics adapter: an optional per-commit counter source that sits next
3
+ * to the DevTools fiber walk.
4
+ *
5
+ * The detector always captures commits through the DevTools hook. Some hosts
6
+ * also run an instrumented react-dom that exposes render counters and calls a
7
+ * commit callback of its own. An adapter wraps one such host surface: it seeds
8
+ * and protects the surface, snapshots its counters, turns two snapshots into a
9
+ * per-commit delta plus a restart count, and wires the host's commit callback
10
+ * to the detector. Without an adapter the detector runs in `devtools` mode and
11
+ * reports restarts as unavailable.
12
+ *
13
+ * One adapter is active at a time. Product adapters live in `src/plugins/`.
14
+ */
15
+ import type { Fiber } from './internals.js';
16
+ export type CommitMetricsRoot = {
17
+ current: Fiber;
18
+ memoizedUpdaters?: Set<Fiber>;
19
+ };
20
+ /** Raw counters read from the host surface at one point in time. */
21
+ export type CommitMetricsSnapshot = Record<string, unknown>;
22
+ /** Counter deltas for one commit, keyed by counter name. */
23
+ export type CommitMetricsDelta = {
24
+ [counter: string]: number | undefined;
25
+ };
26
+ export type CommitMetricsMeasurement = {
27
+ delta: CommitMetricsDelta;
28
+ /** Render passes React performed but did not commit. Never negative. */
29
+ restarts: number;
30
+ };
31
+ export interface CommitMetricsAdapter {
32
+ /** Stable id; the detector reports `integrationMode: 'adapter:<id>'`. */
33
+ readonly id: string;
34
+ /**
35
+ * Stack frame names the host surface adds to every render. Hook ancestry
36
+ * hides them like other renderer internals.
37
+ */
38
+ readonly frameFilters?: readonly string[];
39
+ /**
40
+ * Setup that must run before React evaluates, such as installing an accessor
41
+ * that survives host cleanup. Called by the extension's early bootstrap
42
+ * through `__AVALANCHE__.runPrePageHooks(config)`.
43
+ */
44
+ prePage?(config: unknown): void;
45
+ /** Seed the host surface before a detector installs, unless the host owns it. */
46
+ prepare?(): void;
47
+ /** Whether the host surface is present right now. */
48
+ isAvailable(): boolean;
49
+ snapshot(): CommitMetricsSnapshot | null;
50
+ measure(before: CommitMetricsSnapshot, after: CommitMetricsSnapshot, committedComponents: number, commitId: number): CommitMetricsMeasurement;
51
+ /**
52
+ * Route the host's commit callback to `onCommit`. Returns false when the
53
+ * surface is absent. A callback is proof that the host surface is live.
54
+ */
55
+ attach(onCommit: (root: CommitMetricsRoot) => void): boolean;
56
+ /** True while the callback from attach() is still wired into the live surface. */
57
+ isAttached(): boolean;
58
+ /** Re-seed and re-wire after the host wiped or replaced its surface. */
59
+ reattach(): void;
60
+ /** Remove the callback from attach(), only if it is still ours. */
61
+ detach(): void;
62
+ }
63
+ /**
64
+ * Make `adapter` the active commit metrics adapter for detectors installed
65
+ * afterwards. Replaces any earlier adapter. Returns an unregister function.
66
+ */
67
+ export declare function registerCommitMetricsAdapter(adapter: CommitMetricsAdapter): () => void;
68
+ export declare function getCommitMetricsAdapter(): CommitMetricsAdapter | null;
69
+ /**
70
+ * Run the active adapter's pre-page hook. Returns the adapter id when a hook
71
+ * ran, or null. A failing hook is swallowed: the detector falls back to
72
+ * DevTools capture.
73
+ */
74
+ export declare function runPrePageHooks(config?: unknown): string | null;
75
+ export type IntegrationMode = `adapter:${string}` | 'devtools' | 'none';
76
+ export declare function adapterIdFromMode(mode: IntegrationMode | string | null | undefined): string | null;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Named constants for CommitStats tuple indices.
3
+ *
4
+ * CommitStats is a fixed 10-position tuple per commit. This module provides
5
+ * human-readable names for each index to replace magic numbers in code.
6
+ *
7
+ * Tuple structure:
8
+ * [0] collateral — total updates caused by this source (collateral damage)
9
+ * [1] sources — number of distinct sources that fired in this commit
10
+ * [2] restarts — render count from ATL render-counter delta (or 0 if not available)
11
+ * [3] performedWork — components with PerformedWork flag set
12
+ * [4] effects — effect count
13
+ * [5] memos — memo count
14
+ * [6] callbacks — callback count
15
+ * [7] contextUpdates — context update count
16
+ * [8] unmountedComponents — number of unmounted components
17
+ * [9] unmountedSubtrees — number of unmounted subtrees
18
+ *
19
+ * This positional layout is shared by the detector, serializer, bridge, and
20
+ * offline readers. Changing an index is a wire-format change even though the
21
+ * tuple remains valid TypeScript, so keep the names and length together.
22
+ */
23
+ export declare const CS_COLLATERAL = 0;
24
+ export declare const CS_SOURCES = 1;
25
+ export declare const CS_RESTARTS = 2;
26
+ export declare const CS_PERFORMED_WORK = 3;
27
+ export declare const CS_EFFECTS = 4;
28
+ export declare const CS_MEMOS = 5;
29
+ export declare const CS_CALLBACKS = 6;
30
+ export declare const CS_CONTEXT_UPDATES = 7;
31
+ export declare const CS_UNMOUNTED_COMPONENTS = 8;
32
+ export declare const CS_UNMOUNTED_SUBTREES = 9;
33
+ /**
34
+ * Total length of the CommitStats tuple.
35
+ * Used for validation and bounds checking.
36
+ */
37
+ export declare const COMMIT_STATS_LENGTH = 10;
38
+ /**
39
+ * Array of all commitStats indices for iteration/validation.
40
+ */
41
+ export declare const ALL_COMMIT_STATS_INDICES: readonly [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
@@ -0,0 +1,9 @@
1
+ export interface CommitRoot {
2
+ current: object;
3
+ }
4
+ export declare function createCommitSurfaceOwnership<T>(): {
5
+ readonly adapterObserved: boolean;
6
+ recordDevtools(root: CommitRoot, capture: T): void;
7
+ observeAdapter(root: CommitRoot): T | null;
8
+ reset(): void;
9
+ };