Skip to content

Instantly share code, notes, and snippets.

@utxo-detective
Created January 12, 2026 02:32
Show Gist options
  • Select an option

  • Save utxo-detective/c80e8cd2abbf54d7fa39dff11663f0c7 to your computer and use it in GitHub Desktop.

Select an option

Save utxo-detective/c80e8cd2abbf54d7fa39dff11663f0c7 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name React Component Picker+ (Deep Context + Multi-Select)
// @namespace https://example.local/
// @version 0.3.0
// @description Pick DOM/React components via Fiber and copy rich context for LLMs. Cmd/Ctrl+C toggles picker; Cmd/Ctrl+Shift+C copies context.
// @match https://app.hyperliquid.xyz/*
// @match https://*.your-domain.com/*
// @match http://localhost:*/*
// @match http://127.0.0.1:*/*
// @grant GM_setClipboard
// @grant GM_addStyle
// @run-at document-idle
// @match https://app.hyperliquid.xyz/*
// @match https://app.based.one/*
// @match https://x.com/*
// ==/UserScript==
(() => {
"use strict";
// ----------------------------
// Config
// ----------------------------
const CFG = {
// Toggle picker mode: Cmd/Ctrl + C
toggleHotkey: { code: "KeyC", requireMetaOrCtrl: true, shiftKey: false },
// Copy context: Cmd/Ctrl + Shift + C
copyHotkey: { code: "KeyC", requireMetaOrCtrl: true, shiftKey: true },
// Limits
maxHtmlChars: 12000,
maxPropsChars: 14000,
maxStackDepth: 14,
maxHookItems: 14,
maxFiberTreeDepth: 5,
maxFiberTreeBreadth: 24,
maxComponentDomRoots: 10,
maxHostNodesScan: 700,
// Deep capture (nested components / dropdowns)
deep: {
maxInteractiveNodes: 30,
maxInteractiveHtmlChars: 2500,
maxNotableComponents: 30,
maxNotablePropsKeys: 40,
includeComputedStyle: false, // keep false; can be heavy
},
// Heuristics
primitiveNames: new Set([
"Box", "Flex", "Grid", "Stack", "HStack", "VStack",
"Text", "Heading", "Image", "Icon", "Button",
"styled.div", "styled.span", "styled.button", "styled.a",
"ForwardRef", "Memo", "Fragment",
]),
primitiveNamePrefixes: ["styled.", "sc-", "emotion", "css-"],
ignoreMinifiedNamesShorterThan: 2,
};
// ----------------------------
// CSS
// ----------------------------
const css = `
.rcp__root { position: fixed; inset: 0; pointer-events: none; z-index: 2147483647; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; }
.rcp__hud { position: fixed; bottom: 16px; right: 16px; pointer-events: auto; background: rgba(20,20,20,.92); color: #fff; border-radius: 10px; padding: 10px 12px; box-shadow: 0 10px 30px rgba(0,0,0,.35); width: 420px; display: none; }
.rcp__hud.rcp__hud--open { display: block; }
.rcp__row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.rcp__title { font-weight: 650; font-size: 13px; }
.rcp__pill { font-size: 12px; padding: 2px 8px; border-radius: 999px; background: rgba(255,255,255,.12); }
.rcp__btns { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.rcp__btn { cursor: pointer; user-select: none; border: 0; padding: 8px 10px; border-radius: 8px; font-size: 12px; background: rgba(255,255,255,.12); color: #fff; }
.rcp__btn:hover { background: rgba(255,255,255,.18); }
.rcp__btn--primary { background: rgba(59,130,246,.85); }
.rcp__btn--primary:hover { background: rgba(59,130,246,1); }
.rcp__meta { margin-top: 10px; font-size: 12px; line-height: 1.35; color: rgba(255,255,255,.88); max-height: 240px; overflow: auto; white-space: pre-wrap; }
.rcp__kbd { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New"; font-size: 11px; color: rgba(255,255,255,.8); }
.rcp__hint { margin-top: 8px; font-size: 11px; color: rgba(255,255,255,.7); }
.rcp__highlight { position: fixed; pointer-events: none; border: 2px solid rgba(59,130,246,.95); border-radius: 6px; box-shadow: 0 0 0 3px rgba(59,130,246,.18); display: none; }
.rcp__tooltip { position: fixed; pointer-events: none; background: rgba(20,20,20,.92); color: #fff; border-radius: 8px; padding: 6px 8px; font-size: 12px; max-width: min(640px, 90vw); display: none; box-shadow: 0 10px 30px rgba(0,0,0,.35); }
.rcp__tooltip .rcp__mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New"; font-size: 11px; opacity: .9; }
.rcp__opts { margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.rcp__opt { display: flex; align-items: center; gap: 8px; font-size: 12px; color: rgba(255,255,255,.88); }
.rcp__opt input { width: 14px; height: 14px; }
.rcp__list { margin-top: 8px; font-size: 11px; color: rgba(255,255,255,.78); max-height: 110px; overflow: auto; border-top: 1px solid rgba(255,255,255,.10); padding-top: 8px; }
.rcp__item { margin-bottom: 6px; }
.rcp__item strong { color: rgba(255,255,255,.92); }
`;
if (typeof GM_addStyle === "function") GM_addStyle(css);
else {
const style = document.createElement("style");
style.textContent = css;
document.documentElement.appendChild(style);
}
// ----------------------------
// UI elements
// ----------------------------
const root = document.createElement("div");
root.className = "rcp__root";
document.documentElement.appendChild(root);
const highlight = document.createElement("div");
highlight.className = "rcp__highlight";
root.appendChild(highlight);
const tooltip = document.createElement("div");
tooltip.className = "rcp__tooltip";
root.appendChild(tooltip);
const hud = document.createElement("div");
hud.className = "rcp__hud";
hud.innerHTML = `
<div class="rcp__row">
<div class="rcp__title">React Component Picker+</div>
<div class="rcp__pill" id="rcpModePill">OFF</div>
</div>
<div class="rcp__hint">
Toggle: <span class="rcp__kbd">Cmd/Ctrl+C</span> • Copy: <span class="rcp__kbd">Cmd/Ctrl+Shift+C</span> • Add: <span class="rcp__kbd">Shift+Click</span> • Close: <span class="rcp__kbd">Esc</span>
</div>
<div class="rcp__opts">
<label class="rcp__opt"><input type="checkbox" id="rcpPreferSemantic" checked /> Prefer semantic component</label>
<label class="rcp__opt"><input type="checkbox" id="rcpDeepCapture" checked /> Deep capture (dropdowns)</label>
<label class="rcp__opt"><input type="checkbox" id="rcpMultiSelect" checked /> Multi-select</label>
<label class="rcp__opt"><input type="checkbox" id="rcpIncludeFiberTree" /> Include fiber tree</label>
</div>
<div class="rcp__btns">
<button class="rcp__btn rcp__btn--primary" id="rcpCopyBtn">Copy context</button>
<button class="rcp__btn" id="rcpCopyHtmlBtn">Copy HTML only</button>
<button class="rcp__btn" id="rcpCopyJsonBtn">Copy JSON only</button>
<button class="rcp__btn" id="rcpSelectParentBtn">Select parent semantic (↑)</button>
<button class="rcp__btn" id="rcpClearBtn">Clear</button>
</div>
<div class="rcp__meta" id="rcpMeta"></div>
<div class="rcp__list" id="rcpList" style="display:none;"></div>
`;
root.appendChild(hud);
const modePill = hud.querySelector("#rcpModePill");
const metaEl = hud.querySelector("#rcpMeta");
const listEl = hud.querySelector("#rcpList");
const preferSemanticEl = hud.querySelector("#rcpPreferSemantic");
const deepCaptureEl = hud.querySelector("#rcpDeepCapture");
const multiSelectEl = hud.querySelector("#rcpMultiSelect");
const includeFiberTreeEl = hud.querySelector("#rcpIncludeFiberTree");
const copyBtn = hud.querySelector("#rcpCopyBtn");
const copyHtmlBtn = hud.querySelector("#rcpCopyHtmlBtn");
const copyJsonBtn = hud.querySelector("#rcpCopyJsonBtn");
const selectParentBtn = hud.querySelector("#rcpSelectParentBtn");
const clearBtn = hud.querySelector("#rcpClearBtn");
// ----------------------------
// State
// ----------------------------
let enabled = false;
let hoveredEl = null;
/** @type {Array<{el: Element, ctx: any}>} */
let selections = [];
// ----------------------------
// Helpers
// ----------------------------
const isEditableTarget = (t) => {
if (!t) return false;
const tag = (t.tagName || "").toLowerCase();
return tag === "input" || tag === "textarea" || tag === "select" || t.isContentEditable === true;
};
const hasTextSelection = () => {
const sel = window.getSelection?.();
return sel && String(sel).trim().length > 0;
};
const truncate = (s, max) =>
s && s.length > max ? s.slice(0, max) + `\n…(truncated, ${s.length} chars total)` : s;
function safeStringify(value, maxChars) {
const seen = new WeakSet();
const replacer = (k, v) => {
if (typeof v === "function") return `[Function ${v.name || "anonymous"}]`;
if (v instanceof Element) return `[Element <${v.tagName.toLowerCase()}>]`;
if (v instanceof Node) return `[Node ${v.nodeName}]`;
if (typeof v === "object" && v !== null) {
if (seen.has(v)) return "[Circular]";
seen.add(v);
}
return v;
};
let out;
try {
out = JSON.stringify(value, replacer, 2);
} catch (e) {
out = `"[Unserializable: ${String(e)}]"`;
}
return truncate(out, maxChars);
}
function cssPath(el) {
if (!el || el.nodeType !== 1) return "";
const parts = [];
let cur = el;
while (cur && cur.nodeType === 1 && parts.length < 7) {
let part = cur.tagName.toLowerCase();
if (cur.id) {
part += `#${cur.id}`;
parts.unshift(part);
break;
}
const cls =
cur.className && typeof cur.className === "string"
? cur.className.trim().split(/\s+/).slice(0, 2)
: [];
if (cls.length) part += "." + cls.join(".");
const parent = cur.parentElement;
if (parent) {
const siblings = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
if (siblings.length > 1) {
const idx = siblings.indexOf(cur) + 1;
part += `:nth-of-type(${idx})`;
}
}
parts.unshift(part);
cur = cur.parentElement;
}
return parts.join(" > ");
}
function elementSummary(el) {
if (!el || el.nodeType !== 1) return null;
const tag = el.tagName.toLowerCase();
const id = el.id || null;
const classList =
el.className && typeof el.className === "string"
? el.className.trim().split(/\s+/).filter(Boolean).slice(0, 14)
: [];
const role = el.getAttribute("role") || null;
const dataTestId = el.getAttribute("data-testid") || el.getAttribute("data-test") || null;
const ariaLabel = el.getAttribute("aria-label") || null;
const ariaExpanded = el.getAttribute("aria-expanded") || null;
const ariaHaspopup = el.getAttribute("aria-haspopup") || null;
const dataState = el.getAttribute("data-state") || null;
const textPreview = (el.innerText || "").trim().replace(/\s+/g, " ").slice(0, 220) || null;
let outerHTML = "";
try { outerHTML = el.outerHTML || ""; } catch { outerHTML = ""; }
return {
tag, id, classList, role, dataTestId, ariaLabel, ariaExpanded, ariaHaspopup, dataState,
cssPath: cssPath(el),
textPreview,
outerHTML: truncate(outerHTML, CFG.maxHtmlChars),
};
}
// ----------------------------
// React Fiber extraction
// ----------------------------
function getReactFiberFromDomNode(node) {
if (!node) return null;
const keys = Object.keys(node);
for (const k of keys) {
if (k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$")) return node[k];
}
return null;
}
function getComponentNameFromType(type) {
if (!type) return null;
if (typeof type === "function") return type.displayName || type.name || "Anonymous";
if (typeof type === "object") {
if (type.displayName) return type.displayName;
if (type.type) return getComponentNameFromType(type.type);
if (type.render) return getComponentNameFromType(type.render);
}
return null;
}
// Fiber tags (heuristic; stable enough across React versions)
const FIBER_TAG = {
FunctionComponent: 0,
ClassComponent: 1,
HostRoot: 3,
HostComponent: 5,
HostText: 6,
Fragment: 7,
ContextConsumer: 9,
ContextProvider: 10,
ForwardRef: 11,
MemoComponent: 14,
SimpleMemoComponent: 15,
LazyComponent: 16,
};
function isLikelyComponentFiber(fiber) {
if (!fiber) return false;
const t = fiber.tag;
if (
t === FIBER_TAG.FunctionComponent ||
t === FIBER_TAG.ClassComponent ||
t === FIBER_TAG.ForwardRef ||
t === FIBER_TAG.MemoComponent ||
t === FIBER_TAG.SimpleMemoComponent ||
t === FIBER_TAG.LazyComponent
) return true;
if (fiber.type && typeof fiber.type !== "string") return true;
return false;
}
function normalizeName(name) {
if (!name) return "";
return String(name).trim();
}
function isPrimitiveName(name) {
const n = normalizeName(name);
if (!n) return true;
if (CFG.primitiveNames.has(n)) return true;
for (const p of CFG.primitiveNamePrefixes) {
if (n.startsWith(p)) return true;
}
// Minified names like "a", "Et", "jt"
if (n.length <= CFG.ignoreMinifiedNamesShorterThan) return true;
return false;
}
function getNearestComponentFiber(fromFiber) {
let f = fromFiber, guard = 0;
while (f && guard++ < 240) {
if (isLikelyComponentFiber(f)) return f;
f = f.return;
}
return null;
}
// Walk up until we find something that is not just layout/primitive/minified
function getNearestSemanticComponentFiber(fromFiber) {
let f = fromFiber, guard = 0;
while (f && guard++ < 340) {
if (isLikelyComponentFiber(f)) {
const name = getComponentNameFromType(f.type) || "(Unknown)";
if (!isPrimitiveName(name)) return f;
}
f = f.return;
}
// fallback: nearest component if nothing semantic found
return getNearestComponentFiber(fromFiber);
}
function getComponentStack(componentFiber) {
const stack = [];
let f = componentFiber, guard = 0;
while (f && guard++ < 500 && stack.length < CFG.maxStackDepth) {
if (isLikelyComponentFiber(f)) {
const name = getComponentNameFromType(f.type) || "(Unknown)";
const src = f._debugSource
? { fileName: f._debugSource.fileName, lineNumber: f._debugSource.lineNumber, columnNumber: f._debugSource.columnNumber }
: null;
stack.push({ name, source: src, tag: f.tag });
}
f = f.return;
}
return stack;
}
function getHookSummary(componentFiber) {
// Best-effort: hook chain is not stable API
const out = [];
let h = componentFiber?.memoizedState;
let i = 0;
while (h && i < CFG.maxHookItems) {
out.push({ hookIndex: i, memoizedState: h.memoizedState });
h = h.next;
i++;
}
return out;
}
function buildFiberTree(componentFiber, depth = 0) {
if (!componentFiber || depth > CFG.maxFiberTreeDepth) return null;
const name =
getComponentNameFromType(componentFiber.type) ||
(componentFiber.tag === FIBER_TAG.HostComponent ? componentFiber.type : "(Unknown)");
const node = { name, tag: componentFiber.tag, children: [] };
let child = componentFiber.child;
let count = 0;
while (child && count++ < CFG.maxFiberTreeBreadth) {
const interesting =
isLikelyComponentFiber(child) ||
child.tag === FIBER_TAG.HostComponent ||
child.tag === FIBER_TAG.Fragment;
if (interesting) node.children.push(buildFiberTree(child, depth + 1));
child = child.sibling;
}
return node;
}
function collectHostNodesUnderFiber(fiber) {
const roots = [];
const queue = [];
const seen = new Set();
let scanned = 0;
if (fiber?.child) queue.push(fiber.child);
while (queue.length && scanned++ < CFG.maxHostNodesScan) {
const f = queue.shift();
if (!f || seen.has(f)) continue;
seen.add(f);
if ((f.tag === FIBER_TAG.HostComponent || f.tag === FIBER_TAG.HostText) && f.stateNode) {
roots.push(f.stateNode);
}
if (f.child) queue.push(f.child);
if (f.sibling) queue.push(f.sibling);
}
const hostEls = roots.filter((n) => n && n.nodeType === 1);
const unique = [];
const seenEl = new Set();
for (const el of hostEls) {
if (!seenEl.has(el)) {
unique.push(el);
seenEl.add(el);
}
if (unique.length >= CFG.maxComponentDomRoots) break;
}
return unique;
}
// ----------------------------
// Deep capture: interactive nodes + notable child components
// ----------------------------
function isInteractiveHostFiber(f) {
if (!f || f.tag !== FIBER_TAG.HostComponent) return false;
const props = f.memoizedProps || {};
const keys = Object.keys(props);
const hasReactHandler = keys.some((k) => /^on[A-Z]/.test(k) && typeof props[k] === "function");
const el = f.stateNode;
if (!el || el.nodeType !== 1) return hasReactHandler;
const tag = el.tagName.toLowerCase();
const role = el.getAttribute("role") || "";
const ariaHaspopup = el.getAttribute("aria-haspopup");
const ariaExpanded = el.getAttribute("aria-expanded");
const dataState = el.getAttribute("data-state");
const isClickableTag = tag === "button" || tag === "a" || tag === "summary" || tag === "input";
const looksInteractive =
isClickableTag ||
role === "button" ||
role === "menuitem" ||
role === "listbox" ||
role === "combobox" ||
role === "option" ||
ariaHaspopup != null ||
ariaExpanded != null ||
dataState != null;
return hasReactHandler || looksInteractive;
}
function summarizeHandlersFromProps(props) {
const out = [];
if (!props) return out;
for (const [k, v] of Object.entries(props)) {
if (/^on[A-Z]/.test(k) && typeof v === "function") out.push(k);
}
return out.sort();
}
function pickNotableComponentName(name) {
const n = normalizeName(name);
if (!n) return false;
const keywords = [
"Dropdown", "Select", "Menu", "Popover", "Dialog", "Modal", "Tooltip",
"Combobox", "Listbox", "Option", "Tabs", "Tab", "Accordion",
"OrderBook", "Orderbook", "Book", "Depth", "Table", "Row",
"Input", "Checkbox", "Switch", "Radio",
"Trigger", "Content", "Portal",
];
return keywords.some((k) => n.includes(k));
}
function notablePropsKeys(props) {
if (!props || typeof props !== "object") return [];
const keys = Object.keys(props);
const interesting = [];
const keyHints = [
"open", "isOpen", "defaultOpen",
"value", "defaultValue", "onChange",
"onOpen", "onClose", "onSelect", "onClick",
"items", "options", "children", "render",
"side", "px", "price", "size", "sz",
"cum", "depth", "max", "min", "step",
"disabled", "loading", "active", "selected",
"id", "label", "title", "name",
];
for (const k of keys) {
if (interesting.length >= CFG.deep.maxNotablePropsKeys) break;
if (keyHints.some((h) => k.toLowerCase().includes(h.toLowerCase()))) interesting.push(k);
}
// If none match, take first few stable keys (excluding giant "children" arrays)
if (interesting.length === 0) {
for (const k of keys) {
if (interesting.length >= 12) break;
if (k === "children") continue;
interesting.push(k);
}
}
return interesting;
}
function deepCaptureUnderFiber(rootFiber) {
const interactive = [];
const notableComponents = [];
const queue = [];
const seen = new Set();
if (rootFiber?.child) queue.push(rootFiber.child);
while (queue.length) {
const f = queue.shift();
if (!f || seen.has(f)) continue;
seen.add(f);
// interactive host nodes
if (interactive.length < CFG.deep.maxInteractiveNodes && isInteractiveHostFiber(f)) {
const el = f.stateNode;
const props = f.memoizedProps || {};
const handlers = summarizeHandlersFromProps(props);
let html = "";
if (el && el.nodeType === 1) {
try { html = el.outerHTML || ""; } catch { html = ""; }
}
interactive.push({
cssPath: el && el.nodeType === 1 ? cssPath(el) : null,
tag: el && el.nodeType === 1 ? el.tagName.toLowerCase() : null,
role: el && el.nodeType === 1 ? (el.getAttribute("role") || null) : null,
ariaLabel: el && el.nodeType === 1 ? (el.getAttribute("aria-label") || null) : null,
ariaExpanded: el && el.nodeType === 1 ? (el.getAttribute("aria-expanded") || null) : null,
ariaHaspopup: el && el.nodeType === 1 ? (el.getAttribute("aria-haspopup") || null) : null,
dataState: el && el.nodeType === 1 ? (el.getAttribute("data-state") || null) : null,
textPreview: el && el.nodeType === 1 ? ((el.innerText || "").trim().replace(/\s+/g, " ").slice(0, 160) || null) : null,
reactHandlers: handlers,
// include non-function props shallowly (avoid huge dumps)
propKeys: Object.keys(props).filter((k) => typeof props[k] !== "function").slice(0, 30),
html: truncate(html, CFG.deep.maxInteractiveHtmlChars),
componentStack: (() => {
// stack from this host fiber up
const st = [];
let x = f.return, guard = 0;
while (x && guard++ < 120 && st.length < 8) {
if (isLikelyComponentFiber(x)) {
const nm = getComponentNameFromType(x.type) || "(Unknown)";
st.push(nm);
}
x = x.return;
}
return st;
})(),
});
}
// notable child component instances
if (notableComponents.length < CFG.deep.maxNotableComponents && isLikelyComponentFiber(f)) {
const name = getComponentNameFromType(f.type) || "(Unknown)";
if (pickNotableComponentName(name)) {
const props = f.memoizedProps || null;
const keys = notablePropsKeys(props);
const picked = {};
for (const k of keys) {
picked[k] = props?.[k];
}
notableComponents.push({
name,
propsPicked: safeStringify(picked, 4000),
});
}
}
if (f.child) queue.push(f.child);
if (f.sibling) queue.push(f.sibling);
}
return { interactive, notableComponents };
}
// ----------------------------
// Context extraction
// ----------------------------
function extractContextForElement(el, opts = {}) {
const ctx = {
page: { title: document.title, url: location.href },
selection: null,
react: null,
deep: null,
notes: [],
};
ctx.selection = elementSummary(el);
if (!ctx.selection) {
ctx.notes.push("No element selected.");
return ctx;
}
const fiber = getReactFiberFromDomNode(el);
if (!fiber) {
ctx.notes.push("React Fiber not found on this element (page may not be React, or internals not reachable).");
return ctx;
}
const nearestComponent = getNearestComponentFiber(fiber);
const semanticComponent = getNearestSemanticComponentFiber(fiber);
const preferSemantic = opts.preferSemantic === true;
const chosen = preferSemantic ? (semanticComponent || nearestComponent) : (nearestComponent || semanticComponent);
if (!chosen) {
ctx.notes.push("Found a Fiber node, but could not locate a nearby component Fiber.");
return ctx;
}
const chosenName = getComponentNameFromType(chosen.type) || "(Unknown)";
const chosenPropsStr = safeStringify(chosen.memoizedProps, CFG.maxPropsChars);
const nearestName = nearestComponent ? (getComponentNameFromType(nearestComponent.type) || "(Unknown)") : null;
const semanticName = semanticComponent ? (getComponentNameFromType(semanticComponent.type) || "(Unknown)") : null;
const stack = getComponentStack(chosen);
const hooks = getHookSummary(chosen);
const domRoots = collectHostNodesUnderFiber(chosen);
const domRootsHtml = domRoots
.map((n) => {
try {
if (n.nodeType === 1) return truncate(n.outerHTML || "", Math.floor(CFG.maxHtmlChars / 2));
if (n.nodeType === 3) return truncate(n.textContent || "", 1000);
return `[Node ${n.nodeName}]`;
} catch {
return "[Unserializable node]";
}
})
.filter(Boolean);
ctx.react = {
chosen: {
name: chosenName,
source: chosen._debugSource
? { fileName: chosen._debugSource.fileName, lineNumber: chosen._debugSource.lineNumber, columnNumber: chosen._debugSource.columnNumber }
: null,
props: chosenPropsStr,
},
boundary: {
nearest: nearestName,
semantic: semanticName,
used: preferSemantic ? "semantic" : "nearest",
},
stack,
hooks: safeStringify(hooks, CFG.maxPropsChars),
componentDomRoots: domRootsHtml,
};
if (!ctx.react.chosen.source) {
ctx.notes.push("No React debug source location found (common in production builds).");
}
if (nearestName && semanticName && nearestName !== semanticName) {
ctx.notes.push(`Boundary note: nearest="${nearestName}", semantic="${semanticName}", using="${ctx.react.boundary.used}".`);
}
if (opts.includeFiberTree === true) {
const fiberTree = buildFiberTree(chosen);
ctx.react.fiberTree = safeStringify(fiberTree, CFG.maxPropsChars);
}
if (opts.deepCapture === true) {
try {
ctx.deep = deepCaptureUnderFiber(chosen);
} catch (e) {
ctx.notes.push(`Deep capture failed: ${String(e)}`);
}
}
return ctx;
}
function formatForLLM(allCtx) {
// allCtx: either {mode:"single", ctx} or {mode:"multi", ctxs:[...]}
const lines = [];
const page = allCtx?.page;
if (page) {
lines.push(`PAGE`);
lines.push(`- Title: ${page.title}`);
lines.push(`- URL: ${page.url}`);
lines.push(``);
}
const ctxs = allCtx.mode === "multi" ? allCtx.ctxs : [allCtx.ctx];
ctxs.forEach((ctx, idx) => {
lines.push(`SELECTION ${idx + 1}`);
if (ctx.selection) {
lines.push(`- CSS path: ${ctx.selection.cssPath || "(n/a)"}`);
lines.push(`- tag: <${ctx.selection.tag}>`);
if (ctx.selection.id) lines.push(`- id: ${ctx.selection.id}`);
if (ctx.selection.classList?.length) lines.push(`- classes: ${ctx.selection.classList.join(" ")}`);
if (ctx.selection.role) lines.push(`- role: ${ctx.selection.role}`);
if (ctx.selection.dataTestId) lines.push(`- data-testid: ${ctx.selection.dataTestId}`);
if (ctx.selection.ariaLabel) lines.push(`- aria-label: ${ctx.selection.ariaLabel}`);
if (ctx.selection.ariaHaspopup) lines.push(`- aria-haspopup: ${ctx.selection.ariaHaspopup}`);
if (ctx.selection.ariaExpanded) lines.push(`- aria-expanded: ${ctx.selection.ariaExpanded}`);
if (ctx.selection.dataState) lines.push(`- data-state: ${ctx.selection.dataState}`);
if (ctx.selection.textPreview) lines.push(`- text preview: ${ctx.selection.textPreview}`);
lines.push(``);
lines.push(`HTML (selected node)`);
lines.push(ctx.selection.outerHTML || "(empty)");
lines.push(``);
}
if (ctx.react) {
lines.push(`REACT CONTEXT`);
lines.push(`- Boundary: nearest="${ctx.react.boundary.nearest}" semantic="${ctx.react.boundary.semantic}" using="${ctx.react.boundary.used}"`);
lines.push(`- Chosen component: ${ctx.react.chosen.name}`);
if (ctx.react.chosen.source) {
const s = ctx.react.chosen.source;
lines.push(`- Source: ${s.fileName}:${s.lineNumber}:${s.columnNumber}`);
} else {
lines.push(`- Source: (not available)`);
}
lines.push(``);
lines.push(`Component stack (nearest-first)`);
for (const item of ctx.react.stack || []) {
if (item.source?.fileName) {
lines.push(`- ${item.name} @ ${item.source.fileName}:${item.source.lineNumber}:${item.source.columnNumber}`);
} else {
lines.push(`- ${item.name}`);
}
}
lines.push(``);
lines.push(`Props (sanitized)`);
lines.push(ctx.react.chosen.props || "(none)");
lines.push(``);
lines.push(`Hook state summary (best-effort)`);
lines.push(ctx.react.hooks || "(none)");
lines.push(``);
if (ctx.react.fiberTree) {
lines.push(`Fiber subtree (best-effort)`);
lines.push(ctx.react.fiberTree);
lines.push(``);
}
if (ctx.react.componentDomRoots?.length) {
lines.push(`Component DOM root snippet(s) (best-effort)`);
ctx.react.componentDomRoots.forEach((h, i) => {
lines.push(`--- root ${i + 1} ---`);
lines.push(h);
});
lines.push(``);
}
}
if (ctx.deep) {
lines.push(`DEEP CAPTURE (nested components / dropdowns)`);
if (ctx.deep.notableComponents?.length) {
lines.push(`Notable component instances (heuristic)`);
ctx.deep.notableComponents.forEach((c, i) => {
lines.push(`- ${i + 1}. ${c.name}`);
lines.push(` propsPicked: ${c.propsPicked}`);
});
lines.push(``);
}
if (ctx.deep.interactive?.length) {
lines.push(`Interactive host nodes (heuristic)`);
ctx.deep.interactive.forEach((n, i) => {
lines.push(`- ${i + 1}. <${n.tag || "?"}> ${n.cssPath || "(no path)"}`);
if (n.role) lines.push(` role=${n.role}`);
if (n.ariaLabel) lines.push(` aria-label=${n.ariaLabel}`);
if (n.ariaHaspopup) lines.push(` aria-haspopup=${n.ariaHaspopup}`);
if (n.ariaExpanded) lines.push(` aria-expanded=${n.ariaExpanded}`);
if (n.dataState) lines.push(` data-state=${n.dataState}`);
if (n.textPreview) lines.push(` text="${n.textPreview}"`);
if (n.reactHandlers?.length) lines.push(` reactHandlers=${n.reactHandlers.join(", ")}`);
if (n.componentStack?.length) lines.push(` componentStack=${n.componentStack.join(" > ")}`);
if (n.html) {
lines.push(` html:`);
lines.push(` ${String(n.html).replace(/\n/g, "\n ")}`);
}
});
lines.push(``);
}
}
if (ctx.notes?.length) {
lines.push(`NOTES`);
ctx.notes.forEach((n) => lines.push(`- ${n}`));
lines.push(``);
}
});
lines.push(`REQUEST TO AGENT`);
lines.push(`- Recreate this UI and behavior in React.`);
lines.push(`- Pay special attention to interactive elements (dropdowns/menus/popovers): use the "DEEP CAPTURE" section to infer triggers, open/closed state, and event handlers.`);
lines.push(`- If the chosen component is still a layout primitive, use the boundary info and stack to propose a better component split (e.g., OrderBook -> Side -> Row -> SpreadRow).`);
lines.push(``);
return lines.join("\n");
}
async function copyText(text) {
try {
if (typeof GM_setClipboard === "function") {
GM_setClipboard(text, { type: "text", mimetype: "text/plain" });
return true;
}
await navigator.clipboard.writeText(text);
return true;
} catch (e) {
console.warn("[RCP] Copy failed:", e);
return false;
}
}
function setHudOpen(open) {
hud.classList.toggle("rcp__hud--open", open);
}
function setEnabled(next) {
enabled = next;
modePill.textContent = enabled ? "ON" : "OFF";
modePill.style.background = enabled ? "rgba(34,197,94,.85)" : "rgba(255,255,255,.12)";
highlight.style.display = enabled ? "block" : "none";
tooltip.style.display = enabled ? "block" : "none";
setHudOpen(true);
metaEl.textContent = enabled
? "Picker enabled. Hover to inspect, click to select."
: "Picker disabled. Toggle with Cmd/Ctrl+C.";
}
function renderSelectionList() {
if (!selections.length) {
listEl.style.display = "none";
listEl.innerHTML = "";
return;
}
listEl.style.display = "block";
listEl.innerHTML = selections
.map((s, i) => {
const nm = s.ctx?.react?.chosen?.name || "(no react)";
const path = s.ctx?.selection?.cssPath || "(no path)";
return `<div class="rcp__item"><strong>${i + 1}.</strong> ${nm}<div class="rcp__kbd">${escapeHtml(path)}</div></div>`;
})
.join("");
}
function escapeHtml(str) {
return String(str)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function clearSelection() {
selections = [];
metaEl.textContent = enabled
? "Cleared. Hover to inspect, click to select."
: "Cleared. Toggle with Cmd/Ctrl+C.";
renderSelectionList();
}
function currentOpts() {
return {
preferSemantic: !!preferSemanticEl.checked,
deepCapture: !!deepCaptureEl.checked,
includeFiberTree: !!includeFiberTreeEl.checked,
};
}
function addSelection(el, replace = false) {
const opts = currentOpts();
const ctx = extractContextForElement(el, opts);
if (replace) selections = [{ el, ctx }];
else selections.push({ el, ctx });
// keep selections bounded
if (selections.length > 8) selections = selections.slice(selections.length - 8);
const chosen = ctx?.react?.chosen?.name || "(no react)";
metaEl.textContent = `Selected (${selections.length}): ${chosen}`;
setHudOpen(true);
renderSelectionList();
}
function getActiveCtxBundle() {
if (selections.length === 0) return null;
const page = selections[0]?.ctx?.page || { title: document.title, url: location.href };
if (selections.length === 1) return { mode: "single", page, ctx: selections[0].ctx };
return { mode: "multi", page, ctxs: selections.map((s) => s.ctx) };
}
function getLastSelectionFiber() {
const last = selections[selections.length - 1];
if (!last?.el) return null;
return getReactFiberFromDomNode(last.el);
}
// ----------------------------
// Event handlers
// ----------------------------
function updateHighlightAndTooltip(el, x, y) {
if (!enabled) return;
if (!el || el === document.documentElement || el === document.body) {
highlight.style.display = "none";
tooltip.style.display = "none";
return;
}
const rect = el.getBoundingClientRect();
highlight.style.display = "block";
highlight.style.left = rect.left + "px";
highlight.style.top = rect.top + "px";
highlight.style.width = rect.width + "px";
highlight.style.height = rect.height + "px";
const fiber = getReactFiberFromDomNode(el);
const nearest = fiber ? getNearestComponentFiber(fiber) : null;
const semantic = fiber ? getNearestSemanticComponentFiber(fiber) : null;
const nearestName = nearest ? (getComponentNameFromType(nearest.type) || "(Unknown)") : "(no React fiber)";
const semanticName = semantic ? (getComponentNameFromType(semantic.type) || "(Unknown)") : null;
tooltip.innerHTML = `
<div><strong>${preferSemanticEl.checked ? semanticName || nearestName : nearestName}</strong></div>
<div class="rcp__mono">nearest: ${nearestName}${semanticName ? ` • semantic: ${semanticName}` : ""}</div>
<div class="rcp__mono">${cssPath(el)}</div>
`;
const pad = 14;
const tw = tooltip.offsetWidth || 220;
const th = tooltip.offsetHeight || 40;
const left = Math.min(window.innerWidth - tw - 8, x + pad);
const top = Math.min(window.innerHeight - th - 8, y + pad);
tooltip.style.left = Math.max(8, left) + "px";
tooltip.style.top = Math.max(8, top) + "px";
tooltip.style.display = "block";
}
function getElementUnderPointer(ev) {
const el = document.elementFromPoint(ev.clientX, ev.clientY);
if (!el) return null;
if (hud.contains(el)) return null;
if (root.contains(el) && el !== hud && !hud.contains(el)) return null;
return el;
}
document.addEventListener("mousemove", (ev) => {
if (!enabled) return;
const el = getElementUnderPointer(ev);
hoveredEl = el;
updateHighlightAndTooltip(el, ev.clientX, ev.clientY);
}, true);
document.addEventListener("click", (ev) => {
if (!enabled) return;
if (hud.contains(ev.target)) return;
ev.preventDefault();
ev.stopPropagation();
const el = getElementUnderPointer(ev) || hoveredEl;
if (!el) return;
const multi = !!multiSelectEl.checked;
const add = multi && ev.shiftKey;
addSelection(el, !add);
}, true);
document.addEventListener("keydown", async (ev) => {
const metaOrCtrl = ev.metaKey || ev.ctrlKey;
// Toggle: Cmd/Ctrl + C
const isToggle =
ev.code === CFG.toggleHotkey.code &&
metaOrCtrl === true &&
ev.shiftKey === CFG.toggleHotkey.shiftKey;
if (isToggle) {
// Keep normal copy in inputs or when selecting text
if (isEditableTarget(ev.target) || hasTextSelection()) return;
ev.preventDefault();
setEnabled(!enabled);
return;
}
// Esc
if (ev.code === "Escape") {
if (enabled) {
if (selections.length) clearSelection();
else setEnabled(false);
ev.preventDefault();
} else {
setHudOpen(false);
}
return;
}
// Copy: Cmd/Ctrl + Shift + C
const isCopy =
ev.code === CFG.copyHotkey.code &&
metaOrCtrl === true &&
ev.shiftKey === CFG.copyHotkey.shiftKey;
if (isCopy && enabled) {
if (isEditableTarget(ev.target) || hasTextSelection()) return;
ev.preventDefault();
// If nothing selected, capture hovered element
if (!selections.length && hoveredEl) addSelection(hoveredEl, true);
const bundle = getActiveCtxBundle();
if (!bundle) return;
const payload = formatForLLM(bundle);
const ok = await copyText(payload);
metaEl.textContent = ok ? "Copied context to clipboard." : "Copy failed (see console).";
setHudOpen(true);
}
}, true);
copyBtn.addEventListener("click", async () => {
const bundle = getActiveCtxBundle();
if (!bundle) {
metaEl.textContent = "Nothing selected.";
return;
}
const ok = await copyText(formatForLLM(bundle));
metaEl.textContent = ok ? "Copied context to clipboard." : "Copy failed (see console).";
});
copyHtmlBtn.addEventListener("click", async () => {
if (!selections.length) {
metaEl.textContent = "Nothing selected.";
return;
}
const html = selections.map((s, i) => `<!-- selection ${i + 1} -->\n${s.ctx?.selection?.outerHTML || ""}`).join("\n\n");
const ok = await copyText(html);
metaEl.textContent = ok ? "Copied HTML to clipboard." : "Copy failed (see console).";
});
copyJsonBtn.addEventListener("click", async () => {
const bundle = getActiveCtxBundle();
if (!bundle) {
metaEl.textContent = "Nothing selected.";
return;
}
const ok = await copyText(safeStringify(bundle, 200000));
metaEl.textContent = ok ? "Copied JSON to clipboard." : "Copy failed (see console).";
});
selectParentBtn.addEventListener("click", () => {
if (!selections.length) {
metaEl.textContent = "Nothing selected.";
return;
}
const last = selections[selections.length - 1];
const el = last?.el;
if (!el) return;
const fiber = getReactFiberFromDomNode(el);
if (!fiber) {
metaEl.textContent = "No React fiber for last selection.";
return;
}
const opts = currentOpts();
const nearest = getNearestComponentFiber(fiber);
const semantic = getNearestSemanticComponentFiber(fiber);
// Move selection upward: if currently used semantic, go one semantic parent up; otherwise go to semantic.
const preferSemantic = !!preferSemanticEl.checked;
const base = preferSemantic ? semantic : nearest;
if (!base) {
metaEl.textContent = "Could not find component fiber.";
return;
}
// Find next semantic parent above current semantic (skipping primitives/minified)
let f = base.return, guard = 0;
let next = null;
while (f && guard++ < 500) {
if (isLikelyComponentFiber(f)) {
const name = getComponentNameFromType(f.type) || "(Unknown)";
if (!isPrimitiveName(name)) { next = f; break; }
}
f = f.return;
}
if (!next) {
metaEl.textContent = "No higher semantic parent found.";
return;
}
// Pick a DOM node under that parent as the “anchor” for selection context:
// Use first host node we can find under next fiber.
const roots = collectHostNodesUnderFiber(next);
const anchor = roots?.[0] || el;
addSelection(anchor, true);
metaEl.textContent = `Selected parent semantic: ${getComponentNameFromType(next.type) || "(Unknown)"}`;
});
clearBtn.addEventListener("click", () => clearSelection());
// Keep selection list updated when toggles change (deep capture affects ctx)
const onOptsChange = () => {
if (!selections.length) return;
// Recompute ctx for existing selection elements with new opts
const opts = currentOpts();
selections = selections.map((s) => ({ el: s.el, ctx: extractContextForElement(s.el, opts) }));
renderSelectionList();
metaEl.textContent = `Updated capture options for ${selections.length} selection(s).`;
};
preferSemanticEl.addEventListener("change", onOptsChange);
deepCaptureEl.addEventListener("change", onOptsChange);
includeFiberTreeEl.addEventListener("change", onOptsChange);
// Initial state
setHudOpen(true);
setEnabled(false);
metaEl.textContent = "Ready. Toggle with Cmd/Ctrl+C.";
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment