@atlassian/chippy 0.0.0-stage → 0.0.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +188 -2
  3. package/bin/chippy.mjs +134 -0
  4. package/bin/pack.mjs +184 -0
  5. package/lib/avalanche.js +2570 -0
  6. package/lib/avalanche.js.map +7 -0
  7. package/lib/bootstrap.d.ts +1 -0
  8. package/lib/bootstrap.js +51 -0
  9. package/lib/bootstrap.js.map +7 -0
  10. package/lib/browser.js +15 -0
  11. package/lib/browser.js.map +7 -0
  12. package/lib/extension/avalanche.es.js +5396 -0
  13. package/lib/extension/avalanche.es.js.map +1 -0
  14. package/lib/extension/chippy-page-bridge.js +1833 -0
  15. package/lib/extension/inspect-react.js +216 -0
  16. package/lib/extension/layout-lab.js +1453 -0
  17. package/lib/extension/panel-dom.js +90 -0
  18. package/lib/extension/resolve-probes.js +342 -0
  19. package/lib/mcp-skill/LICENSE +201 -0
  20. package/lib/mcp-skill/README.md +150 -0
  21. package/lib/mcp-skill/SKILL.md +330 -0
  22. package/lib/mcp-skill/browser-scripts/playwright-probes.js +368 -0
  23. package/lib/mcp-skill/browser-scripts/playwright-session.js +114 -0
  24. package/lib/mcp-skill/extension/avalanche.es.js +5396 -0
  25. package/lib/mcp-skill/extension/avalanche.es.js.map +1 -0
  26. package/lib/mcp-skill/extension/avalanche.iife.js +5397 -0
  27. package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -0
  28. package/lib/mcp-skill/extension/background.js +323 -0
  29. package/lib/mcp-skill/extension/chippy-page-bridge.js +1833 -0
  30. package/lib/mcp-skill/extension/content-script.js +167 -0
  31. package/lib/mcp-skill/extension/devtools-hook-shim.js +68 -0
  32. package/lib/mcp-skill/extension/early-bootstrap.js +47 -0
  33. package/lib/mcp-skill/extension/icon-128.png +0 -0
  34. package/lib/mcp-skill/extension/icon-16.png +0 -0
  35. package/lib/mcp-skill/extension/icon-32.png +0 -0
  36. package/lib/mcp-skill/extension/icon-48.png +0 -0
  37. package/lib/mcp-skill/extension/inspect-react.js +216 -0
  38. package/lib/mcp-skill/extension/layout-lab.js +1453 -0
  39. package/lib/mcp-skill/extension/logo.svg +14 -0
  40. package/lib/mcp-skill/extension/manifest.json +69 -0
  41. package/lib/mcp-skill/extension/panel-dom.js +90 -0
  42. package/lib/mcp-skill/extension/panel-settings.js +191 -0
  43. package/lib/mcp-skill/extension/popup.html +41 -0
  44. package/lib/mcp-skill/extension/popup.js +80 -0
  45. package/lib/mcp-skill/extension/probe-descriptor.js +241 -0
  46. package/lib/mcp-skill/extension/resolve-probes.js +342 -0
  47. package/lib/mcp-skill/package.json +1 -0
  48. package/lib/mcp-skill/references/README.md +56 -0
  49. package/lib/mcp-skill/references/active-stimulus.md +85 -0
  50. package/lib/mcp-skill/references/avalanche-detector.md +731 -0
  51. package/lib/mcp-skill/references/bdd-router.md +310 -0
  52. package/lib/mcp-skill/references/browser-runtime-api.md +193 -0
  53. package/lib/mcp-skill/references/command-output-formats.md +1075 -0
  54. package/lib/mcp-skill/references/command-surface.md +183 -0
  55. package/lib/mcp-skill/references/cynefin.md +99 -0
  56. package/lib/mcp-skill/references/debug-hook.md +161 -0
  57. package/lib/mcp-skill/references/fixing-performance.md +490 -0
  58. package/lib/mcp-skill/references/hook-metrics.md +199 -0
  59. package/lib/mcp-skill/references/hooks-inspection.md +61 -0
  60. package/lib/mcp-skill/references/jira-taxonomy.md +81 -0
  61. package/lib/mcp-skill/references/mcp.md +122 -0
  62. package/lib/mcp-skill/references/mode-a-injection.md +68 -0
  63. package/lib/mcp-skill/references/operating-model.md +128 -0
  64. package/lib/mcp-skill/references/playwright-cli-setup.md +105 -0
  65. package/lib/mcp-skill/references/playwright-probes.md +205 -0
  66. package/lib/mcp-skill/references/procedures.md +545 -0
  67. package/lib/mcp-skill/references/recipes.txt +114 -0
  68. package/lib/mcp-skill/references/stability-render.md +428 -0
  69. package/lib/mcp-skill/references/test-mode-playbook.md +193 -0
  70. package/lib/mcp-skill/references/troubleshooting.md +54 -0
  71. package/lib/mcp-skill/scripts/avalanche-analyze.cjs +495 -0
  72. package/lib/mcp-skill/scripts/build-avalanche-command.js +34 -0
  73. package/lib/mcp-skill/scripts/build-fiber-state-command.js +19 -0
  74. package/lib/mcp-skill/scripts/chippy-helper-server.js +1710 -0
  75. package/lib/mcp-skill/scripts/chippy-mcp.cjs +273 -0
  76. package/lib/mcp-skill/scripts/chippy.sh +1218 -0
  77. package/lib/mcp-skill/scripts/format-probe-result.js +34 -0
  78. package/lib/mcp-skill/scripts/helper-access.js +144 -0
  79. package/lib/mcp-skill/scripts/probe-annotation-channel.js +116 -0
  80. package/lib/mcp-skill/scripts/search-recipes.js +248 -0
  81. package/lib/mcp-skill/snippets/README.md +89 -0
  82. package/lib/mcp-skill/snippets/count-context-consumers.js +40 -0
  83. package/lib/mcp-skill/snippets/find-context-providers.js +38 -0
  84. package/lib/mcp-skill/snippets/find-legacy-context.js +47 -0
  85. package/lib/mcp-skill/snippets/find-prop.js +83 -0
  86. package/lib/mcp-skill/snippets/hook-metrics-sample.js +69 -0
  87. package/lib/mcp-skill/snippets/hook-metrics.js +60 -0
  88. package/lib/mcp-skill/snippets/hook-tracker.js +198 -0
  89. package/lib/mcp-skill/snippets/stability-nudge.js +101 -0
  90. package/lib/package.json +1 -0
  91. package/lib/plugin.js +119 -0
  92. package/lib/plugin.js.map +7 -0
  93. package/lib/storybook/avalanche-panel-model.js +79 -0
  94. package/lib/storybook/brand.jsx +6 -0
  95. package/lib/storybook/connection-fallback.jsx +47 -0
  96. package/lib/storybook/constants.js +15 -0
  97. package/lib/storybook/context-card.jsx +171 -0
  98. package/lib/storybook/manager.jsx +403 -0
  99. package/lib/storybook/preset.js +7 -0
  100. package/lib/storybook/preview.js +263 -0
  101. package/lib/storybook/probe-annotation-editor.jsx +52 -0
  102. package/lib/storybook/probe-intent.js +23 -0
  103. package/lib/storybook/setup.js +93 -0
  104. package/lib/types/avalanche/commit-metrics-adapter.d.ts +76 -0
  105. package/lib/types/avalanche/commit-stats-indices.d.ts +41 -0
  106. package/lib/types/avalanche/commit-surface-ownership.d.ts +9 -0
  107. package/lib/types/avalanche/detector.d.ts +183 -0
  108. package/lib/types/avalanche/interaction-capture.d.ts +25 -0
  109. package/lib/types/avalanche/internals.d.ts +189 -0
  110. package/lib/types/avalanche/report-serializer.d.ts +283 -0
  111. package/lib/types/avalanche/reporter.d.ts +610 -0
  112. package/lib/types/chippy-bridge/fiber-source.d.ts +37 -0
  113. package/lib/types/chippy-bridge/react-renderer.d.ts +49 -0
  114. package/lib/types/chippy-bridge/source-map.d.ts +30 -0
  115. package/lib/types/debug-render/core.d.ts +72 -0
  116. package/lib/types/layout-lab/fiber-boundaries.d.ts +51 -0
  117. package/lib/types/layout-lab/fiber-walk.d.ts +131 -0
  118. package/lib/types/layout-lab/marker-context.d.ts +94 -0
  119. package/lib/types/package/avalanche.d.ts +6 -0
  120. package/lib/types/package/browser.d.ts +10 -0
  121. package/lib/types/package/plugin.d.ts +15 -0
  122. package/lib/types/plugins/atl/commit-metrics-adapter.d.ts +65 -0
  123. package/lib/types/plugins/atl/metrics-slot.d.ts +37 -0
  124. package/lib/types/plugins/index.d.ts +49 -0
  125. package/lib/types/plugins/jira/context-provider.d.ts +36 -0
  126. package/package.json +104 -4
package/lib/plugin.js ADDED
@@ -0,0 +1,119 @@
1
+ // src/chippy-bridge/source-map.ts
2
+ var DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3
+ var VALUE_OF = new Int8Array(128).fill(-1);
4
+ for (let index = 0; index < DIGITS.length; index += 1) {
5
+ VALUE_OF[DIGITS.charCodeAt(index)] = index;
6
+ }
7
+
8
+ // src/chippy-bridge/fiber-source.ts
9
+ var V8_FRAME = /^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/;
10
+ var AT_FRAME = /^\s*(?:(.*?)@)(.+?):(\d+):(\d+)$/;
11
+ var MAX_CALL_SITE_FRAMES = 4;
12
+ function parseStackFrames(stack) {
13
+ const frames = [];
14
+ for (const raw of stack.split("\n")) {
15
+ const line = raw.trimEnd();
16
+ if (line === "" || line.includes("(eval at ")) continue;
17
+ const match = V8_FRAME.exec(line) ?? AT_FRAME.exec(line);
18
+ if (!match) continue;
19
+ const [, name, url = "", lineDigits = "", columnDigits = ""] = match;
20
+ if (url === "" || url.startsWith("data:")) continue;
21
+ const at = Number(lineDigits);
22
+ const column = Number(columnDigits);
23
+ if (!Number.isFinite(at) || !Number.isFinite(column) || at < 1) continue;
24
+ frames.push({
25
+ url,
26
+ line: at,
27
+ column,
28
+ ...name !== void 0 && name !== "" ? { function: name } : {}
29
+ });
30
+ }
31
+ return frames;
32
+ }
33
+ function isVendorPath(path) {
34
+ return path.includes("node_modules") || path.includes("/.vite/deps/") || path.includes("/.yarn/");
35
+ }
36
+ function rawLocation(frame) {
37
+ return { fileName: frame.url, lineNumber: frame.line, columnNumber: frame.column };
38
+ }
39
+ function locationFromOriginal(original) {
40
+ return { fileName: original.source, lineNumber: original.line, columnNumber: original.column };
41
+ }
42
+ function writerLocationOf(frames, originalFor) {
43
+ for (const frame of frames) {
44
+ if (isVendorPath(frame.url)) continue;
45
+ const original = originalFor(frame);
46
+ if (original === null) return rawLocation(frame);
47
+ if (isVendorPath(original.source)) continue;
48
+ return locationFromOriginal(original);
49
+ }
50
+ return null;
51
+ }
52
+ function debugSourceOf(value) {
53
+ if (!value || typeof value !== "object") return null;
54
+ const source = value;
55
+ if (typeof source.fileName !== "string" || typeof source.lineNumber !== "number") return null;
56
+ return {
57
+ fileName: source.fileName,
58
+ lineNumber: source.lineNumber,
59
+ columnNumber: typeof source.columnNumber === "number" ? source.columnNumber : null
60
+ };
61
+ }
62
+ function recordedSource(fiber) {
63
+ const direct = debugSourceOf(fiber._debugSource);
64
+ if (direct) return direct;
65
+ const type = fiber.type;
66
+ return type && (typeof type === "object" || typeof type === "function") ? debugSourceOf(type._debugSource) : null;
67
+ }
68
+ function getFiberStackFrames(fiber) {
69
+ if (!fiber || !fiber._debugStack || typeof fiber._debugStack !== "object") return [];
70
+ const stack = fiber._debugStack.stack;
71
+ if (typeof stack !== "string" || stack === "") return [];
72
+ const candidates = [];
73
+ for (const frame of parseStackFrames(stack)) {
74
+ if (isVendorPath(frame.url)) continue;
75
+ candidates.push(frame);
76
+ if (candidates.length >= MAX_CALL_SITE_FRAMES) break;
77
+ }
78
+ return candidates;
79
+ }
80
+ function getFiberSourceCandidate(fiber) {
81
+ if (!fiber) return null;
82
+ const recorded = recordedSource(fiber);
83
+ if (recorded) return recorded;
84
+ return writerLocationOf(getFiberStackFrames(fiber), () => null);
85
+ }
86
+
87
+ // src/layout-lab/fiber-walk.ts
88
+ function getDisplayName(type) {
89
+ if (!type) return null;
90
+ if (typeof type === "string") return type;
91
+ const obj = type;
92
+ return obj.displayName || obj.name || null;
93
+ }
94
+
95
+ // src/avalanche/internals.tsx
96
+ var COMPONENT_TAGS = /* @__PURE__ */ new Set([0, 1, 2, 11, 14, 15]);
97
+ function isComponentFiber(tag) {
98
+ return COMPONENT_TAGS.has(tag);
99
+ }
100
+ function getComponentName(fiber) {
101
+ const type = fiber.type;
102
+ const elementType = fiber.elementType;
103
+ const named = type?.displayName || type?.name || elementType?.displayName || elementType?.name;
104
+ if (named) return named;
105
+ const source = getFiberSourceCandidate(fiber);
106
+ return source && `<${source.fileName}:${source.lineNumber}>` || "Anonymous";
107
+ }
108
+
109
+ // src/package/plugin.ts
110
+ function definePlugin(plugin) {
111
+ return plugin;
112
+ }
113
+ export {
114
+ definePlugin,
115
+ getComponentName,
116
+ getDisplayName,
117
+ isComponentFiber
118
+ };
119
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/chippy-bridge/source-map.ts", "../src/chippy-bridge/fiber-source.ts", "../src/layout-lab/fiber-walk.ts", "../src/avalanche/internals.tsx", "../src/package/plugin.ts"],
4
+ "sourcesContent": ["/**\n * Small, dependency-free Source Map v3 reader.\n *\n * Chippy only needs the generated-position lookup used by React 19's\n * `_debugStack` source path, so it deliberately omits names and source contents.\n */\n\nconst DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\nconst VALUE_OF = new Int8Array(128).fill(-1);\nfor (let index = 0; index < DIGITS.length; index += 1) {\n VALUE_OF[DIGITS.charCodeAt(index)] = index;\n}\n\ntype Segment = {\n generatedColumn: number;\n sourceIndex: number | null;\n originalLine: number | null;\n originalColumn: number | null;\n};\n\nexport type SourceMap = {\n sources: readonly string[];\n lines: readonly (readonly Segment[])[];\n offset: { line: number; column: number };\n};\n\nexport type OriginalPosition = {\n source: string;\n line: number;\n column: number;\n};\n\ntype RawMap = {\n version?: unknown;\n sources?: unknown;\n sourceRoot?: unknown;\n mappings?: unknown;\n sections?: unknown;\n};\n\nfunction decodeVlq(field: string): number[] {\n const values: number[] = [];\n let value = 0;\n let shift = 0;\n\n for (let index = 0; index < field.length; index += 1) {\n const code = field.charCodeAt(index);\n const digit = code < 128 ? VALUE_OF[code]! : -1;\n if (digit < 0) return values;\n\n const more = (digit & 32) !== 0;\n value += (digit & 31) << shift;\n if (more) {\n shift += 5;\n continue;\n }\n\n const negative = (value & 1) === 1;\n const magnitude = value >>> 1;\n values.push(negative ? (magnitude === 0 ? -0x80000000 : -magnitude) : magnitude);\n value = 0;\n shift = 0;\n }\n\n return values;\n}\n\nfunction decodeMappings(mappings: string, sourceCount: number): Segment[][] {\n const lines: Segment[][] = [];\n let sourceIndex = 0;\n let originalLine = 0;\n let originalColumn = 0;\n\n for (const encodedLine of mappings.split(';')) {\n const segments: Segment[] = [];\n let generatedColumn = 0;\n\n for (const field of encodedLine === '' ? [] : encodedLine.split(',')) {\n const values = decodeVlq(field);\n if (values.length < 4) {\n if (values.length > 0) {\n generatedColumn += values[0]!;\n segments.push({ generatedColumn, sourceIndex: null, originalLine: null, originalColumn: null });\n }\n continue;\n }\n\n generatedColumn += values[0]!;\n sourceIndex += values[1]!;\n originalLine += values[2]!;\n originalColumn += values[3]!;\n if (sourceIndex >= 0 && sourceIndex < sourceCount) {\n segments.push({ generatedColumn, sourceIndex, originalLine, originalColumn });\n }\n }\n\n segments.sort((left, right) => left.generatedColumn - right.generatedColumn);\n lines.push(segments);\n }\n\n return lines;\n}\n\nfunction resolveSources(raw: RawMap): string[] | null {\n if (!Array.isArray(raw.sources)) return null;\n const root = typeof raw.sourceRoot === 'string' ? raw.sourceRoot : '';\n\n return raw.sources.map((source) => {\n if (typeof source !== 'string') return '';\n if (root === '' || source === '' || /^[a-z][a-z0-9+.-]*:/i.test(source)) return source;\n if (root.endsWith('/') && source.startsWith('/')) return `${root}${source.slice(1)}`;\n if (root.endsWith('/') || source.startsWith('/')) return `${root}${source}`;\n return `${root}/${source}`;\n });\n}\n\nexport function parseSourceMap(text: string): SourceMap[] {\n let raw: RawMap;\n try {\n raw = JSON.parse(text) as RawMap;\n } catch {\n return [];\n }\n\n if (raw === null || typeof raw !== 'object') return [];\n if (raw.version !== 3) return [];\n\n if (Array.isArray(raw.sections)) {\n return raw.sections.flatMap((section) => {\n if (section === null || typeof section !== 'object') return [];\n const { offset, map } = section as { offset?: unknown; map?: unknown };\n if (offset === null || typeof offset !== 'object' || map === null || typeof map !== 'object') return [];\n const { line, column } = offset as { line?: unknown; column?: unknown };\n if (typeof line !== 'number' || typeof column !== 'number') return [];\n return parseSourceMap(JSON.stringify(map)).map((parsed) => ({ ...parsed, offset: { line, column } }));\n });\n }\n\n const sources = resolveSources(raw);\n if (sources === null || typeof raw.mappings !== 'string') return [];\n return [{ sources, lines: decodeMappings(raw.mappings, sources.length), offset: { line: 0, column: 0 } }];\n}\n\nfunction sectionsFor(maps: readonly SourceMap[], line: number, column: number): readonly SourceMap[] {\n if (\n maps.length === 0 ||\n (maps.length === 1 && maps[0]?.offset.line === 0 && maps[0]?.offset.column === 0)\n ) return maps;\n const zeroBasedLine = line - 1;\n const candidates = maps.filter(\n (map) =>\n map.offset.line < zeroBasedLine ||\n (map.offset.line === zeroBasedLine && map.offset.column < column),\n );\n return candidates.length === 0 ? [] : [candidates[candidates.length - 1]!];\n}\n\nfunction segmentAt(segments: readonly Segment[], column: number): Segment | undefined {\n let low = 0;\n let high = segments.length - 1;\n let found: Segment | undefined;\n\n while (low <= high) {\n const middle = (low + high) >> 1;\n const segment = segments[middle]!;\n if (segment.generatedColumn <= column) {\n found = segment;\n low = middle + 1;\n } else {\n high = middle - 1;\n }\n }\n\n return found;\n}\n\nexport function originalPositionFor(\n maps: readonly SourceMap[],\n line: number,\n column: number,\n): OriginalPosition | null {\n for (const map of sectionsFor(maps, line, column)) {\n const localLine = line - map.offset.line;\n const localColumn = localLine === 1 ? column - map.offset.column : column;\n const segments = map.lines[localLine - 1];\n if (segments === undefined || segments.length === 0) continue;\n\n const found = segmentAt(segments, localColumn - 1);\n if (found === undefined) continue;\n if (\n found.sourceIndex === null ||\n found.originalLine === null ||\n found.originalColumn === null\n ) return null;\n const source = map.sources[found.sourceIndex];\n if (source === undefined || source === '') continue;\n return { source, line: found.originalLine + 1, column: found.originalColumn + 1 };\n }\n\n return null;\n}\n\nexport function sourceMappingUrlOf(code: string): string | null {\n const found = [...code.matchAll(/^[/*]{2,}[#@]\\s*sourceMappingURL=(\\S+)\\s*(?:\\*\\/)?$/gm)];\n return found.at(-1)?.[1] ?? null;\n}\n\nexport function inlineSourceMapOf(url: string): string | null {\n const match = /^data:application\\/json[^,]*,(.*)$/s.exec(url);\n if (match === null) return null;\n const payload = match[1]!;\n\n if (!/;base64/i.test(url)) {\n try {\n return decodeURIComponent(payload);\n } catch {\n return null;\n }\n }\n\n try {\n const decode = (globalThis as { atob?: (input: string) => string }).atob;\n if (decode) {\n const binary = decode(payload);\n const Decoder = (globalThis as {\n TextDecoder?: new () => { decode(bytes: Uint8Array): string };\n }).TextDecoder;\n return Decoder\n ? new Decoder().decode(Uint8Array.from(binary, (character) => character.charCodeAt(0)))\n : binary;\n }\n const buffer = (globalThis as {\n Buffer?: { from(value: string, encoding: string): { toString(encoding: string): string } };\n }).Buffer;\n return buffer?.from(payload, 'base64').toString('utf8') ?? null;\n } catch {\n return null;\n }\n}\n", "import {\n inlineSourceMapOf,\n originalPositionFor,\n parseSourceMap,\n sourceMappingUrlOf,\n type OriginalPosition,\n type SourceMap,\n} from './source-map';\n\nexport type FiberSourceLocation = {\n fileName: string;\n lineNumber: number;\n columnNumber: number | null;\n};\n\nexport type StackFrame = {\n url: string;\n line: number;\n column: number;\n function?: string;\n};\n\nexport type SourceFiber = {\n type?: unknown;\n _debugSource?: unknown;\n _debugStack?: unknown;\n};\n\nexport type FetchModule = (url: string) => Promise<string | null>;\n\nexport type CallSiteResolver = {\n locate(frames: readonly StackFrame[]): Promise<FiberSourceLocation | null>;\n readonly stats: { modules: number; sites: number; located: number };\n};\n\ntype ResolvedMaps = {\n maps: readonly SourceMap[];\n sourceBaseUrl: string;\n};\n\nconst V8_FRAME = /^\\s*at\\s+(?:(.+?)\\s+\\()?(.+?):(\\d+):(\\d+)\\)?$/;\nconst AT_FRAME = /^\\s*(?:(.*?)@)(.+?):(\\d+):(\\d+)$/;\nconst MAX_CALL_SITE_FRAMES = 4;\n\nconst resolvedFiberSources = new WeakMap<object, FiberSourceLocation | null>();\nconst pendingFiberSources = new WeakMap<object, Promise<FiberSourceLocation | null>>();\nlet defaultResolver: CallSiteResolver | null = null;\n\nexport function parseStackFrames(stack: string): StackFrame[] {\n const frames: StackFrame[] = [];\n\n for (const raw of stack.split('\\n')) {\n const line = raw.trimEnd();\n if (line === '' || line.includes('(eval at ')) continue;\n const match = V8_FRAME.exec(line) ?? AT_FRAME.exec(line);\n if (!match) continue;\n\n const [, name, url = '', lineDigits = '', columnDigits = ''] = match;\n if (url === '' || url.startsWith('data:')) continue;\n const at = Number(lineDigits);\n const column = Number(columnDigits);\n if (!Number.isFinite(at) || !Number.isFinite(column) || at < 1) continue;\n frames.push({\n url,\n line: at,\n column,\n ...(name !== undefined && name !== '' ? { function: name } : {}),\n });\n }\n\n return frames;\n}\n\nexport function isVendorPath(path: string): boolean {\n return (\n path.includes('node_modules') ||\n path.includes('/.vite/deps/') ||\n path.includes('/.yarn/')\n );\n}\n\nfunction rawLocation(frame: StackFrame): FiberSourceLocation {\n return { fileName: frame.url, lineNumber: frame.line, columnNumber: frame.column };\n}\n\nfunction locationFromOriginal(original: OriginalPosition): FiberSourceLocation {\n return { fileName: original.source, lineNumber: original.line, columnNumber: original.column };\n}\n\nexport function writerLocationOf(\n frames: readonly StackFrame[],\n originalFor: (frame: StackFrame) => OriginalPosition | null,\n): FiberSourceLocation | null {\n for (const frame of frames) {\n if (isVendorPath(frame.url)) continue;\n const original = originalFor(frame);\n if (original === null) return rawLocation(frame);\n if (isVendorPath(original.source)) continue;\n return locationFromOriginal(original);\n }\n return null;\n}\n\nfunction parseUrl(url: string, base?: string): URL | null {\n try {\n return new URL(url, base);\n } catch {\n return null;\n }\n}\n\nfunction sourcePath(source: string, moduleUrl: string): string {\n if (/^\\/(?:Users|home|private|Volumes)\\//.test(source)) return source;\n const direct = parseUrl(source);\n if (direct?.protocol === 'file:') return decodeURIComponent(direct.pathname);\n const resolved = parseUrl(source, moduleUrl)?.href ?? source;\n const parsed = parseUrl(resolved);\n return parsed ? parsed.pathname.replace(/^\\/+/, '') : resolved;\n}\n\nexport function createCallSiteResolver(fetchModule: FetchModule): CallSiteResolver {\n const maps = new Map<string, Promise<ResolvedMaps | null>>();\n const sites = new Map<string, Promise<FiberSourceLocation | null>>();\n const stats = { modules: 0, sites: 0, located: 0 };\n\n async function mapsFor(url: string): Promise<ResolvedMaps | null> {\n const cached = maps.get(url);\n if (cached) return cached;\n\n const pending = (async () => {\n stats.modules += 1;\n const code = await fetchModule(url);\n if (code === null) return null;\n const annotation = sourceMappingUrlOf(code);\n if (annotation === null) return null;\n const inline = inlineSourceMapOf(annotation);\n if (inline !== null) return { maps: parseSourceMap(inline), sourceBaseUrl: url };\n const mapUrl = parseUrl(annotation, url)?.href ?? null;\n const text = mapUrl === null ? null : await fetchModule(mapUrl);\n return text === null || mapUrl === null\n ? null\n : { maps: parseSourceMap(text), sourceBaseUrl: mapUrl };\n })();\n\n maps.set(url, pending);\n return pending;\n }\n\n async function locateFrame(frame: StackFrame): Promise<FiberSourceLocation | null> {\n const parsed = await mapsFor(frame.url);\n if (!parsed || parsed.maps.length === 0) return null;\n const original = originalPositionFor(parsed.maps, frame.line, frame.column);\n if (!original) return null;\n return {\n fileName: sourcePath(original.source, parsed.sourceBaseUrl),\n lineNumber: original.line,\n columnNumber: original.column,\n };\n }\n\n async function locate(frames: readonly StackFrame[]): Promise<FiberSourceLocation | null> {\n for (const frame of frames) {\n if (isVendorPath(frame.url)) continue;\n const key = `${frame.url}:${frame.line}:${frame.column}`;\n let answer = sites.get(key);\n if (!answer) {\n stats.sites += 1;\n answer = locateFrame(frame);\n sites.set(key, answer);\n }\n\n const located = await answer;\n const chosen = located === null\n ? writerLocationOf([frame], () => null)\n : isVendorPath(located.fileName) ? null : located;\n if (chosen) {\n stats.located += 1;\n return chosen;\n }\n }\n return null;\n }\n\n return { locate, stats };\n}\n\nfunction debugSourceOf(value: unknown): FiberSourceLocation | null {\n if (!value || typeof value !== 'object') return null;\n const source = value as { fileName?: unknown; lineNumber?: unknown; columnNumber?: unknown };\n if (typeof source.fileName !== 'string' || typeof source.lineNumber !== 'number') return null;\n return {\n fileName: source.fileName,\n lineNumber: source.lineNumber,\n columnNumber: typeof source.columnNumber === 'number' ? source.columnNumber : null,\n };\n}\n\nfunction recordedSource(fiber: SourceFiber): FiberSourceLocation | null {\n const direct = debugSourceOf(fiber._debugSource);\n if (direct) return direct;\n const type = fiber.type;\n return type && (typeof type === 'object' || typeof type === 'function')\n ? debugSourceOf((type as { _debugSource?: unknown })._debugSource)\n : null;\n}\n\nexport function getFiberStackFrames(fiber: SourceFiber | null | undefined): StackFrame[] {\n if (!fiber || !fiber._debugStack || typeof fiber._debugStack !== 'object') return [];\n const stack = (fiber._debugStack as { stack?: unknown }).stack;\n if (typeof stack !== 'string' || stack === '') return [];\n const candidates: StackFrame[] = [];\n for (const frame of parseStackFrames(stack)) {\n if (isVendorPath(frame.url)) continue;\n candidates.push(frame);\n if (candidates.length >= MAX_CALL_SITE_FRAMES) break;\n }\n return candidates;\n}\n\n/** Stable synchronous source, before any async source-map upgrade. */\nexport function getFiberSourceCandidate(fiber: SourceFiber | null | undefined): FiberSourceLocation | null {\n if (!fiber) return null;\n const recorded = recordedSource(fiber);\n if (recorded) return recorded;\n return writerLocationOf(getFiberStackFrames(fiber), () => null);\n}\n\n/** Synchronous source read used by fiber walks and snapshots. */\nexport function getFiberSource(fiber: SourceFiber | null | undefined): FiberSourceLocation | null {\n if (!fiber) return null;\n const recorded = recordedSource(fiber);\n if (recorded) return recorded;\n const cached = resolvedFiberSources.get(fiber as object);\n if (cached !== undefined) return cached;\n return getFiberSourceCandidate(fiber);\n}\n\nasync function fetchModuleFromPage(url: string): Promise<string | null> {\n const fetchFn = (globalThis as { fetch?: typeof fetch }).fetch;\n if (!fetchFn) return null;\n const controller = typeof AbortController === 'function' ? new AbortController() : null;\n const timeout = controller ? setTimeout(() => controller.abort(), 2000) : null;\n try {\n const response = await fetchFn(url, {\n credentials: 'same-origin',\n signal: controller?.signal,\n });\n return response.ok ? await response.text() : null;\n } catch {\n return null;\n } finally {\n if (timeout !== null) clearTimeout(timeout);\n }\n}\n\nfunction pageResolver(): CallSiteResolver {\n if (!defaultResolver) defaultResolver = createCallSiteResolver(fetchModuleFromPage);\n return defaultResolver;\n}\n\n/** Resolve a React 19 stack frame through the module's inline or external map. */\nexport function resolveFiberSource(\n fiber: SourceFiber | null | undefined,\n resolver: CallSiteResolver = pageResolver(),\n): Promise<FiberSourceLocation | null> {\n if (!fiber) return Promise.resolve(null);\n const recorded = recordedSource(fiber);\n if (recorded) return Promise.resolve(recorded);\n\n const cached = pendingFiberSources.get(fiber as object);\n if (cached) return cached;\n const frames = getFiberStackFrames(fiber);\n const fallback = writerLocationOf(frames, () => null);\n const pending = resolver.locate(frames)\n .catch(() => null)\n .then((source) => {\n const located = source ?? fallback;\n resolvedFiberSources.set(fiber as object, located);\n return located;\n });\n pendingFiberSources.set(fiber as object, pending);\n return pending;\n}\n", "/**\n * Pure fiber-walk helpers extracted from layout-lab.js.\n * No DOM writes, no side effects \u2014 read-only introspection of React fiber trees.\n */\n\nimport {\n getFiberSource as readFiberSource,\n resolveFiberSource as resolveSourceLocation,\n} from '../chippy-bridge/fiber-source';\n\n/**\n * Minimal duck-typed Fiber shape.\n * Only fields used by these helpers are typed; rest may exist but are not read.\n */\nexport type Fiber = {\n type?: unknown;\n elementType?: unknown;\n key?: string | null;\n memoizedState?: unknown;\n memoizedProps?: Record<string, unknown> | null;\n stateNode?: Element | null;\n return?: Fiber | null;\n child?: Fiber | null;\n sibling?: Fiber | null;\n _debugOwner?: Fiber | null;\n _debugSource?: SourceLocation | null;\n _debugStack?: unknown;\n};\n\nexport type SourceLocation = {\n fileName?: string | null;\n lineNumber?: number | null;\n columnNumber?: number | null;\n};\n\nexport type DescribedSource = {\n fileName: string | null;\n lineNumber: number | null;\n columnNumber: number | null;\n};\n\nexport type HookSummary = {\n hookCount: number;\n hookCountTruncated: boolean;\n hookCountMax: number;\n};\n\nexport type OwnerEntry = {\n name: string | null;\n key: string | null;\n props: string[];\n} & HookSummary & {\n source: DescribedSource | null;\n};\n\nexport type FiberTarget = {\n domTag: string | null;\n domText: string;\n fiberType: string | null;\n source: DescribedSource | null;\n};\n\nexport type ComponentDescription = {\n name: string | null;\n key: string | null;\n props: Record<string, unknown>;\n source: DescribedSource | null;\n stateNode: string | null;\n owners: OwnerEntry[];\n parents: OwnerEntry[];\n} & HookSummary;\n\n/**\n * Find the React fiber key in a DOM node's own properties.\n * Returns the property name (e.g. '__reactFiber$abc123') or null.\n */\nexport function getFiberKey(node: Node | Element | null | undefined): string | null {\n if (!node) return null;\n const keys = Object.getOwnPropertyNames(node as object);\n const key = keys.find((k) => k.startsWith('__reactFiber$'));\n return key || null;\n}\n\n/**\n * Get the React fiber attached to a DOM node, or null if not a React host node.\n */\nexport function getFiber(node: Node | Element | null | undefined): Fiber | null {\n if (!node) return null;\n const key = getFiberKey(node);\n // React stores the fiber as an expando property on the host node.\n return key ? ((node as unknown as Record<string, unknown>)[key] as Fiber | null) : null;\n}\n\n/**\n * Extract display name from a React type (class, function, or string).\n * Returns displayName > name > null for functions; 'string' for primitives.\n */\nexport function getDisplayName(type: unknown): string | null {\n if (!type) return null;\n if (typeof type === 'string') return type;\n const obj = type as { displayName?: unknown; name?: unknown };\n return (obj.displayName as string) || (obj.name as string) || null;\n}\n\n/**\n * True if fiber is a user-defined component (not a DOM string, not a hook-internal wrapper).\n */\nexport function isUserComponent(fiber: Fiber | null | undefined): boolean {\n if (!fiber) return false;\n const name = getDisplayName(fiber.type);\n if (!name || typeof fiber.type === 'string') return false;\n if (['hookified', 'unboundStoryFn', 'Anonymous'].includes(name)) return false;\n return true;\n}\n\n/**\n * Walk DOM parents to find the nearest fiber-attached element.\n */\nexport function getNearestFiber(domNode: Node | Element | null | undefined): Fiber | null {\n let current = domNode as Element | null;\n while (current) {\n const fiber = getFiber(current);\n if (fiber) return fiber;\n current = (current as Element).parentElement;\n }\n return null;\n}\n\n/**\n * Walk DOM parents, then walk fiber return chain, to find the nearest user component.\n */\nexport function getNearestComponent(domNode: Node | Element | null | undefined): Fiber | null {\n let fiber = getNearestFiber(domNode);\n while (fiber) {\n if (isUserComponent(fiber)) return fiber;\n fiber = fiber.return || null;\n }\n return null;\n}\n\n/**\n * Count hooks in fiber.memoizedState linked list; detect cycle.\n */\nexport function getHookSummary(fiber: Fiber | null | undefined): HookSummary {\n const MAX_HOOKS = 500;\n const seen = new Set<unknown>();\n let hookNode = fiber?.memoizedState || null;\n let hookCount = 0;\n let hookCountTruncated = false;\n\n while (hookNode) {\n if (seen.has(hookNode)) break;\n if (hookCount >= MAX_HOOKS) {\n hookCountTruncated = true;\n break;\n }\n seen.add(hookNode);\n hookCount += 1;\n hookNode = (hookNode as { next?: unknown }).next || null;\n }\n\n return { hookCount, hookCountTruncated, hookCountMax: MAX_HOOKS };\n}\n\n/**\n * Walk fiber._debugOwner chain; collect all user components with metadata.\n * Returns empty array if fiber or owner chain is null.\n */\nexport function getOwnerChain(fiber: Fiber | null | undefined): OwnerEntry[] {\n const chain: OwnerEntry[] = [];\n let current = fiber?._debugOwner || null;\n while (current) {\n if (isUserComponent(current)) {\n chain.push({\n name: getDisplayName(current.type),\n key: current.key || null,\n props: current.memoizedProps ? Object.keys(current.memoizedProps) : [],\n ...getHookSummary(current),\n source: describeFiberSource(current),\n });\n }\n current = current._debugOwner || null;\n }\n return chain;\n}\n\n/**\n * Walk fiber._debugOwner chain; return raw fiber nodes (for navigation).\n */\nexport function getOwnerFibers(fiber: Fiber | null | undefined): Fiber[] {\n const fibers: Fiber[] = [];\n let current = fiber?._debugOwner || null;\n while (current) {\n if (isUserComponent(current)) fibers.push(current);\n current = current._debugOwner || null;\n }\n return fibers;\n}\n\n/**\n * Recursively walk fiber.child/sibling to find the first host DOM element rendered by this subtree.\n */\nexport function getFiberDomNode(fiber: Fiber | null | undefined): Element | null {\n if (!fiber) return null;\n // Host fiber \u2014 stateNode is a DOM element\n if (fiber.stateNode instanceof Element) return fiber.stateNode;\n // Walk children\n let child = fiber.child || null;\n while (child) {\n const found = getFiberDomNode(child);\n if (found) return found;\n child = child.sibling || null;\n }\n return null;\n}\n\n/**\n * Walk fiber.return chain; collect all user component parents with metadata.\n */\nexport function getReturnComponentChain(fiber: Fiber | null | undefined): OwnerEntry[] {\n const chain: OwnerEntry[] = [];\n let current = fiber?.return || null;\n while (current) {\n if (isUserComponent(current)) {\n chain.push({\n name: getDisplayName(current.type),\n key: current.key || null,\n props: current.memoizedProps ? Object.keys(current.memoizedProps) : [],\n ...getHookSummary(current),\n source: describeFiberSource(current),\n });\n }\n current = current.return || null;\n }\n return chain;\n}\n\n/**\n * Walk fiber.return chain; return raw fiber nodes (for navigation).\n */\nexport function getReturnComponentFibers(fiber: Fiber | null | undefined): Fiber[] {\n const fibers: Fiber[] = [];\n let current = fiber?.return || null;\n while (current) {\n if (isUserComponent(current)) fibers.push(current);\n current = current.return || null;\n }\n return fibers;\n}\n\n/**\n * Normalize source location object to canonical shape.\n */\nexport function describeSource(source: SourceLocation | null | undefined): DescribedSource | null {\n if (!source) return null;\n return {\n fileName: source.fileName || null,\n lineNumber: source.lineNumber || null,\n columnNumber: source.columnNumber || null,\n };\n}\n\n/**\n * Read a fiber source across React versions.\n *\n * React <=18 exposes `_debugSource`. React 19 exposes `_debugStack`; its first\n * non-vendor frame is returned synchronously and may later be upgraded from the\n * bundle position to the original source position by `resolveFiberSource`.\n */\nexport function describeFiberSource(fiber: Fiber | null | undefined): DescribedSource | null {\n return describeSource(readFiberSource(fiber));\n}\n\nexport async function resolveFiberSource(fiber: Fiber | null | undefined): Promise<DescribedSource | null> {\n return describeSource(await resolveSourceLocation(fiber));\n}\n\n/**\n * Describe a fiber + DOM node pair; used for inspection UI.\n */\nexport function describeFiberTarget(fiber: Fiber | null | undefined, domNode: Element | null | undefined): FiberTarget | null {\n if (!fiber && !domNode) return null;\n return {\n domTag: domNode?.tagName?.toLowerCase() || null,\n domText: domNode?.textContent?.trim().slice(0, 60) || '',\n fiberType: getDisplayName(fiber?.type) || getDisplayName(fiber?.elementType) || null,\n source: describeFiberSource(fiber),\n };\n}\n\n/**\n * Full component description: name, props, hook summary, source, owners, parents.\n */\nexport function describeComponent(fiber: Fiber | null | undefined): ComponentDescription | null {\n if (!fiber) return null;\n return {\n name: getDisplayName(fiber.type),\n key: fiber.key || null,\n props: fiber.memoizedProps || {},\n ...getHookSummary(fiber),\n source: describeFiberSource(fiber),\n stateNode: fiber.stateNode instanceof Element ? fiber.stateNode.tagName.toLowerCase() : null,\n owners: getOwnerChain(fiber),\n parents: getReturnComponentChain(fiber),\n };\n}\n", "// React fiber constants and structural comparisons used by the commit detector.\n\nimport { getFiberSourceCandidate } from '../chippy-bridge/fiber-source';\n\nexport type ComponentType =\n | Function\n | symbol\n | {\n // ForwardRef/memo/etc may be objects.\n $$typeof?: symbol;\n render?: Function;\n displayName?: string;\n name?: string;\n };\n\ninterface StoreCheck {\n getSnapshot: () => unknown;\n value: unknown;\n}\n\nexport interface Fiber {\n tag: number;\n type: ComponentType;\n alternate: Fiber | null;\n return: Fiber | null;\n child: Fiber | null;\n sibling: Fiber | null;\n flags: number;\n subtreeFlags: number;\n lanes: number;\n memoizedState: Hook | null;\n memoizedProps?: unknown;\n pendingProps?: unknown;\n updateQueue: { stores: StoreCheck[] | null } | null;\n /**\n * React populates this array during the render phase with fibers that will be\n * deleted in the commit phase. Present when subtreeFlags & ChildDeletion !== 0.\n * Only available in React 18+ (replaces the older effectList pattern).\n */\n deletions?: Fiber[] | null;\n\n // Dev-only / optional debug fields (not always present).\n elementType?: unknown;\n _debugSource?: { fileName: string; lineNumber: number };\n _debugStack?: unknown;\n _debugHookTypes?: string[];\n}\n\nexport interface Hook {\n memoizedState: unknown;\n baseState: unknown;\n baseQueue: unknown;\n queue: HookQueue | null;\n next: Hook | null;\n}\n\ninterface HookQueue {\n dispatch?: Function;\n value?: unknown;\n getSnapshot?: Function;\n lastRenderedState?: unknown;\n}\n\nexport interface DepOrigin {\n type: ComponentType;\n hookIndex: number;\n depth: number;\n hookType: 'useCallback' | 'useMemo';\n}\n\nexport interface ThrashInfo {\n hookIndex: number;\n hookType: 'effect' | 'callback' | 'memo';\n deps: unknown[];\n changedDepIndices: number[];\n changedDepOrigins: (DepOrigin | null)[];\n effectTag?: number;\n memoValue?: unknown;\n /** The hook body function (for useCallback) or memoized value (for useMemo) */\n hookBody?: Function | unknown;\n}\n\nexport type ChangedHook =\n | {\n prevValue: unknown;\n currentValue: unknown;\n info?: string; // For structural changes\n }\n | 'repeat'; // Entire object replaced with string if identical to previous commit\n\nexport interface StateChangeResult {\n hasChange: boolean;\n actionable: boolean;\n changedIndices: number[];\n changedHooks: ChangedHook[];\n /** True if any of the changes came from useSyncExternalStore */\n hasExternalStoreChange: boolean;\n}\n\nexport interface ThrashingResult {\n effects: ThrashInfo[];\n callbacks: ThrashInfo[];\n memos: ThrashInfo[];\n}\n\n// React fiber flags (React 18.3.1)\nexport const PerformedWork = 1;\n\n// Flags relevant to filtering out non-preventable work from collateral counts.\n// - Placement: mount/insert work (initial render)\n// - Hydrating: work done while hydrating server-rendered markup\n// These are stable for React 18.x.\nexport const Placement = 2;\nexport const Hydrating = 4096;\n\n// Special sentinel values for hook change detection\n\n// Component fiber tags for React 18.3.1 (stable within 18.x)\n// 0: FunctionComponent, 1: ClassComponent, 2: IndeterminateComponent\n// 11: MemoComponent, 14: SimpleMemoComponent, 15: ForwardRef\nconst COMPONENT_TAGS = new Set([0, 1, 2, 11, 14, 15]);\n\n// Context Provider tag\nconst CONTEXT_PROVIDER_TAG = 10;\n\n// Suspense fiber tag (React 18.3.1 \u2014 stable within 18.x)\nexport const SuspenseTag = 13;\n\n// ChildDeletion \u2014 set on fiber.flags (NOT subtreeFlags) when the fiber has deletions[].\n// Value 16 is stable for React 18.3.1.\nexport const ChildDeletion = 16;\n\n// Update \u2014 set on fiber.flags when the fiber received an update this commit.\n// This is the flag React puts on Suspense fibers when they transition state.\n// Value 4 is stable for React 18.3.1.\nexport const Update = 4;\n\n// DidCapture \u2014 render-phase flag, cleared before commit. Do NOT check at commit time.\n// Kept for documentation. Value 128 in React 18.3.1.\nexport const DidCapture = 128;\n\nexport function isComponentFiber(tag: number): boolean {\n return COMPONENT_TAGS.has(tag);\n}\n\nexport function isContextProviderFiber(tag: number): boolean {\n return tag === CONTEXT_PROVIDER_TAG;\n}\n\nexport function isSuspenseFiber(tag: number): boolean {\n return tag === SuspenseTag;\n}\n\n/**\n * Detect whether a Suspense boundary transitioned between suspended/unsuspended states.\n *\n * Suspense memoizedState is:\n * null \u2192 primary content is showing (not suspended)\n * non-null (object) \u2192 boundary is showing the fallback (suspended/dehydrated)\n *\n * Returns:\n * { entered: true } \u2014 boundary just entered fallback\n * - null \u2192 non-null (update path)\n * - no alternate + memoizedState non-null (first render directly to fallback)\n * { left: true } \u2014 boundary just left fallback / revealed primary (non-null \u2192 null)\n * null \u2014 no state change (both null, both non-null, or first render not suspended)\n *\n * A Suspense that renders without delay never enters fallback, so memoizedState stays\n * null \u2192 returns null \u2192 correctly ignored by the detector.\n */\nexport function didSuspenseStateChange(\n fiber: Fiber\n): { entered: boolean; left: boolean } | null {\n const currSuspended = fiber.memoizedState !== null;\n\n if (!fiber.alternate) {\n // First render of this Suspense boundary.\n // If it went directly to fallback (memoizedState non-null), that IS an enter.\n // If it rendered primary content (memoizedState null), no transition.\n if (currSuspended) {\n return { entered: true, left: false };\n }\n return null;\n }\n\n const prevSuspended = fiber.alternate.memoizedState !== null;\n if (prevSuspended === currSuspended) return null;\n return { entered: currSuspended, left: prevSuspended };\n}\n\n/**\n * Check if a Context Provider's value changed.\n */\nexport function didContextValueChange(fiber: Fiber): boolean {\n if (fiber.tag !== CONTEXT_PROVIDER_TAG || !fiber.alternate) return false;\n\n type FiberWithProps = Fiber & { memoizedProps?: { value?: unknown } };\n const alternateFiber: FiberWithProps = fiber.alternate;\n const currentFiber: FiberWithProps = fiber;\n\n const prevProps = alternateFiber.memoizedProps;\n const currentProps = currentFiber.memoizedProps;\n\n return prevProps && currentProps && !Object.is(prevProps.value, currentProps.value);\n}\n\n// Detect React built-in fiber types (Suspense/Offscreen/etc).\n// IMPORTANT: Do NOT use component names. User components can be named anything.\n// In React 18, built-ins like Suspense/Offscreen are represented as symbols.\nexport function isReactInternalComponentType(type: ComponentType): boolean {\n return typeof type === 'symbol';\n}\n\nexport function getComponentName(fiber: Fiber): string {\n type TypeWithName = {\n displayName?: string;\n name?: string;\n };\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n const type = fiber.type as TypeWithName;\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n const elementType = fiber.elementType as TypeWithName;\n const named = (\n type?.displayName ||\n type?.name ||\n elementType?.displayName ||\n elementType?.name\n );\n if (named) return named;\n const source = getFiberSourceCandidate(fiber);\n return (source && `<${source.fileName}:${source.lineNumber}>`) || 'Anonymous';\n}\n\n/**\n * Check if hook is a state hook (useState/useReducer/useSyncExternalStore).\n *\n * All hooks share a base shape. The reliable differentiators:\n * - useState/useReducer: queue.dispatch is a function\n * - useSyncExternalStore: queue has 'value' and 'getSnapshot' properties\n */\nexport function isStateHook(hook: Hook): boolean {\n const queue = hook.queue;\n if (!queue) return false;\n\n return typeof queue.dispatch === 'function' || ('value' in queue && 'getSnapshot' in queue);\n}\n\n/** True if the hook is a useSyncExternalStore-like hook. */\nexport function isExternalStoreHook(hook: Hook): boolean {\n const queue = hook.queue;\n return (\n queue && typeof queue.dispatch !== 'function' && 'value' in queue && 'getSnapshot' in queue\n );\n}\n\n/**\n * Compare hook chains to detect state changes.\n *\n * IMPORTANT: Hook indices are React internal memoizedState indices, NOT code order.\n * `useContext` hooks are not in memoizedState, causing index misalignment.\n */\nexport function detectHookStateChanges(\n prevHookChain: Hook | null,\n currentHookChain: Hook | null\n): StateChangeResult {\n if (prevHookChain === currentHookChain) {\n return {\n hasChange: false,\n actionable: false,\n changedIndices: [],\n changedHooks: [],\n hasExternalStoreChange: false,\n };\n }\n\n let prevHook = prevHookChain;\n let currentHook = currentHookChain;\n let memoizedStateIndex = 0;\n const changedIndices: number[] = [];\n const changedHooks: ChangedHook[] = [];\n let hasExternalStoreChange = false;\n\n while (prevHook && currentHook) {\n if (isStateHook(currentHook)) {\n const prevValueRaw =\n prevHook.memoizedState === undefined\n ? prevHook.queue?.lastRenderedState\n : prevHook.memoizedState;\n const curValueRaw =\n currentHook.memoizedState === undefined\n ? currentHook.queue?.lastRenderedState\n : currentHook.memoizedState;\n\n if (!Object.is(prevValueRaw, curValueRaw)) {\n const isExternal = isExternalStoreHook(currentHook);\n if (isExternal) hasExternalStoreChange = true;\n\n changedIndices.push(memoizedStateIndex);\n changedHooks.push({\n prevValue: prevValueRaw,\n currentValue: curValueRaw,\n ...(isExternal && { isExternalStore: true }),\n });\n }\n }\n\n prevHook = prevHook.next;\n currentHook = currentHook.next;\n memoizedStateIndex++;\n }\n\n return {\n hasChange: changedIndices.length > 0,\n actionable: changedIndices.length > 0,\n changedIndices,\n changedHooks,\n hasExternalStoreChange,\n };\n}\n\n/**\n * Detect thrashing for hooks with deps: useEffect/useLayoutEffect/useInsertionEffect,\n * useCallback and useMemo.\n */\nexport function detectHookThrashing(\n prevHookChain: Hook | null,\n currentHookChain: Hook | null,\n mode: 'non-primitive' | 'all' = 'non-primitive',\n valueProvenance: WeakMap<object | Function, DepOrigin> | null = null\n): ThrashingResult {\n if (prevHookChain === currentHookChain) {\n return { effects: [], callbacks: [], memos: [] };\n }\n\n const thrashedEffects: ThrashInfo[] = [];\n const thrashedCallbacks: ThrashInfo[] = [];\n const thrashedMemos: ThrashInfo[] = [];\n\n let prevHook = prevHookChain;\n let currentHook = currentHookChain;\n let hookIndex = 0;\n\n while (prevHook && currentHook) {\n if (!isStateHook(currentHook)) {\n const prevValue = prevHook.memoizedState;\n const currentValue = currentHook.memoizedState;\n\n let prevDeps: unknown[] | null = null;\n let currentDeps: unknown[] | null = null;\n let hookType: ThrashInfo['hookType'] | null = null;\n\n // Effect hooks store an object with `deps`.\n if (\n prevValue &&\n currentValue &&\n typeof prevValue === 'object' &&\n typeof currentValue === 'object' &&\n 'deps' in prevValue &&\n 'deps' in currentValue &&\n 'tag' in currentValue\n ) {\n type EffectHook = { deps: unknown[] };\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n prevDeps = (prevValue as EffectHook).deps;\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n currentDeps = (currentValue as EffectHook).deps;\n hookType = 'effect';\n }\n // useMemo/useCallback store [result, deps].\n else if (\n Array.isArray(prevValue) &&\n Array.isArray(currentValue) &&\n prevValue.length === 2 &&\n currentValue.length === 2 &&\n Array.isArray(prevValue[1]) &&\n Array.isArray(currentValue[1])\n ) {\n prevDeps = prevValue[1];\n currentDeps = currentValue[1];\n hookType = typeof currentValue[0] === 'function' ? 'callback' : 'memo';\n }\n\n if (hookType && Array.isArray(prevDeps) && Array.isArray(currentDeps)) {\n const changedDepIndices: number[] = [];\n const changedDepOrigins: (DepOrigin | null)[] = [];\n\n // eslint-disable-next-line @atlassian/perf-linting/no-quadratic-complexity\n for (let j = 0; j < currentDeps.length; j++) {\n if (!Object.is(prevDeps[j], currentDeps[j])) {\n changedDepIndices.push(j);\n\n let origin: DepOrigin | null = null;\n const dep = currentDeps[j];\n if (\n valueProvenance &&\n dep != null &&\n (typeof dep === 'object' || typeof dep === 'function')\n ) {\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n origin = valueProvenance.get(dep as object | ((...args: never[]) => unknown)) || null;\n }\n changedDepOrigins.push(origin);\n }\n }\n\n let shouldReport = changedDepIndices.length > 0;\n if (shouldReport && mode === 'non-primitive') {\n // Only report if at least one non-primitive dep changed.\n // eslint-disable-next-line @atlassian/perf-linting/no-quadratic-complexity\n shouldReport = changedDepIndices.some((idx) => {\n const dep = currentDeps[idx];\n const t = typeof dep;\n return !(\n dep == null ||\n t === 'string' ||\n t === 'number' ||\n t === 'boolean' ||\n t === 'symbol' ||\n t === 'bigint'\n );\n });\n }\n\n if (shouldReport) {\n const thrashInfo: ThrashInfo = {\n hookIndex,\n hookType,\n deps: currentDeps,\n changedDepIndices,\n changedDepOrigins,\n };\n\n if (hookType === 'effect') {\n type EffectValue = { tag?: number; create?: Function };\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n const effectValue = currentValue as EffectValue;\n thrashInfo.effectTag = effectValue.tag;\n // Capture the effect function itself (the create function)\n if (effectValue.create) {\n thrashInfo.hookBody = effectValue.create;\n }\n thrashedEffects.push(thrashInfo);\n } else if (hookType === 'callback') {\n type CallbackValue = [Function, unknown[]];\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n const callbackFn = (currentValue as CallbackValue)[0];\n // Capture the callback function itself\n thrashInfo.hookBody = callbackFn;\n thrashedCallbacks.push(thrashInfo);\n } else {\n type MemoValue = [unknown, unknown[]];\n // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n const memoValue = (currentValue as MemoValue)[0];\n thrashInfo.memoValue = memoValue;\n // For useMemo, also expose the memoized value as hookBody\n thrashInfo.hookBody = memoValue;\n thrashedMemos.push(thrashInfo);\n }\n }\n }\n }\n\n prevHook = prevHook.next;\n currentHook = currentHook.next;\n hookIndex++;\n }\n\n return {\n effects: thrashedEffects,\n callbacks: thrashedCallbacks,\n memos: thrashedMemos,\n };\n}\n\nexport function didFiberUpdate(fiber: Fiber): boolean {\n // eslint-disable-next-line no-bitwise\n return (fiber.flags & PerformedWork) !== 0;\n}\n\n/**\n * Detect if fiber has actionable hook state changes by comparing with fiber.alternate.\n *\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2502 \u26A0\uFE0F WARNING: fiber.alternate COMPARISON IS UNRELIABLE \u2502\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n *\n * This function compares fiber.memoizedState vs fiber.alternate.memoizedState.\n *\n * THE PROBLEM:\n * fiber.alternate is NOT always \"state from the previous commit\".\n * It's \"state from last time this fiber was used as work-in-progress\".\n *\n * If a component re-renders due to parent/context changes (not self-update),\n * its alternate may be stale - from 1 commit ago, or 100 commits ago!\n *\n * This causes FALSE POSITIVES: we detect \"state changed\" when it didn't\n * change in THIS commit - it changed in some PAST commit.\n *\n * WHEN THIS FUNCTION IS RELIABLE:\n * \u2713 Component called setState (it IS the initiator)\n * \u2713 Component has useSyncExternalStore that just triggered\n * \u2713 Used alongside memoizedUpdaters validation\n *\n * WHEN THIS FUNCTION IS UNRELIABLE:\n * \u2717 Component re-rendered due to parent update\n * \u2717 Component re-rendered due to context change\n * \u2717 Component was memoized and skipped N commits\n * \u2717 Any scenario where alternate wasn't recently updated\n *\n * RECOMMENDED USAGE:\n * - In development: Use memoizedUpdaters to verify component is an initiator,\n * THEN use this function to get hook change details.\n * - In production: Use lastSeenHookState tracking (see detector.tsx)\n * instead of relying on fiber.alternate.\n *\n * See detector.tsx for the full explanation of the \"swap\" problem and solution.\n */\nexport function didFiberSelfUpdate(fiber: Fiber): StateChangeResult | null {\n // Early exit: only check fibers that actually rendered (have PerformedWork flag)\n if (!didFiberUpdate(fiber)) return null;\n\n const alt = fiber.alternate;\n if (!alt) return null;\n\n const prevState = alt.memoizedState;\n const currentState = fiber.memoizedState;\n\n const result = detectHookStateChanges(prevState, currentState);\n return result.hasChange ? result : null;\n}\n\nexport function isDevelopment(): boolean {\n return process.env.NODE_ENV !== 'production';\n}\n", "/**\n * Types and helpers for writing a Chippy plugin. `chippy pack` bundles a\n * plugin module's default export into a custom skill and extension build.\n */\nimport type { ChippyPlugin } from '../plugins/index';\n\nexport type { ChippyPlugin, PluginStatus } from '../plugins/index';\nexport type {\n ContextBoundaryKind,\n ContextContribution,\n ContextField,\n ContextProvider,\n} from '../layout-lab/marker-context';\nexport type { Fiber } from '../layout-lab/fiber-walk';\nexport type {\n CommitMetricsAdapter,\n CommitMetricsDelta,\n CommitMetricsMeasurement,\n CommitMetricsRoot,\n CommitMetricsSnapshot,\n} from '../avalanche/commit-metrics-adapter';\n/** Display name of a fiber's `type`: function, class, memo, or forwardRef. */\nexport { getDisplayName } from '../layout-lab/fiber-walk';\n/** Fiber name helpers that commit metrics adapters use for hook ancestry. */\nexport { getComponentName, isComponentFiber } from '../avalanche/internals';\n\n/** Identity helper that type-checks a plugin definition. */\nexport function definePlugin(plugin: ChippyPlugin): ChippyPlugin {\n return plugin;\n}\n"],
5
+ "mappings": ";AAOA,IAAM,SAAS;AACf,IAAM,WAAW,IAAI,UAAU,GAAG,EAAE,KAAK,EAAE;AAC3C,SAAS,QAAQ,GAAG,QAAQ,OAAO,QAAQ,SAAS,GAAG;AACrD,WAAS,OAAO,WAAW,KAAK,CAAC,IAAI;AACvC;;;AC6BA,IAAM,WAAW;AACjB,IAAM,WAAW;AACjB,IAAM,uBAAuB;AAMtB,SAAS,iBAAiB,OAA6B;AAC5D,QAAM,SAAuB,CAAC;AAE9B,aAAW,OAAO,MAAM,MAAM,IAAI,GAAG;AACnC,UAAM,OAAO,IAAI,QAAQ;AACzB,QAAI,SAAS,MAAM,KAAK,SAAS,WAAW,EAAG;AAC/C,UAAM,QAAQ,SAAS,KAAK,IAAI,KAAK,SAAS,KAAK,IAAI;AACvD,QAAI,CAAC,MAAO;AAEZ,UAAM,CAAC,EAAE,MAAM,MAAM,IAAI,aAAa,IAAI,eAAe,EAAE,IAAI;AAC/D,QAAI,QAAQ,MAAM,IAAI,WAAW,OAAO,EAAG;AAC3C,UAAM,KAAK,OAAO,UAAU;AAC5B,UAAM,SAAS,OAAO,YAAY;AAClC,QAAI,CAAC,OAAO,SAAS,EAAE,KAAK,CAAC,OAAO,SAAS,MAAM,KAAK,KAAK,EAAG;AAChE,WAAO,KAAK;AAAA,MACV;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA,GAAI,SAAS,UAAa,SAAS,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AAAA,IAChE,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAEO,SAAS,aAAa,MAAuB;AAClD,SACE,KAAK,SAAS,cAAc,KAC5B,KAAK,SAAS,cAAc,KAC5B,KAAK,SAAS,SAAS;AAE3B;AAEA,SAAS,YAAY,OAAwC;AAC3D,SAAO,EAAE,UAAU,MAAM,KAAK,YAAY,MAAM,MAAM,cAAc,MAAM,OAAO;AACnF;AAEA,SAAS,qBAAqB,UAAiD;AAC7E,SAAO,EAAE,UAAU,SAAS,QAAQ,YAAY,SAAS,MAAM,cAAc,SAAS,OAAO;AAC/F;AAEO,SAAS,iBACd,QACA,aAC4B;AAC5B,aAAW,SAAS,QAAQ;AAC1B,QAAI,aAAa,MAAM,GAAG,EAAG;AAC7B,UAAM,WAAW,YAAY,KAAK;AAClC,QAAI,aAAa,KAAM,QAAO,YAAY,KAAK;AAC/C,QAAI,aAAa,SAAS,MAAM,EAAG;AACnC,WAAO,qBAAqB,QAAQ;AAAA,EACtC;AACA,SAAO;AACT;AAqFA,SAAS,cAAc,OAA4C;AACjE,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,SAAS;AACf,MAAI,OAAO,OAAO,aAAa,YAAY,OAAO,OAAO,eAAe,SAAU,QAAO;AACzF,SAAO;AAAA,IACL,UAAU,OAAO;AAAA,IACjB,YAAY,OAAO;AAAA,IACnB,cAAc,OAAO,OAAO,iBAAiB,WAAW,OAAO,eAAe;AAAA,EAChF;AACF;AAEA,SAAS,eAAe,OAAgD;AACtE,QAAM,SAAS,cAAc,MAAM,YAAY;AAC/C,MAAI,OAAQ,QAAO;AACnB,QAAM,OAAO,MAAM;AACnB,SAAO,SAAS,OAAO,SAAS,YAAY,OAAO,SAAS,cACxD,cAAe,KAAoC,YAAY,IAC/D;AACN;AAEO,SAAS,oBAAoB,OAAqD;AACvF,MAAI,CAAC,SAAS,CAAC,MAAM,eAAe,OAAO,MAAM,gBAAgB,SAAU,QAAO,CAAC;AACnF,QAAM,QAAS,MAAM,YAAoC;AACzD,MAAI,OAAO,UAAU,YAAY,UAAU,GAAI,QAAO,CAAC;AACvD,QAAM,aAA2B,CAAC;AAClC,aAAW,SAAS,iBAAiB,KAAK,GAAG;AAC3C,QAAI,aAAa,MAAM,GAAG,EAAG;AAC7B,eAAW,KAAK,KAAK;AACrB,QAAI,WAAW,UAAU,qBAAsB;AAAA,EACjD;AACA,SAAO;AACT;AAGO,SAAS,wBAAwB,OAAmE;AACzG,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,WAAW,eAAe,KAAK;AACrC,MAAI,SAAU,QAAO;AACrB,SAAO,iBAAiB,oBAAoB,KAAK,GAAG,MAAM,IAAI;AAChE;;;AChIO,SAAS,eAAe,MAA8B;AAC3D,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,QAAM,MAAM;AACZ,SAAQ,IAAI,eAA2B,IAAI,QAAmB;AAChE;;;ACkBA,IAAM,iBAAiB,oBAAI,IAAI,CAAC,GAAG,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;AAqB7C,SAAS,iBAAiB,KAAsB;AACrD,SAAO,eAAe,IAAI,GAAG;AAC/B;AAsEO,SAAS,iBAAiB,OAAsB;AAMrD,QAAM,OAAO,MAAM;AAEnB,QAAM,cAAc,MAAM;AAC1B,QAAM,QACJ,MAAM,eACN,MAAM,QACN,aAAa,eACb,aAAa;AAEf,MAAI,MAAO,QAAO;AAClB,QAAM,SAAS,wBAAwB,KAAK;AAC5C,SAAQ,UAAU,IAAI,OAAO,QAAQ,IAAI,OAAO,UAAU,OAAQ;AACpE;;;AC5MO,SAAS,aAAa,QAAoC;AAC/D,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,79 @@
1
+ // TODO: This is a deliberately lossy UI signature. Commits that differ
2
+ // only in omitted timing, rendered, ATL, or other diagnostic fields collapse
3
+ // into one "repeat" count; either expose that scope in the UI or expand the key
4
+ // before treating grouped rows as equivalent behavior.
5
+ const PATTERN_STAT_KEYS = [
6
+ 'collateral',
7
+ 'restarts',
8
+ 'performedWork',
9
+ 'effects',
10
+ 'memos',
11
+ 'callbacks',
12
+ 'contextUpdates',
13
+ 'unmountedComponents',
14
+ 'unmountedSubtrees',
15
+ ];
16
+
17
+ function count(value) {
18
+ const number = Number(value);
19
+ return Number.isFinite(number) ? number : 0;
20
+ }
21
+
22
+ function actionIdentity(row) {
23
+ const action = row?.latestAction;
24
+ return [
25
+ action?.action || '',
26
+ action?.dom?.nearestTestId || '',
27
+ action?.targetFiber?.name || '',
28
+ action?.dom?.tag || '',
29
+ ];
30
+ }
31
+
32
+ function patternKey(row) {
33
+ return JSON.stringify([
34
+ count(row?.sourceCount),
35
+ ...PATTERN_STAT_KEYS.map((key) => count(row?.stat?.[key])),
36
+ ...actionIdentity(row),
37
+ ]);
38
+ }
39
+
40
+ export function groupCommitRows(rows) {
41
+ const groups = new Map();
42
+
43
+ for (const row of rows || []) {
44
+ const key = patternKey(row);
45
+ const commitId = count(row?.commitId);
46
+ const current = groups.get(key);
47
+
48
+ if (!current) {
49
+ groups.set(key, {
50
+ key,
51
+ count: 1,
52
+ firstCommitId: commitId,
53
+ lastCommitId: commitId,
54
+ representative: row,
55
+ });
56
+ continue;
57
+ }
58
+
59
+ current.count += 1;
60
+ current.firstCommitId = Math.min(current.firstCommitId, commitId);
61
+ current.lastCommitId = Math.max(current.lastCommitId, commitId);
62
+ if (commitId >= count(current.representative?.commitId)) current.representative = row;
63
+ }
64
+
65
+ return Array.from(groups.values()).sort((a, b) => b.lastCommitId - a.lastCommitId);
66
+ }
67
+
68
+ export function describeCommitAction(row) {
69
+ const action = row?.latestAction;
70
+ if (!action?.action) return 'render';
71
+ const target = action.dom?.nearestTestId || action.targetFiber?.name || action.dom?.tag;
72
+ return target ? `${action.action} · ${target}` : action.action;
73
+ }
74
+
75
+ export function commitSpan(group) {
76
+ if (!group) return '—';
77
+ if (group.firstCommitId === group.lastCommitId) return `#${group.lastCommitId}`;
78
+ return `#${group.firstCommitId}–#${group.lastCommitId}`;
79
+ }
@@ -0,0 +1,6 @@
1
+ // Generated by scripts/sync-chippy-brand.mjs from extension/logo.svg.
2
+ import React from 'react';
3
+
4
+ export function ChippyLogo() {
5
+ return <span style={{ display: 'inline-flex' }} dangerouslySetInnerHTML={{ __html: "<svg data-chippy-logo aria-hidden=\"true\" width=\"24\" height=\"24\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\" fill=\"none\">\n <defs>\n <linearGradient id=\"chippy-trace-blue\" x1=\"4\" y1=\"25\" x2=\"28\" y2=\"7\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#1558BC\"/><stop offset=\"1\" stop-color=\"#357DE8\"/></linearGradient>\n <linearGradient id=\"chippy-trace-light\" x1=\"4\" y1=\"9\" x2=\"27\" y2=\"9\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#85B8FF\"/><stop offset=\"1\" stop-color=\"#357DE8\"/></linearGradient>\n </defs>\n <style>\n @keyframes chippy-trace-reveal { from { opacity: .45; transform: translateY(-1.5px); } to { opacity: 1; transform: translateY(0); } }\n .chippy-trace-cut { animation: chippy-trace-reveal 650ms ease-out both; }\n @media (prefers-reduced-motion: reduce) { .chippy-trace-cut { animation: none; } }\n </style>\n <path d=\"M4 9 16 1.5 28 9 21 13.4 16 10.3 11 13.4v5.2l5 3.1 5-3.1 7 4.4L16 30.5 4 23Z\" fill=\"url(#chippy-trace-blue)\"/>\n <path class=\"chippy-trace-cut\" d=\"M4 9 16 1.5 28 9 21 13.4 16 10.3 11 13.4Z\" fill=\"url(#chippy-trace-light)\"/>\n <path d=\"m11 18.6 5 3.1 5-3.1 7 4.4-12 7.5-5-3.1Z\" fill=\"#1868DB\"/>\n</svg>" }} />;
6
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+
3
+ export const CHIPPY_INSTALL_COMMAND = 'npx @atlassian/chippy install';
4
+
5
+ const unavailableConnectionStates = new Set(['failed', 'reconnecting']);
6
+
7
+ export function shouldShowChippyInstallHint(connState) {
8
+ return unavailableConnectionStates.has(connState);
9
+ }
10
+
11
+ export function ChippyConnectionFallback({ connState }) {
12
+ if (!shouldShowChippyInstallHint(connState)) return null;
13
+
14
+ return (
15
+ <div
16
+ role="status"
17
+ data-testid="chippy-install-hint"
18
+ style={{
19
+ marginBottom: 8,
20
+ padding: '10px 12px',
21
+ border: '1px solid var(--color-warning-border, #b88a00)',
22
+ borderRadius: 8,
23
+ background: 'var(--color-warning-background, color-mix(in srgb, #b88a00 12%, Canvas))',
24
+ color: 'var(--color-default-text, CanvasText)',
25
+ fontSize: 12,
26
+ lineHeight: 1.45,
27
+ }}
28
+ >
29
+ <div style={{ marginBottom: 6 }}>
30
+ Chippy’s local helper isn’t reachable. If Chippy isn’t installed for your agent, run:
31
+ </div>
32
+ <code
33
+ style={{
34
+ display: 'block',
35
+ padding: '7px 8px',
36
+ borderRadius: 6,
37
+ background: 'var(--color-elevated-surface, color-mix(in srgb, Canvas 88%, CanvasText 12%))',
38
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
39
+ overflowWrap: 'anywhere',
40
+ userSelect: 'all',
41
+ }}
42
+ >
43
+ {CHIPPY_INSTALL_COMMAND}
44
+ </code>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,15 @@
1
+ export const ADDON_ID = 'local/chippy';
2
+ export const CHIPPY_PANEL_ID = `${ADDON_ID}/chippy-panel`;
3
+ export const AVALANCHE_PANEL_ID = `${ADDON_ID}/avalanche-panel`;
4
+
5
+ export const EVENTS = {
6
+ REQUEST_STATE: `${ADDON_ID}/request-state`,
7
+ STATE: `${ADDON_ID}/state`,
8
+ COMMAND: `${ADDON_ID}/command`,
9
+ RESULT: `${ADDON_ID}/result`,
10
+ SET_MODE: `${ADDON_ID}/set-mode`,
11
+ BLOCKED: `${ADDON_ID}/blocked`,
12
+ ERROR: `${ADDON_ID}/error`,
13
+ };
14
+
15
+ export const PREVIEW_MESSAGE_SOURCE = 'local/chippy/storybook-preview-message';
@@ -0,0 +1,171 @@
1
+ import React from 'react';
2
+
3
+ const colors = {
4
+ text: 'var(--color-default-text, CanvasText)',
5
+ muted: 'var(--color-subtle-text, color-mix(in srgb, CanvasText 64%, transparent))',
6
+ border: 'var(--color-medium-border, color-mix(in srgb, CanvasText 18%, transparent))',
7
+ provider: 'var(--color-accent, #5e6c84)',
8
+ error: 'var(--color-negative-text, #ae2e24)',
9
+ suspense: 'var(--color-purple-text, #7c3aed)',
10
+ };
11
+
12
+ function DetailFields({ fields }) {
13
+ const visible = fields.filter(([, value]) => value != null && value !== '');
14
+ if (!visible.length) return null;
15
+ return (
16
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '3px 10px', marginTop: 5, color: colors.muted, fontSize: 11 }}>
17
+ {visible.map(([label, value]) => (
18
+ <span key={label}>
19
+ <span style={{ textTransform: 'lowercase' }}>{label}</span>{' '}
20
+ <strong style={{ color: colors.text }}>{String(value)}</strong>
21
+ </span>
22
+ ))}
23
+ </div>
24
+ );
25
+ }
26
+
27
+ function DistanceBadge({ distance }) {
28
+ if (distance == null) return null;
29
+ const hops = distance === 1 ? '1 hop' : `${distance} hops`;
30
+ return (
31
+ <span
32
+ data-testid="chippy-boundary-distance"
33
+ style={{
34
+ marginLeft: 'auto',
35
+ padding: '1px 6px',
36
+ borderRadius: 999,
37
+ background: 'color-mix(in srgb, CanvasText 12%, transparent)',
38
+ color: colors.muted,
39
+ fontSize: 10,
40
+ fontWeight: 600,
41
+ flexShrink: 0,
42
+ }}
43
+ >
44
+ {hops}
45
+ </span>
46
+ );
47
+ }
48
+
49
+ // A single declarative boundary block. Clickable: reports the boundary + its
50
+ // fiber-hop distance to the Chippy backend so it can be investigated later.
51
+ // This is NOT the React render owner -- it is a collateral-scope boundary.
52
+ function BoundaryBlock({ testId, kind, label, title, accent, distance, fields, onInspect, selected, onSelect }) {
53
+ const clickable = typeof onInspect === 'function';
54
+ const activate = clickable
55
+ ? () => {
56
+ if (onSelect) onSelect(kind);
57
+ onInspect(kind);
58
+ }
59
+ : undefined;
60
+ return (
61
+ <div
62
+ data-testid={testId}
63
+ data-chippy-boundary={kind}
64
+ role={clickable ? 'button' : undefined}
65
+ tabIndex={clickable ? 0 : undefined}
66
+ onClick={activate}
67
+ onKeyDown={
68
+ clickable
69
+ ? (e) => {
70
+ if (e.key === 'Enter' || e.key === ' ') {
71
+ e.preventDefault();
72
+ activate();
73
+ }
74
+ }
75
+ : undefined
76
+ }
77
+ style={{
78
+ borderLeft: `3px solid ${accent}`,
79
+ padding: '6px 8px',
80
+ background: 'color-mix(in srgb, Canvas 94%, CanvasText 6%)',
81
+ cursor: clickable ? 'pointer' : 'default',
82
+ outline: selected ? '2px solid var(--color-default-text, CanvasText)' : 'none',
83
+ outlineOffset: selected ? 1 : 0,
84
+ }}
85
+ data-chippy-boundary-selected={selected ? 'true' : undefined}
86
+ >
87
+ <div style={{ display: 'flex', alignItems: 'baseline', gap: 6, minWidth: 0 }}>
88
+ <span style={{ color: accent, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '.04em', flexShrink: 0 }}>{label}</span>
89
+ <strong style={{ color: colors.text, fontSize: 12, overflowWrap: 'anywhere' }}>{title}</strong>
90
+ <DistanceBadge distance={distance} />
91
+ </div>
92
+ <DetailFields fields={fields} />
93
+ </div>
94
+ );
95
+ }
96
+
97
+ const EMPTY_DISPLAY = { kinds: [], titles: {}, fields: {}, showError: false };
98
+
99
+ function contextDisplay(context) {
100
+ return context.display || EMPTY_DISPLAY;
101
+ }
102
+
103
+ function toFieldPairs(fields) {
104
+ return (fields || []).map((field) => [field.label, field.value]);
105
+ }
106
+
107
+ /**
108
+ * Renders the marker context: provider boundary kinds first (for example a
109
+ * team ownership marker), then the fiber-native error and suspense boundaries.
110
+ * Without onInspectBoundary the boundary blocks render as static facts.
111
+ * @param {{ context: any, onInspectBoundary?: (kind: string) => void }} props
112
+ */
113
+ export function ContextCard({ context, onInspectBoundary }) {
114
+ const [selectedKind, setSelectedKind] = React.useState(null);
115
+ if (!context) return null;
116
+ const display = contextDisplay(context);
117
+ const boundaries = context.boundaries || {};
118
+ const providerKinds = (display.kinds || []).filter((kind) => boundaries[kind.kind]);
119
+ const error = boundaries.error || null;
120
+ const suspense = boundaries.suspense || null;
121
+ const showError = !!(error || display.showError);
122
+ if (!providerKinds.length && !showError && !suspense) return null;
123
+
124
+ const blockProps = (kind) => ({
125
+ onInspect: onInspectBoundary,
126
+ selected: selectedKind === kind,
127
+ onSelect: setSelectedKind,
128
+ });
129
+
130
+ return (
131
+ <div data-testid="chippy-context" style={{ display: 'grid', gap: 5, marginTop: 8, borderTop: `1px solid ${colors.border}`, paddingTop: 8 }}>
132
+ {providerKinds.map((kind) => (
133
+ <BoundaryBlock
134
+ key={kind.kind}
135
+ testId={`chippy-boundary-${kind.kind}`}
136
+ kind={kind.kind}
137
+ label={kind.label}
138
+ title={display.titles?.[kind.kind] || boundaries[kind.kind]?.componentName || kind.label}
139
+ accent={kind.accent || colors.provider}
140
+ distance={boundaries[kind.kind]?.distance}
141
+ fields={toFieldPairs(display.fields?.[kind.kind])}
142
+ {...blockProps(kind.kind)}
143
+ />
144
+ ))}
145
+ {showError ? (
146
+ <BoundaryBlock
147
+ testId="chippy-boundary-error"
148
+ kind="error"
149
+ label="error boundary"
150
+ title={display.titles?.error || error?.componentName || 'ErrorBoundary'}
151
+ accent={colors.error}
152
+ distance={error?.distance}
153
+ fields={toFieldPairs(display.fields?.error)}
154
+ {...blockProps('error')}
155
+ />
156
+ ) : null}
157
+ {suspense ? (
158
+ <BoundaryBlock
159
+ testId="chippy-boundary-suspense"
160
+ kind="suspense"
161
+ label="suspense boundary"
162
+ title={suspense.componentName || 'Suspense'}
163
+ accent={colors.suspense}
164
+ distance={suspense.distance}
165
+ fields={[]}
166
+ {...blockProps('suspense')}
167
+ />
168
+ ) : null}
169
+ </div>
170
+ );
171
+ }