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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +276 -2
  3. package/bin/chippy.mjs +92 -0
  4. package/bin/pack.mjs +204 -0
  5. package/bin/skill-dir.mjs +15 -0
  6. package/lib/avalanche.js +2601 -0
  7. package/lib/avalanche.js.map +7 -0
  8. package/lib/bootstrap.d.ts +1 -0
  9. package/lib/bootstrap.js +70 -0
  10. package/lib/bootstrap.js.map +7 -0
  11. package/lib/browser.js +15 -0
  12. package/lib/browser.js.map +7 -0
  13. package/lib/extension/avalanche.es.js +5535 -0
  14. package/lib/extension/avalanche.es.js.map +1 -0
  15. package/lib/extension/chippy-page-bridge.js +1953 -0
  16. package/lib/extension/inspect-react.js +216 -0
  17. package/lib/extension/layout-lab.js +1453 -0
  18. package/lib/extension/panel-dom.js +90 -0
  19. package/lib/extension/resolve-probes.js +342 -0
  20. package/lib/mcp-skill/LICENSE +201 -0
  21. package/lib/mcp-skill/README.md +148 -0
  22. package/lib/mcp-skill/SKILL.md +335 -0
  23. package/lib/mcp-skill/browser-scripts/playwright-probes.js +368 -0
  24. package/lib/mcp-skill/browser-scripts/playwright-session.js +114 -0
  25. package/lib/mcp-skill/extension/armed-reload-startup.js +83 -0
  26. package/lib/mcp-skill/extension/avalanche.es.js +5535 -0
  27. package/lib/mcp-skill/extension/avalanche.es.js.map +1 -0
  28. package/lib/mcp-skill/extension/avalanche.iife.js +5536 -0
  29. package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -0
  30. package/lib/mcp-skill/extension/background.js +325 -0
  31. package/lib/mcp-skill/extension/chippy-page-bridge.js +1953 -0
  32. package/lib/mcp-skill/extension/content-script.js +190 -0
  33. package/lib/mcp-skill/extension/devtools-hook-shim.js +85 -0
  34. package/lib/mcp-skill/extension/early-bootstrap.js +49 -0
  35. package/lib/mcp-skill/extension/icon-128.png +0 -0
  36. package/lib/mcp-skill/extension/icon-16.png +0 -0
  37. package/lib/mcp-skill/extension/icon-32.png +0 -0
  38. package/lib/mcp-skill/extension/icon-48.png +0 -0
  39. package/lib/mcp-skill/extension/inspect-react.js +216 -0
  40. package/lib/mcp-skill/extension/layout-lab.js +1453 -0
  41. package/lib/mcp-skill/extension/logo.svg +14 -0
  42. package/lib/mcp-skill/extension/manifest.json +69 -0
  43. package/lib/mcp-skill/extension/panel-dom.js +90 -0
  44. package/lib/mcp-skill/extension/panel-settings.js +173 -0
  45. package/lib/mcp-skill/extension/popup.html +41 -0
  46. package/lib/mcp-skill/extension/popup.js +80 -0
  47. package/lib/mcp-skill/extension/probe-descriptor.js +241 -0
  48. package/lib/mcp-skill/extension/resolve-probes.js +342 -0
  49. package/lib/mcp-skill/extension/startup-handoff.js +30 -0
  50. package/lib/mcp-skill/package.json +1 -0
  51. package/lib/mcp-skill/references/README.md +56 -0
  52. package/lib/mcp-skill/references/active-stimulus.md +85 -0
  53. package/lib/mcp-skill/references/avalanche-detector.md +731 -0
  54. package/lib/mcp-skill/references/bdd-router.md +310 -0
  55. package/lib/mcp-skill/references/browser-runtime-api.md +193 -0
  56. package/lib/mcp-skill/references/command-output-formats.md +1085 -0
  57. package/lib/mcp-skill/references/command-surface.md +183 -0
  58. package/lib/mcp-skill/references/cynefin.md +99 -0
  59. package/lib/mcp-skill/references/debug-hook.md +161 -0
  60. package/lib/mcp-skill/references/fixing-performance.md +490 -0
  61. package/lib/mcp-skill/references/hook-metrics.md +199 -0
  62. package/lib/mcp-skill/references/hooks-inspection.md +61 -0
  63. package/lib/mcp-skill/references/jira-taxonomy.md +81 -0
  64. package/lib/mcp-skill/references/mcp.md +122 -0
  65. package/lib/mcp-skill/references/mode-a-injection.md +102 -0
  66. package/lib/mcp-skill/references/operating-model.md +128 -0
  67. package/lib/mcp-skill/references/playwright-cli-setup.md +105 -0
  68. package/lib/mcp-skill/references/playwright-probes.md +205 -0
  69. package/lib/mcp-skill/references/procedures.md +545 -0
  70. package/lib/mcp-skill/references/recipes.txt +114 -0
  71. package/lib/mcp-skill/references/stability-render.md +428 -0
  72. package/lib/mcp-skill/references/test-mode-playbook.md +194 -0
  73. package/lib/mcp-skill/references/troubleshooting.md +59 -0
  74. package/lib/mcp-skill/scripts/avalanche-analyze.cjs +495 -0
  75. package/lib/mcp-skill/scripts/build-avalanche-command.js +34 -0
  76. package/lib/mcp-skill/scripts/build-fiber-state-command.js +19 -0
  77. package/lib/mcp-skill/scripts/chippy-helper-server.js +1768 -0
  78. package/lib/mcp-skill/scripts/chippy-mcp.cjs +273 -0
  79. package/lib/mcp-skill/scripts/chippy.sh +1252 -0
  80. package/lib/mcp-skill/scripts/format-probe-result.js +34 -0
  81. package/lib/mcp-skill/scripts/helper-access.js +158 -0
  82. package/lib/mcp-skill/scripts/helper-proxy.js +87 -0
  83. package/lib/mcp-skill/scripts/page-socket-trust.js +81 -0
  84. package/lib/mcp-skill/scripts/probe-annotation-channel.js +116 -0
  85. package/lib/mcp-skill/scripts/search-recipes.js +248 -0
  86. package/lib/mcp-skill/snippets/README.md +89 -0
  87. package/lib/mcp-skill/snippets/count-context-consumers.js +40 -0
  88. package/lib/mcp-skill/snippets/find-context-providers.js +38 -0
  89. package/lib/mcp-skill/snippets/find-legacy-context.js +47 -0
  90. package/lib/mcp-skill/snippets/find-prop.js +83 -0
  91. package/lib/mcp-skill/snippets/hook-metrics-sample.js +69 -0
  92. package/lib/mcp-skill/snippets/hook-metrics.js +60 -0
  93. package/lib/mcp-skill/snippets/hook-tracker.js +198 -0
  94. package/lib/mcp-skill/snippets/stability-nudge.js +101 -0
  95. package/lib/package.json +1 -0
  96. package/lib/plugin.js +119 -0
  97. package/lib/plugin.js.map +7 -0
  98. package/lib/storybook/avalanche-panel-model.js +79 -0
  99. package/lib/storybook/brand.jsx +6 -0
  100. package/lib/storybook/connection-fallback.jsx +47 -0
  101. package/lib/storybook/constants.js +15 -0
  102. package/lib/storybook/context-card.jsx +171 -0
  103. package/lib/storybook/manager-owner.js +42 -0
  104. package/lib/storybook/manager.jsx +425 -0
  105. package/lib/storybook/preset.js +7 -0
  106. package/lib/storybook/preview.js +298 -0
  107. package/lib/storybook/probe-annotation-editor.jsx +52 -0
  108. package/lib/storybook/probe-intent.js +23 -0
  109. package/lib/storybook/setup.js +104 -0
  110. package/lib/types/avalanche/commit-metrics-adapter.d.ts +76 -0
  111. package/lib/types/avalanche/commit-stats-indices.d.ts +41 -0
  112. package/lib/types/avalanche/commit-surface-ownership.d.ts +9 -0
  113. package/lib/types/avalanche/detector.d.ts +183 -0
  114. package/lib/types/avalanche/interaction-capture.d.ts +25 -0
  115. package/lib/types/avalanche/internals.d.ts +189 -0
  116. package/lib/types/avalanche/report-serializer.d.ts +283 -0
  117. package/lib/types/avalanche/reporter.d.ts +610 -0
  118. package/lib/types/chippy-bridge/fiber-source.d.ts +48 -0
  119. package/lib/types/chippy-bridge/react-renderer.d.ts +74 -0
  120. package/lib/types/chippy-bridge/recorded-source.d.ts +44 -0
  121. package/lib/types/chippy-bridge/source-map.d.ts +30 -0
  122. package/lib/types/debug-render/core.d.ts +78 -0
  123. package/lib/types/layout-lab/fiber-boundaries.d.ts +51 -0
  124. package/lib/types/layout-lab/fiber-walk.d.ts +131 -0
  125. package/lib/types/layout-lab/marker-context.d.ts +94 -0
  126. package/lib/types/package/avalanche.d.ts +6 -0
  127. package/lib/types/package/browser.d.ts +10 -0
  128. package/lib/types/package/plugin.d.ts +15 -0
  129. package/lib/types/plugins/atl/commit-metrics-adapter.d.ts +65 -0
  130. package/lib/types/plugins/atl/metrics-slot.d.ts +37 -0
  131. package/lib/types/plugins/index.d.ts +49 -0
  132. package/lib/types/plugins/jira/context-provider.d.ts +36 -0
  133. package/package.json +106 -4
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
2
+ <defs>
3
+ <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>
4
+ <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>
5
+ </defs>
6
+ <style>
7
+ @keyframes chippy-trace-reveal { from { opacity: .45; transform: translateY(-1.5px); } to { opacity: 1; transform: translateY(0); } }
8
+ .chippy-trace-cut { animation: chippy-trace-reveal 650ms ease-out both; }
9
+ @media (prefers-reduced-motion: reduce) { .chippy-trace-cut { animation: none; } }
10
+ </style>
11
+ <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)"/>
12
+ <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)"/>
13
+ <path d="m11 18.6 5 3.1 5-3.1 7 4.4-12 7.5-5-3.1Z" fill="#1868DB"/>
14
+ </svg>
@@ -0,0 +1,69 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Chippy Probe Bridge",
4
+ "version": "0.4.25",
5
+ "description": "Inspects connected React pages to trace components, boundaries, hooks, props, and render behavior.",
6
+ "permissions": [
7
+ "storage",
8
+ "scripting",
9
+ "webNavigation"
10
+ ],
11
+ "host_permissions": [
12
+ "http://*/*",
13
+ "https://*/*"
14
+ ],
15
+ "background": {
16
+ "service_worker": "background.js"
17
+ },
18
+ "action": {
19
+ "default_title": "Chippy Probe Bridge",
20
+ "default_popup": "popup.html",
21
+ "default_icon": {
22
+ "16": "icon-16.png",
23
+ "32": "icon-32.png",
24
+ "48": "icon-48.png",
25
+ "128": "icon-128.png"
26
+ }
27
+ },
28
+ "content_scripts": [
29
+ {
30
+ "matches": [
31
+ "http://*/*",
32
+ "https://*/*"
33
+ ],
34
+ "js": [
35
+ "panel-settings.js",
36
+ "content-script.js"
37
+ ],
38
+ "run_at": "document_start",
39
+ "all_frames": true
40
+ }
41
+ ],
42
+ "web_accessible_resources": [
43
+ {
44
+ "resources": [
45
+ "devtools-hook-shim.js",
46
+ "early-bootstrap.js",
47
+ "inspect-react.js",
48
+ "panel-dom.js",
49
+ "layout-lab.js",
50
+ "resolve-probes.js",
51
+ "probe-descriptor.js",
52
+ "chippy-page-bridge.js",
53
+ "avalanche.iife.js",
54
+ "avalanche.iife.js.map",
55
+ "avalanche.es.js.map"
56
+ ],
57
+ "matches": [
58
+ "http://*/*",
59
+ "https://*/*"
60
+ ]
61
+ }
62
+ ],
63
+ "icons": {
64
+ "16": "icon-16.png",
65
+ "32": "icon-32.png",
66
+ "48": "icon-48.png",
67
+ "128": "icon-128.png"
68
+ }
69
+ }
@@ -0,0 +1,90 @@
1
+ /* eslint-disable */
2
+ (function installChippyPanelDom(global) {
3
+ function nodeKey(node) {
4
+ return node?.nodeType === 1 ? node.getAttribute('data-chippy-key') : null;
5
+ }
6
+
7
+ function canPatch(current, next) {
8
+ if (!current || !next || current.nodeType !== next.nodeType) return false;
9
+ if (current.nodeType === 1 && current.tagName !== next.tagName) return false;
10
+ const currentKey = nodeKey(current);
11
+ const nextKey = nodeKey(next);
12
+ return currentKey || nextKey ? currentKey === nextKey : true;
13
+ }
14
+
15
+ function syncAttributes(current, next) {
16
+ Array.from(current.attributes).forEach((attribute) => {
17
+ if (!next.hasAttribute(attribute.name)) current.removeAttribute(attribute.name);
18
+ });
19
+ Array.from(next.attributes).forEach((attribute) => {
20
+ if (current.getAttribute(attribute.name) !== attribute.value) {
21
+ current.setAttribute(attribute.name, attribute.value);
22
+ }
23
+ });
24
+ }
25
+
26
+ function patchNode(current, next) {
27
+ if (current.nodeType === 3 || current.nodeType === 8) {
28
+ if (current.data !== next.data) current.data = next.data;
29
+ return;
30
+ }
31
+ if (current.nodeType !== 1) return;
32
+
33
+ const tagName = current.tagName;
34
+ const preservesValue = tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT';
35
+ const value = preservesValue ? current.value : null;
36
+ const checked = tagName === 'INPUT' ? current.checked : null;
37
+
38
+ syncAttributes(current, next);
39
+ patchChildNodes(current, next);
40
+
41
+ // Probe annotations are drafts until Attach is clicked. Rendering new
42
+ // runtime evidence must not overwrite an in-progress value.
43
+ if (preservesValue) current.value = value;
44
+ if (tagName === 'INPUT') current.checked = checked;
45
+ }
46
+
47
+ function patchChildNodes(currentParent, nextParent) {
48
+ const currentChildren = Array.from(currentParent.childNodes);
49
+ const keyed = new Map(
50
+ currentChildren
51
+ .map((node) => [nodeKey(node), node])
52
+ .filter(([key]) => Boolean(key)),
53
+ );
54
+ const used = new Set();
55
+ let cursor = currentParent.firstChild;
56
+
57
+ Array.from(nextParent.childNodes).forEach((nextChild) => {
58
+ const key = nodeKey(nextChild);
59
+ let current = key ? keyed.get(key) : null;
60
+
61
+ while (cursor && used.has(cursor)) cursor = cursor.nextSibling;
62
+ if (!current && cursor && !nodeKey(cursor) && canPatch(cursor, nextChild)) current = cursor;
63
+
64
+ if (current && canPatch(current, nextChild)) {
65
+ if (current !== cursor) currentParent.insertBefore(current, cursor);
66
+ patchNode(current, nextChild);
67
+ } else {
68
+ current = nextChild.cloneNode(true);
69
+ currentParent.insertBefore(current, cursor);
70
+ }
71
+
72
+ used.add(current);
73
+ cursor = current.nextSibling;
74
+ });
75
+
76
+ Array.from(currentParent.childNodes).forEach((child) => {
77
+ if (used.has(child)) return;
78
+ if (child.nodeType === 1 && child.hasAttribute('data-chippy-preserve')) return;
79
+ child.remove();
80
+ });
81
+ }
82
+
83
+ function patchChildren(target, html) {
84
+ const template = target.ownerDocument.createElement('template');
85
+ template.innerHTML = html;
86
+ patchChildNodes(target, template.content);
87
+ }
88
+
89
+ global.__CHIPPY_PANEL_DOM__ = { patchChildren };
90
+ })(window);
@@ -0,0 +1,173 @@
1
+ /* eslint-disable */
2
+ // Runs in Chrome's isolated content-script world. The content script owns one
3
+ // stable host + closed shadow root and portals it into the MAIN-world panel.
4
+ (function installChippyPanelSettings() {
5
+ try { globalThis.__CHIPPY_PANEL_SETTINGS_RUNTIME__?.destroy?.(); } catch {}
6
+
7
+ const DEFAULT_HELPER = 'http://127.0.0.1:7823';
8
+ // A packed build (`chippy pack`) prepends globalThis.__CHIPPY_BUILD to this
9
+ // file with its plugin list and the plugins it enables by default.
10
+ const BUILD = globalThis.__CHIPPY_BUILD || {};
11
+ const PLUGIN_OPTIONS = Array.isArray(BUILD.plugins) ? BUILD.plugins : [
12
+ { id: 'jira', label: 'Jira ownership markers' },
13
+ { id: 'atl', label: 'ATL commit metrics' },
14
+ ];
15
+ const DEFAULT_PLUGINS = Array.isArray(BUILD.defaultPlugins) ? BUILD.defaultPlugins : [];
16
+ const SETTINGS_SLOT_SELECTOR = '[data-chippy-settings-slot]';
17
+ let observer = null;
18
+ let waitingForDom = false;
19
+
20
+ function normalizeBaseUrl(value) {
21
+ return String(value || DEFAULT_HELPER).trim().replace(/\/+$/, '');
22
+ }
23
+
24
+ function validateHelperUrl(value) {
25
+ try {
26
+ const url = new URL(value);
27
+ return url.protocol === 'http:' || url.protocol === 'https:';
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ function escapeHtml(value) {
34
+ return String(value).replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
35
+ }
36
+
37
+ function pluginMarkup() {
38
+ if (PLUGIN_OPTIONS.length === 0) return '';
39
+ const rows = PLUGIN_OPTIONS.map((plugin) =>
40
+ `<label><input data-plugin type="checkbox" value="${escapeHtml(plugin.id)}" />${escapeHtml(plugin.label || plugin.id)}</label>`);
41
+ return `<fieldset>
42
+ <legend>Plugins · apply on reload</legend>
43
+ ${rows.join('\n ')}
44
+ </fieldset>`;
45
+ }
46
+
47
+ function settingsMarkup() {
48
+ return `
49
+ <style>
50
+ :host { all: initial; display: block; color-scheme: dark; }
51
+ * { box-sizing: border-box; }
52
+ form { display: grid; gap: 8px; color: #CECFD2; font: 11px/1.4 'Atlassian Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
53
+ label { display: grid; gap: 3px; }
54
+ label span { color: #A9ABAF; font-size: 9px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; }
55
+ input { width: 100%; border: 1px solid rgba(148,163,184,.25); border-radius: 7px; padding: 6px 8px; background: #1F1F21; color: #CECFD2; font: inherit; }
56
+ input:focus { outline: 2px solid #669DF1; outline-offset: 1px; }
57
+ fieldset { display: grid; gap: 4px; margin: 0; padding: 0; border: 0; }
58
+ legend { margin-bottom: 3px; padding: 0; color: #A9ABAF; font-size: 9px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; }
59
+ fieldset label { display: flex; align-items: center; gap: 6px; }
60
+ fieldset input { width: auto; margin: 0; }
61
+ .actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
62
+ button { border: 1px solid rgba(96,165,250,.35); border-radius: 7px; padding: 5px 10px; background: rgba(59,130,246,.2); color: #B8D6FF; font: inherit; font-weight: 700; cursor: pointer; }
63
+ [data-status] { min-height: 15px; color: #86efac; font-size: 10px; text-align: right; }
64
+ [data-status][data-error="true"] { color: #FD9891; }
65
+ </style>
66
+ <form>
67
+ <label>
68
+ <span>Chippy URL</span>
69
+ <input data-helper-url type="url" inputmode="url" required placeholder="${DEFAULT_HELPER}" />
70
+ </label>
71
+ ${pluginMarkup()}
72
+ <div class="actions">
73
+ <button type="submit">Save settings</button>
74
+ <div data-status role="status" aria-live="polite"></div>
75
+ </div>
76
+ </form>
77
+ `;
78
+ }
79
+
80
+ const host = document.createElement('div');
81
+ host.setAttribute('data-chippy-settings-host', 'true');
82
+ host.setAttribute('data-chippy-preserve', 'true');
83
+ const pluginRowsHeight = PLUGIN_OPTIONS.length ? 18 + 19 * PLUGIN_OPTIONS.length : 0;
84
+ host.style.cssText = `display:block;min-height:${124 + pluginRowsHeight}px;`;
85
+ const root = host.attachShadow({ mode: 'closed' });
86
+ const surface = document.createElement('div');
87
+ surface.innerHTML = settingsMarkup();
88
+ root.appendChild(surface);
89
+
90
+ const form = root.querySelector('form');
91
+ const helperInput = root.querySelector('[data-helper-url]');
92
+ const pluginInputs = Array.from(root.querySelectorAll('[data-plugin]'));
93
+ const statusNode = root.querySelector('[data-status]');
94
+
95
+ function setStatus(message, isError = false) {
96
+ statusNode.textContent = message;
97
+ statusNode.setAttribute('data-error', isError ? 'true' : 'false');
98
+ }
99
+
100
+ chrome.storage.local.get({
101
+ helperBaseUrl: DEFAULT_HELPER,
102
+ plugins: DEFAULT_PLUGINS,
103
+ }).then((stored) => {
104
+ helperInput.value = normalizeBaseUrl(stored.helperBaseUrl);
105
+ const plugins = Array.isArray(stored.plugins) ? stored.plugins : [];
106
+ for (const input of pluginInputs) input.checked = plugins.includes(input.value);
107
+ }).catch((error) => setStatus(error.message || 'Could not load settings.', true));
108
+
109
+ form.addEventListener('submit', async (event) => {
110
+ event.preventDefault();
111
+ const helperBaseUrl = normalizeBaseUrl(helperInput.value);
112
+ if (!validateHelperUrl(helperBaseUrl)) {
113
+ setStatus('Use an http(s) URL.', true);
114
+ return;
115
+ }
116
+
117
+ try {
118
+ const plugins = pluginInputs.filter((input) => input.checked).map((input) => input.value);
119
+ await chrome.storage.local.set({ helperBaseUrl, plugins });
120
+ document.dispatchEvent(new CustomEvent('chippy-ext-command', {
121
+ detail: { type: 'apply-config', helperBaseUrl, activated: true },
122
+ }));
123
+ setStatus('Saved.');
124
+ } catch (error) {
125
+ setStatus(error.message || 'Could not save settings.', true);
126
+ }
127
+ });
128
+
129
+ function mountPanelSettings() {
130
+ const liveDocument = globalThis.document;
131
+ if (!liveDocument?.querySelectorAll) return;
132
+ const slots = liveDocument.querySelectorAll(SETTINGS_SLOT_SELECTOR);
133
+ const slot = slots[slots.length - 1] || null;
134
+ if (slot && host.parentNode !== slot) slot.appendChild(host);
135
+ }
136
+
137
+ function installMountObserver() {
138
+ mountPanelSettings();
139
+ if (!globalThis.document?.documentElement) {
140
+ if (!waitingForDom) {
141
+ waitingForDom = true;
142
+ globalThis.document?.addEventListener('DOMContentLoaded', onDomReady, { once: true });
143
+ }
144
+ return;
145
+ }
146
+ observer?.disconnect();
147
+ observer = new MutationObserver(mountPanelSettings);
148
+ observer.observe(globalThis.document.documentElement, { childList: true, subtree: true });
149
+ }
150
+
151
+ function onDomReady() {
152
+ waitingForDom = false;
153
+ installMountObserver();
154
+ }
155
+
156
+ function onMountRequest() {
157
+ mountPanelSettings();
158
+ }
159
+
160
+ function destroy() {
161
+ observer?.disconnect();
162
+ globalThis.document?.removeEventListener('DOMContentLoaded', onDomReady);
163
+ globalThis.document?.removeEventListener('chippy-settings-mount-request', onMountRequest);
164
+ host.remove();
165
+ }
166
+
167
+ globalThis.__CHIPPY_PANEL_SETTINGS_RUNTIME__ = {
168
+ destroy,
169
+ mount: mountPanelSettings,
170
+ };
171
+ document.addEventListener('chippy-settings-mount-request', onMountRequest);
172
+ installMountObserver();
173
+ })();
@@ -0,0 +1,41 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Chippy</title>
6
+ <style>
7
+ * { box-sizing: border-box; }
8
+ body {
9
+ width: 224px;
10
+ margin: 0;
11
+ padding: 12px;
12
+ display: grid;
13
+ grid-template-columns: 1fr 1fr;
14
+ gap: 8px;
15
+ background: #fff;
16
+ color: #292A2E;
17
+ font: 13px 'Atlassian Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
18
+ }
19
+ button {
20
+ border: 1px solid #DDDEE1;
21
+ border-radius: 6px;
22
+ padding: 8px 10px;
23
+ background: #fff;
24
+ color: #292A2E;
25
+ font: inherit;
26
+ font-weight: 600;
27
+ cursor: pointer;
28
+ }
29
+ #btn-activate { background: #1868DB; border-color: #1868DB; color: #fff; }
30
+ header { grid-column: 1 / -1; display: flex; align-items: center; gap: 8px; margin-bottom: 4px; font-size: 16px; font-weight: 650; }
31
+ button:focus-visible { outline: 2px solid #1868DB; outline-offset: 2px; }
32
+ button:disabled { cursor: default; opacity: .4; }
33
+ </style>
34
+ </head>
35
+ <body>
36
+ <header><img src="logo.svg" width="28" height="28" alt="" />Chippy</header>
37
+ <button id="btn-activate" type="button">Enable</button>
38
+ <button id="btn-deactivate" type="button">Disable</button>
39
+ <script src="popup.js"></script>
40
+ </body>
41
+ </html>
@@ -0,0 +1,80 @@
1
+ /* eslint-disable */
2
+ const DEFAULT_HELPER = 'http://127.0.0.1:7823';
3
+
4
+ const btnActivate = document.querySelector('#btn-activate');
5
+ const btnDeactivate = document.querySelector('#btn-deactivate');
6
+
7
+ async function getActiveTab() {
8
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
9
+ return tab || null;
10
+ }
11
+
12
+ function getOrigin(tab) {
13
+ try {
14
+ return new URL(tab?.url || '').origin;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ async function getStoredConfig() {
21
+ return chrome.storage.local.get({
22
+ helperBaseUrl: DEFAULT_HELPER,
23
+ enabledOrigins: {},
24
+ });
25
+ }
26
+
27
+ async function setOriginEnabled(origin, enabled) {
28
+ const stored = await getStoredConfig();
29
+ const enabledOrigins = { ...(stored.enabledOrigins || {}) };
30
+ if (enabled) enabledOrigins[origin] = true;
31
+ else delete enabledOrigins[origin];
32
+ await chrome.storage.local.set({ enabledOrigins });
33
+ return stored;
34
+ }
35
+
36
+ async function renderState() {
37
+ const tab = await getActiveTab();
38
+ const origin = getOrigin(tab);
39
+ if (!tab || !origin) {
40
+ btnActivate.disabled = true;
41
+ btnDeactivate.disabled = true;
42
+ return;
43
+ }
44
+
45
+ const stored = await getStoredConfig();
46
+ const enabled = Boolean(stored.enabledOrigins?.[origin]);
47
+ btnActivate.disabled = enabled;
48
+ btnDeactivate.disabled = !enabled;
49
+ }
50
+
51
+ btnActivate.addEventListener('click', async () => {
52
+ const tab = await getActiveTab();
53
+ const origin = getOrigin(tab);
54
+ if (!tab || !origin) return;
55
+
56
+ const stored = await setOriginEnabled(origin, true);
57
+ try {
58
+ await chrome.runtime.sendMessage({ type: 'chippy-ensure-content-script', tabId: tab.id });
59
+ await chrome.runtime.sendMessage({
60
+ type: 'chippy-inject-bundle',
61
+ tabId: tab.id,
62
+ helperBaseUrl: stored.helperBaseUrl || DEFAULT_HELPER,
63
+ });
64
+ } catch {}
65
+ window.close();
66
+ });
67
+
68
+ btnDeactivate.addEventListener('click', async () => {
69
+ const tab = await getActiveTab();
70
+ const origin = getOrigin(tab);
71
+ if (!tab || !origin) return;
72
+
73
+ await setOriginEnabled(origin, false);
74
+ try {
75
+ await chrome.tabs.sendMessage(tab.id, { type: 'chippy-disable-now' });
76
+ } catch {}
77
+ window.close();
78
+ });
79
+
80
+ renderState();