@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.
- package/LICENSE +201 -0
- package/README.md +276 -2
- package/bin/chippy.mjs +92 -0
- package/bin/pack.mjs +204 -0
- package/bin/skill-dir.mjs +15 -0
- package/lib/avalanche.js +2601 -0
- package/lib/avalanche.js.map +7 -0
- package/lib/bootstrap.d.ts +1 -0
- package/lib/bootstrap.js +70 -0
- package/lib/bootstrap.js.map +7 -0
- package/lib/browser.js +15 -0
- package/lib/browser.js.map +7 -0
- package/lib/extension/avalanche.es.js +5535 -0
- package/lib/extension/avalanche.es.js.map +1 -0
- package/lib/extension/chippy-page-bridge.js +1953 -0
- package/lib/extension/inspect-react.js +216 -0
- package/lib/extension/layout-lab.js +1453 -0
- package/lib/extension/panel-dom.js +90 -0
- package/lib/extension/resolve-probes.js +342 -0
- package/lib/mcp-skill/LICENSE +201 -0
- package/lib/mcp-skill/README.md +148 -0
- package/lib/mcp-skill/SKILL.md +335 -0
- package/lib/mcp-skill/browser-scripts/playwright-probes.js +368 -0
- package/lib/mcp-skill/browser-scripts/playwright-session.js +114 -0
- package/lib/mcp-skill/extension/armed-reload-startup.js +83 -0
- package/lib/mcp-skill/extension/avalanche.es.js +5535 -0
- package/lib/mcp-skill/extension/avalanche.es.js.map +1 -0
- package/lib/mcp-skill/extension/avalanche.iife.js +5536 -0
- package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -0
- package/lib/mcp-skill/extension/background.js +325 -0
- package/lib/mcp-skill/extension/chippy-page-bridge.js +1953 -0
- package/lib/mcp-skill/extension/content-script.js +190 -0
- package/lib/mcp-skill/extension/devtools-hook-shim.js +85 -0
- package/lib/mcp-skill/extension/early-bootstrap.js +49 -0
- package/lib/mcp-skill/extension/icon-128.png +0 -0
- package/lib/mcp-skill/extension/icon-16.png +0 -0
- package/lib/mcp-skill/extension/icon-32.png +0 -0
- package/lib/mcp-skill/extension/icon-48.png +0 -0
- package/lib/mcp-skill/extension/inspect-react.js +216 -0
- package/lib/mcp-skill/extension/layout-lab.js +1453 -0
- package/lib/mcp-skill/extension/logo.svg +14 -0
- package/lib/mcp-skill/extension/manifest.json +69 -0
- package/lib/mcp-skill/extension/panel-dom.js +90 -0
- package/lib/mcp-skill/extension/panel-settings.js +173 -0
- package/lib/mcp-skill/extension/popup.html +41 -0
- package/lib/mcp-skill/extension/popup.js +80 -0
- package/lib/mcp-skill/extension/probe-descriptor.js +241 -0
- package/lib/mcp-skill/extension/resolve-probes.js +342 -0
- package/lib/mcp-skill/extension/startup-handoff.js +30 -0
- package/lib/mcp-skill/package.json +1 -0
- package/lib/mcp-skill/references/README.md +56 -0
- package/lib/mcp-skill/references/active-stimulus.md +85 -0
- package/lib/mcp-skill/references/avalanche-detector.md +731 -0
- package/lib/mcp-skill/references/bdd-router.md +310 -0
- package/lib/mcp-skill/references/browser-runtime-api.md +193 -0
- package/lib/mcp-skill/references/command-output-formats.md +1085 -0
- package/lib/mcp-skill/references/command-surface.md +183 -0
- package/lib/mcp-skill/references/cynefin.md +99 -0
- package/lib/mcp-skill/references/debug-hook.md +161 -0
- package/lib/mcp-skill/references/fixing-performance.md +490 -0
- package/lib/mcp-skill/references/hook-metrics.md +199 -0
- package/lib/mcp-skill/references/hooks-inspection.md +61 -0
- package/lib/mcp-skill/references/jira-taxonomy.md +81 -0
- package/lib/mcp-skill/references/mcp.md +122 -0
- package/lib/mcp-skill/references/mode-a-injection.md +102 -0
- package/lib/mcp-skill/references/operating-model.md +128 -0
- package/lib/mcp-skill/references/playwright-cli-setup.md +105 -0
- package/lib/mcp-skill/references/playwright-probes.md +205 -0
- package/lib/mcp-skill/references/procedures.md +545 -0
- package/lib/mcp-skill/references/recipes.txt +114 -0
- package/lib/mcp-skill/references/stability-render.md +428 -0
- package/lib/mcp-skill/references/test-mode-playbook.md +194 -0
- package/lib/mcp-skill/references/troubleshooting.md +59 -0
- package/lib/mcp-skill/scripts/avalanche-analyze.cjs +495 -0
- package/lib/mcp-skill/scripts/build-avalanche-command.js +34 -0
- package/lib/mcp-skill/scripts/build-fiber-state-command.js +19 -0
- package/lib/mcp-skill/scripts/chippy-helper-server.js +1768 -0
- package/lib/mcp-skill/scripts/chippy-mcp.cjs +273 -0
- package/lib/mcp-skill/scripts/chippy.sh +1252 -0
- package/lib/mcp-skill/scripts/format-probe-result.js +34 -0
- package/lib/mcp-skill/scripts/helper-access.js +158 -0
- package/lib/mcp-skill/scripts/helper-proxy.js +87 -0
- package/lib/mcp-skill/scripts/page-socket-trust.js +81 -0
- package/lib/mcp-skill/scripts/probe-annotation-channel.js +116 -0
- package/lib/mcp-skill/scripts/search-recipes.js +248 -0
- package/lib/mcp-skill/snippets/README.md +89 -0
- package/lib/mcp-skill/snippets/count-context-consumers.js +40 -0
- package/lib/mcp-skill/snippets/find-context-providers.js +38 -0
- package/lib/mcp-skill/snippets/find-legacy-context.js +47 -0
- package/lib/mcp-skill/snippets/find-prop.js +83 -0
- package/lib/mcp-skill/snippets/hook-metrics-sample.js +69 -0
- package/lib/mcp-skill/snippets/hook-metrics.js +60 -0
- package/lib/mcp-skill/snippets/hook-tracker.js +198 -0
- package/lib/mcp-skill/snippets/stability-nudge.js +101 -0
- package/lib/package.json +1 -0
- package/lib/plugin.js +119 -0
- package/lib/plugin.js.map +7 -0
- package/lib/storybook/avalanche-panel-model.js +79 -0
- package/lib/storybook/brand.jsx +6 -0
- package/lib/storybook/connection-fallback.jsx +47 -0
- package/lib/storybook/constants.js +15 -0
- package/lib/storybook/context-card.jsx +171 -0
- package/lib/storybook/manager-owner.js +42 -0
- package/lib/storybook/manager.jsx +425 -0
- package/lib/storybook/preset.js +7 -0
- package/lib/storybook/preview.js +298 -0
- package/lib/storybook/probe-annotation-editor.jsx +52 -0
- package/lib/storybook/probe-intent.js +23 -0
- package/lib/storybook/setup.js +104 -0
- package/lib/types/avalanche/commit-metrics-adapter.d.ts +76 -0
- package/lib/types/avalanche/commit-stats-indices.d.ts +41 -0
- package/lib/types/avalanche/commit-surface-ownership.d.ts +9 -0
- package/lib/types/avalanche/detector.d.ts +183 -0
- package/lib/types/avalanche/interaction-capture.d.ts +25 -0
- package/lib/types/avalanche/internals.d.ts +189 -0
- package/lib/types/avalanche/report-serializer.d.ts +283 -0
- package/lib/types/avalanche/reporter.d.ts +610 -0
- package/lib/types/chippy-bridge/fiber-source.d.ts +48 -0
- package/lib/types/chippy-bridge/react-renderer.d.ts +74 -0
- package/lib/types/chippy-bridge/recorded-source.d.ts +44 -0
- package/lib/types/chippy-bridge/source-map.d.ts +30 -0
- package/lib/types/debug-render/core.d.ts +78 -0
- package/lib/types/layout-lab/fiber-boundaries.d.ts +51 -0
- package/lib/types/layout-lab/fiber-walk.d.ts +131 -0
- package/lib/types/layout-lab/marker-context.d.ts +94 -0
- package/lib/types/package/avalanche.d.ts +6 -0
- package/lib/types/package/browser.d.ts +10 -0
- package/lib/types/package/plugin.d.ts +15 -0
- package/lib/types/plugins/atl/commit-metrics-adapter.d.ts +65 -0
- package/lib/types/plugins/atl/metrics-slot.d.ts +37 -0
- package/lib/types/plugins/index.d.ts +49 -0
- package/lib/types/plugins/jira/context-provider.d.ts +36 -0
- package/package.json +106 -4
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { ChippyLogo } from './brand.jsx';
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { addons, types } from 'storybook/manager-api';
|
|
4
|
+
import { AddonPanel, Badge, Button, ScrollArea } from 'storybook/internal/components';
|
|
5
|
+
import { ADDON_ID, AVALANCHE_PANEL_ID, CHIPPY_PANEL_ID, EVENTS, PREVIEW_MESSAGE_SOURCE } from './constants.js';
|
|
6
|
+
import { ChippyConnectionFallback } from './connection-fallback.jsx';
|
|
7
|
+
import { commitSpan, describeCommitAction, groupCommitRows } from './avalanche-panel-model.js';
|
|
8
|
+
import { ContextCard } from './context-card.jsx';
|
|
9
|
+
import { claimManagerOwner, extensionBridgeInManager } from './manager-owner.js';
|
|
10
|
+
import { ProbeAnnotationEditor } from './probe-annotation-editor.jsx';
|
|
11
|
+
|
|
12
|
+
const panelFont = "'Atlassian Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
|
13
|
+
const theme = {
|
|
14
|
+
text: 'var(--ds-text, var(--color-default-text, CanvasText))',
|
|
15
|
+
muted: 'var(--color-subtle-text, color-mix(in srgb, CanvasText 64%, transparent))',
|
|
16
|
+
border: 'var(--color-medium-border, color-mix(in srgb, CanvasText 18%, transparent))',
|
|
17
|
+
panel: 'var(--ds-surface, var(--color-surface, Canvas))',
|
|
18
|
+
raised: 'var(--color-elevated-surface, color-mix(in srgb, Canvas 94%, CanvasText 6%))',
|
|
19
|
+
row: 'var(--color-hover-background, color-mix(in srgb, Canvas 88%, CanvasText 12%))',
|
|
20
|
+
danger: 'var(--color-negative-text, #ffb4ab)',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
claimManagerOwner();
|
|
24
|
+
|
|
25
|
+
function postToPreview(message) {
|
|
26
|
+
const iframe = document.getElementById('storybook-preview-iframe');
|
|
27
|
+
iframe?.contentWindow?.postMessage({ source: PREVIEW_MESSAGE_SOURCE, ...message }, '*');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isModeSelected(mode) {
|
|
31
|
+
const label = mode === 'avalanche' ? 'Avalanche' : 'Chippy';
|
|
32
|
+
const tab = Array.from(document.querySelectorAll('[role="tab"]'))
|
|
33
|
+
.find((candidate) => candidate.textContent?.trim() === label);
|
|
34
|
+
return tab?.getAttribute('aria-selected') === 'true';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function syncMode(channel, mode, active) {
|
|
38
|
+
if (!active || !isModeSelected(mode)) return;
|
|
39
|
+
channel.emit(EVENTS.SET_MODE, mode);
|
|
40
|
+
postToPreview({ event: EVENTS.SET_MODE, payload: mode });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function useChippyChannel(mode, active) {
|
|
44
|
+
const channel = addons.getChannel();
|
|
45
|
+
const [payload, setPayload] = useState(null);
|
|
46
|
+
const [lastResult, setLastResult] = useState(null);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const onState = (next) => setPayload(next);
|
|
50
|
+
const onResult = (next) => setLastResult(next);
|
|
51
|
+
channel.on(EVENTS.STATE, onState);
|
|
52
|
+
channel.on(EVENTS.RESULT, onResult);
|
|
53
|
+
channel.emit(EVENTS.REQUEST_STATE);
|
|
54
|
+
syncMode(channel, mode, active);
|
|
55
|
+
const timer = setInterval(() => {
|
|
56
|
+
channel.emit(EVENTS.REQUEST_STATE);
|
|
57
|
+
syncMode(channel, mode, active);
|
|
58
|
+
}, 3000);
|
|
59
|
+
return () => {
|
|
60
|
+
clearInterval(timer);
|
|
61
|
+
channel.off(EVENTS.STATE, onState);
|
|
62
|
+
channel.off(EVENTS.RESULT, onResult);
|
|
63
|
+
};
|
|
64
|
+
}, [channel, active, mode]);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
payload,
|
|
68
|
+
lastResult,
|
|
69
|
+
command: (type, extra = {}) => {
|
|
70
|
+
const command = { type, ...extra };
|
|
71
|
+
if (!active || !isModeSelected(mode)) return;
|
|
72
|
+
channel.emit(EVENTS.COMMAND, command);
|
|
73
|
+
postToPreview({ event: EVENTS.COMMAND, payload: command });
|
|
74
|
+
},
|
|
75
|
+
refresh: () => channel.emit(EVENTS.REQUEST_STATE),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function statusFrom(payload) {
|
|
80
|
+
if (payload?.blocked) return { label: 'blocked', status: 'negative' };
|
|
81
|
+
const state = payload?.state;
|
|
82
|
+
if (!state) return { label: 'waiting', status: 'warning' };
|
|
83
|
+
return { label: state.connState || 'ready', status: state.connState === 'connected' ? 'positive' : 'warning' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function RuntimeLine({ payload }) {
|
|
87
|
+
const state = payload?.state;
|
|
88
|
+
const owner = payload?.owner || state?.owner;
|
|
89
|
+
const integration = state?.integration || owner?.integration;
|
|
90
|
+
const status = statusFrom(payload);
|
|
91
|
+
const story = state?.page?.storyId || 'no story';
|
|
92
|
+
const client = state?.clientId ? String(state.clientId).slice(0, 8) : 'no client';
|
|
93
|
+
return (
|
|
94
|
+
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center', color: theme.muted, fontSize: 11 }}>
|
|
95
|
+
<Badge status={status.status}>{status.label}</Badge>
|
|
96
|
+
<span>{integration?.who || 'Unidentified Chippy integration'}</span>
|
|
97
|
+
<span>({integration?.kind || 'unknown'})</span>
|
|
98
|
+
<span>·</span>
|
|
99
|
+
<span>{story}</span>
|
|
100
|
+
<span>·</span>
|
|
101
|
+
<span>{client}</span>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function Header({ title, children }) {
|
|
107
|
+
return (
|
|
108
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, marginBottom: 10 }}>
|
|
109
|
+
<h2 style={{ margin: 0, fontSize: 14, lineHeight: 1, color: theme.text, display: 'flex', alignItems: 'center', gap: 8 }}><ChippyLogo />{title}</h2>
|
|
110
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>{children}</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function Footer({ payload }) {
|
|
116
|
+
return (
|
|
117
|
+
<details style={{ borderTop: `1px solid ${theme.border}`, paddingTop: 8, marginTop: 8 }}>
|
|
118
|
+
<summary style={{ cursor: 'pointer', color: theme.muted, fontSize: 11 }}>Connection details</summary>
|
|
119
|
+
<div style={{ marginTop: 8 }}><RuntimeLine payload={payload} /></div>
|
|
120
|
+
</details>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function Section({ title, children }) {
|
|
125
|
+
return (
|
|
126
|
+
<section style={{ borderTop: `1px solid ${theme.border}`, padding: '10px 0', color: theme.text }}>
|
|
127
|
+
{title ? <h3 style={{ margin: '0 0 8px', fontSize: 13, color: theme.text }}>{title}</h3> : null}
|
|
128
|
+
{children}
|
|
129
|
+
</section>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function Empty({ children }) {
|
|
134
|
+
return <div style={{ color: theme.muted, fontSize: 12, padding: '8px 0' }}>{children}</div>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function SmallJson({ value }) {
|
|
138
|
+
if (value == null) return null;
|
|
139
|
+
return (
|
|
140
|
+
<details style={{ marginTop: 8, color: theme.text }}>
|
|
141
|
+
<summary style={{ cursor: 'pointer', color: theme.muted, fontSize: 12 }}>raw</summary>
|
|
142
|
+
<pre style={{ margin: '8px 0 0', whiteSpace: 'pre-wrap', fontSize: 11, lineHeight: 1.4, maxHeight: 240, overflow: 'auto', color: theme.text, background: theme.raised, border: `1px solid ${theme.border}`, borderRadius: 6, padding: 8 }}>
|
|
143
|
+
{JSON.stringify(value, null, 2)}
|
|
144
|
+
</pre>
|
|
145
|
+
</details>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function compactSource(source) {
|
|
150
|
+
if (!source?.fileName) return '—';
|
|
151
|
+
const file = String(source.fileName).split('/').slice(-2).join('/');
|
|
152
|
+
return `${file}${source.lineNumber ? `:${source.lineNumber}` : ''}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function MarkerSummary({ markers, onInspectBoundary, onAttachAnnotation }) {
|
|
156
|
+
const entries = Object.entries(markers || {});
|
|
157
|
+
if (!entries.length) return <Empty>Open the Chippy panel to activate probes. Drag P1/P2 from the dock onto the story.</Empty>;
|
|
158
|
+
return (
|
|
159
|
+
<div style={{ display: 'grid', gap: 8 }}>
|
|
160
|
+
{entries.map(([id, marker]) => {
|
|
161
|
+
const resolved = marker?.resolved || {};
|
|
162
|
+
const component = resolved.component || {};
|
|
163
|
+
const dom = resolved.dom || {};
|
|
164
|
+
const owners = component.owners || [];
|
|
165
|
+
const source = component.source || dom.source;
|
|
166
|
+
const markerContext = marker?.context ?? null;
|
|
167
|
+
return (
|
|
168
|
+
<div key={id} style={{ border: `1px solid ${theme.border}`, borderRadius: 8, padding: 8, background: theme.raised, color: theme.text }}>
|
|
169
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8 }}>
|
|
170
|
+
<strong>P{id}</strong>
|
|
171
|
+
<Badge status={resolved.confidence === 'dev' || resolved.confidence === 'high' ? 'positive' : 'neutral'}>{marker?.docked ? 'docked' : 'active'}</Badge>
|
|
172
|
+
</div>
|
|
173
|
+
<div style={{ color: theme.text, fontSize: 13, marginTop: 6, fontWeight: 600 }}>
|
|
174
|
+
{component.name || dom.tag || 'Drag marker onto the story'}
|
|
175
|
+
</div>
|
|
176
|
+
<div style={{ color: theme.muted, fontSize: 12, marginTop: 3 }}>
|
|
177
|
+
{resolved.nearestTestId ? `testId ${resolved.nearestTestId} · ` : ''}{dom.tag ? `<${dom.tag}>` : 'no DOM target'}
|
|
178
|
+
</div>
|
|
179
|
+
<div style={{ color: theme.muted, fontSize: 12, marginTop: 3 }}>source {compactSource(source)}</div>
|
|
180
|
+
{owners.length ? <div style={{ color: theme.muted, fontSize: 12, marginTop: 3 }}>owner {owners[0]?.name || '—'} · chain {owners.length}</div> : null}
|
|
181
|
+
<ContextCard
|
|
182
|
+
context={markerContext}
|
|
183
|
+
onInspectBoundary={
|
|
184
|
+
onInspectBoundary
|
|
185
|
+
? (kind) => onInspectBoundary(id, kind, markerContext?.boundaries?.[kind] || null)
|
|
186
|
+
: undefined
|
|
187
|
+
}
|
|
188
|
+
/>
|
|
189
|
+
{!marker?.docked ? (
|
|
190
|
+
<ProbeAnnotationEditor
|
|
191
|
+
markerId={id}
|
|
192
|
+
intent={marker?.intent}
|
|
193
|
+
comment={marker?.comment}
|
|
194
|
+
onAttach={(annotation) => onAttachAnnotation?.(id, annotation)}
|
|
195
|
+
/>
|
|
196
|
+
) : null}
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
})}
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function resultEnvelope(lastResult) {
|
|
205
|
+
const result = lastResult?.result || null;
|
|
206
|
+
const payload = result?.payload || null;
|
|
207
|
+
return { result, payload };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function rowsFromAvalanchePayload(payload) {
|
|
211
|
+
const auto = payload?.avalancheAuto || {};
|
|
212
|
+
return {
|
|
213
|
+
hotItems: auto.hotSources?.payload?.items || auto.hotSources?.items || [],
|
|
214
|
+
commitRows: auto.commitList?.rows || auto.commitList?.payload?.rows || [],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function rowsFromAvalancheResult(lastResult) {
|
|
219
|
+
const { result, payload } = resultEnvelope(lastResult);
|
|
220
|
+
if (!result) return { result: null, hotItems: [], commitRows: [], peek: null };
|
|
221
|
+
return {
|
|
222
|
+
result,
|
|
223
|
+
hotItems: result.action === 'hot-sources' ? payload?.items || [] : [],
|
|
224
|
+
commitRows: result.action === 'commit-list' ? result.rows || [] : [],
|
|
225
|
+
peek: result.action === 'commit-peek' ? payload || null : null,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function SimpleTable({ columns, rows, empty, onRowClick }) {
|
|
230
|
+
if (!rows?.length) return <Empty>{empty}</Empty>;
|
|
231
|
+
return (
|
|
232
|
+
<div style={{ border: `1px solid ${theme.border}`, borderRadius: 8, overflow: 'hidden', background: theme.panel, color: theme.text }}>
|
|
233
|
+
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12, color: theme.text }}>
|
|
234
|
+
<thead style={{ background: theme.raised }}>
|
|
235
|
+
<tr>{columns.map((column) => <th key={column.key} style={{ textAlign: 'left', padding: 8, borderBottom: `1px solid ${theme.border}`, color: theme.text }}>{column.label}</th>)}</tr>
|
|
236
|
+
</thead>
|
|
237
|
+
<tbody>
|
|
238
|
+
{rows.map((row, index) => (
|
|
239
|
+
<tr key={row.commitId || row.sourceId || index} onClick={onRowClick ? () => onRowClick(row) : undefined} style={{ cursor: onRowClick ? 'pointer' : 'default' }}>
|
|
240
|
+
{columns.map((column) => <td key={column.key} style={{ padding: 8, borderTop: index ? `1px solid ${theme.border}` : 'none', verticalAlign: 'top', color: theme.text }}>{column.render ? column.render(row) : String(row[column.key] ?? '—')}</td>)}
|
|
241
|
+
</tr>
|
|
242
|
+
))}
|
|
243
|
+
</tbody>
|
|
244
|
+
</table>
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Shown when an extension bridge is still connected from the manager window, so
|
|
250
|
+
// a `pages` entry of kind `extension` would inspect Storybook's own UI.
|
|
251
|
+
function ExtensionConflict() {
|
|
252
|
+
const [conflict, setConflict] = useState(() => !!extensionBridgeInManager());
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
const timer = setInterval(() => setConflict(!!extensionBridgeInManager()), 3000);
|
|
255
|
+
return () => clearInterval(timer);
|
|
256
|
+
}, []);
|
|
257
|
+
if (!conflict) return null;
|
|
258
|
+
return (
|
|
259
|
+
<div role="alert" style={{ color: theme.danger, fontSize: 12, marginBottom: 12 }}>
|
|
260
|
+
The Chippy extension is also connected from the Storybook manager. Its `extension` page inspects
|
|
261
|
+
Storybook's UI; use the `storybook` page for the story, or update the extension.
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function ChippyPanel({ active }) {
|
|
267
|
+
const { payload, command } = useChippyChannel('chippy', active);
|
|
268
|
+
const inspectBoundary = React.useCallback(
|
|
269
|
+
(markerId, boundaryKind, boundary) =>
|
|
270
|
+
command('inspect-boundary', {
|
|
271
|
+
markerId,
|
|
272
|
+
boundaryKind,
|
|
273
|
+
boundary,
|
|
274
|
+
distance: boundary?.distance ?? null,
|
|
275
|
+
}),
|
|
276
|
+
[command],
|
|
277
|
+
);
|
|
278
|
+
const attachAnnotation = React.useCallback(
|
|
279
|
+
(markerId, annotation) => command('set-probe-annotation', { markerId, ...annotation }),
|
|
280
|
+
[command],
|
|
281
|
+
);
|
|
282
|
+
const state = payload?.state;
|
|
283
|
+
const markers = state?.markers || {};
|
|
284
|
+
const probesVisible = Object.values(markers).some((marker) => marker?.position != null);
|
|
285
|
+
return (
|
|
286
|
+
<AddonPanel active={active}>
|
|
287
|
+
<ScrollArea vertical horizontal>
|
|
288
|
+
<div style={{ padding: 16, fontFamily: panelFont, maxWidth: 840, color: theme.text, background: theme.panel, minHeight: '100%' }}>
|
|
289
|
+
<Header title="Chippy">
|
|
290
|
+
<Button
|
|
291
|
+
size="small"
|
|
292
|
+
ariaLabel={false}
|
|
293
|
+
variant={probesVisible ? 'outline' : undefined}
|
|
294
|
+
onClick={() => command(probesVisible ? 'clear-probes' : 'ensure-probes')}
|
|
295
|
+
>
|
|
296
|
+
{probesVisible ? 'Hide probes' : 'Show probes'}
|
|
297
|
+
</Button>
|
|
298
|
+
</Header>
|
|
299
|
+
<ChippyConnectionFallback connState={state?.connState} />
|
|
300
|
+
<ExtensionConflict />
|
|
301
|
+
<Section>
|
|
302
|
+
{probesVisible ? (
|
|
303
|
+
<div style={{ color: theme.muted, fontSize: 12, marginBottom: 8 }}>
|
|
304
|
+
While dragging: ← parent DOM · → toward target · ↑ parent component · ↓ toward target
|
|
305
|
+
</div>
|
|
306
|
+
) : null}
|
|
307
|
+
{probesVisible
|
|
308
|
+
? <MarkerSummary markers={markers} onInspectBoundary={inspectBoundary} onAttachAnnotation={attachAnnotation} />
|
|
309
|
+
: <Empty>Show probes when you want to inspect an element in the story.</Empty>}
|
|
310
|
+
</Section>
|
|
311
|
+
<Footer payload={payload} />
|
|
312
|
+
</div>
|
|
313
|
+
</ScrollArea>
|
|
314
|
+
</AddonPanel>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function AvalanchePanel({ active }) {
|
|
319
|
+
const { payload, lastResult, command } = useChippyChannel('avalanche', active);
|
|
320
|
+
const autoRows = rowsFromAvalanchePayload(payload);
|
|
321
|
+
const { result, peek } = rowsFromAvalancheResult(lastResult);
|
|
322
|
+
const hotItems = autoRows.hotItems;
|
|
323
|
+
const rows = autoRows.commitRows;
|
|
324
|
+
const patterns = groupCommitRows(rows);
|
|
325
|
+
|
|
326
|
+
const status = payload?.avalancheStatus?.status || payload?.state?.avalanche?.status || null;
|
|
327
|
+
const error = result?.ok === false ? result.error : null;
|
|
328
|
+
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
if (!active) return;
|
|
331
|
+
if (status?.installed) return;
|
|
332
|
+
const timer = setTimeout(() => command('avalanche-install', { reason: 'panel-active-retry' }), 250);
|
|
333
|
+
return () => clearTimeout(timer);
|
|
334
|
+
}, [active, status?.installed]);
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<AddonPanel active={active}>
|
|
338
|
+
<ScrollArea vertical horizontal>
|
|
339
|
+
<div style={{ padding: 16, fontFamily: panelFont, maxWidth: 980, color: theme.text, background: theme.panel, minHeight: '100%' }}>
|
|
340
|
+
<Header title="Avalanche" />
|
|
341
|
+
<ChippyConnectionFallback connState={payload?.state?.connState} />
|
|
342
|
+
{error ? <div style={{ color: theme.danger, fontSize: 12, marginBottom: 8 }}>{error}</div> : null}
|
|
343
|
+
<Section title="Commit patterns">
|
|
344
|
+
<SimpleTable
|
|
345
|
+
rows={patterns}
|
|
346
|
+
empty="Waiting for the story to render something new."
|
|
347
|
+
onRowClick={(group) => command('avalanche-commit-peek', { commitId: group.representative.commitId })}
|
|
348
|
+
columns={[
|
|
349
|
+
{ key: 'count', label: 'repeats', render: (group) => group.count === 1 ? 'once' : `${group.count}×` },
|
|
350
|
+
{ key: 'span', label: 'commits', render: commitSpan },
|
|
351
|
+
{ key: 'action', label: 'activity', render: (group) => describeCommitAction(group.representative) },
|
|
352
|
+
{ key: 'sources', label: 'sources', render: (group) => group.representative.sourceCount ?? 0 },
|
|
353
|
+
{ key: 'collateral', label: 'collateral', render: (group) => group.representative.stat?.collateral ?? 0 },
|
|
354
|
+
{ key: 'effects', label: 'effects', render: (group) => group.representative.stat?.effects ?? 0 },
|
|
355
|
+
]}
|
|
356
|
+
/>
|
|
357
|
+
</Section>
|
|
358
|
+
<Section title="Sources with the widest impact">
|
|
359
|
+
<SimpleTable
|
|
360
|
+
rows={hotItems.slice(0, 10)}
|
|
361
|
+
empty="No source has caused measurable downstream work yet."
|
|
362
|
+
columns={[
|
|
363
|
+
{ key: 'sourceId', label: 'source' },
|
|
364
|
+
{ key: 'collateralUpdates', label: 'collateral' },
|
|
365
|
+
{ key: 'causedUpdates', label: 'caused' },
|
|
366
|
+
{ key: 'restartWaste', label: 'restart waste' },
|
|
367
|
+
]}
|
|
368
|
+
/>
|
|
369
|
+
</Section>
|
|
370
|
+
{peek ? (
|
|
371
|
+
<Section title={`Commit ${peek.commitId} peek`}>
|
|
372
|
+
<div style={{ display: 'flex', gap: 10, color: theme.muted, fontSize: 12, marginBottom: 8 }}>
|
|
373
|
+
<span>found: <strong style={{ color: theme.text }}>{String(!!peek.found)}</strong></span>
|
|
374
|
+
<span>sources: <strong style={{ color: theme.text }}>{peek.sourcesTotal ?? peek.sources?.length ?? 0}</strong></span>
|
|
375
|
+
<span>actions: <strong style={{ color: theme.text }}>{peek.actionsTotal ?? peek.actions?.length ?? 0}</strong></span>
|
|
376
|
+
</div>
|
|
377
|
+
<SimpleTable
|
|
378
|
+
rows={peek.sources || []}
|
|
379
|
+
empty="No source rows in this peek."
|
|
380
|
+
columns={[
|
|
381
|
+
{ key: 'sourceId', label: 'source' },
|
|
382
|
+
{ key: 'collateralAtCommit', label: 'collateral@commit' },
|
|
383
|
+
{ key: 'causedUpdatesTotal', label: 'caused total' },
|
|
384
|
+
{ key: 'collateralUpdatesTotal', label: 'collateral total' },
|
|
385
|
+
]}
|
|
386
|
+
/>
|
|
387
|
+
<SmallJson value={peek} />
|
|
388
|
+
</Section>
|
|
389
|
+
) : null}
|
|
390
|
+
<details style={{ marginTop: 8 }}>
|
|
391
|
+
<summary style={{ cursor: 'pointer', color: theme.muted, fontSize: 12 }}>Individual commits ({rows.length})</summary>
|
|
392
|
+
<div style={{ marginTop: 8 }}>
|
|
393
|
+
<SimpleTable
|
|
394
|
+
rows={rows}
|
|
395
|
+
empty="No commits captured."
|
|
396
|
+
onRowClick={(row) => command('avalanche-commit-peek', { commitId: row.commitId })}
|
|
397
|
+
columns={[
|
|
398
|
+
{ key: 'commitId', label: 'commit' },
|
|
399
|
+
{ key: 'sourceCount', label: 'sources' },
|
|
400
|
+
{ key: 'collateral', label: 'collateral', render: (row) => row.stat?.collateral ?? 0 },
|
|
401
|
+
{ key: 'effects', label: 'effects', render: (row) => row.stat?.effects ?? 0 },
|
|
402
|
+
{ key: 'action', label: 'activity', render: describeCommitAction },
|
|
403
|
+
]}
|
|
404
|
+
/>
|
|
405
|
+
</div>
|
|
406
|
+
</details>
|
|
407
|
+
<Footer payload={payload} />
|
|
408
|
+
</div>
|
|
409
|
+
</ScrollArea>
|
|
410
|
+
</AddonPanel>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
addons.register(ADDON_ID, () => {
|
|
415
|
+
addons.add(CHIPPY_PANEL_ID, {
|
|
416
|
+
title: 'Chippy',
|
|
417
|
+
type: types.PANEL,
|
|
418
|
+
render: ({ active }) => <ChippyPanel active={active} />,
|
|
419
|
+
});
|
|
420
|
+
addons.add(AVALANCHE_PANEL_ID, {
|
|
421
|
+
title: 'Avalanche',
|
|
422
|
+
type: types.PANEL,
|
|
423
|
+
render: ({ active }) => <AvalanchePanel active={active} />,
|
|
424
|
+
});
|
|
425
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
|
|
6
|
+
export const managerEntries = (entry = []) => [...entry, join(here, 'manager.jsx')];
|
|
7
|
+
export const previewAnnotations = (entry = []) => [...entry, join(here, 'preview.js')];
|