diff --git a/.codex/skills/inspect-elements/SKILL.md b/.codex/skills/inspect-elements/SKILL.md new file mode 100644 index 00000000..06ff3919 --- /dev/null +++ b/.codex/skills/inspect-elements/SKILL.md @@ -0,0 +1,94 @@ +--- +name: inspect-elements +description: Resolve on-screen 5chan DOM elements to React source files, line numbers, component names, and ownership stacks using the app's dev-only element-source helpers and playwright-cli. Use when Codex needs to inspect a page element, map a snapshot ref to source code, confirm which component rendered a node, or follow up after $profile-browsing finds a rerender hotspot and needs file-level attribution. +--- + +# Inspect Elements + +Use this skill to jump from a concrete DOM node in the running 5chan app to the React file and component stack that produced it. + +## Prerequisites + +- Dev server running at `http://5chan.localhost:1355` +- `playwright-cli` installed +- Use the local dev app, not production. The element-source helpers are only exposed in dev mode. + +## Quick workflow + +1. Open the target route with `playwright-cli`. +2. Run `playwright-cli snapshot` and choose the relevant element ref. +3. Resolve that ref through the app helper: + +```bash +playwright-cli -s=inspect eval "async el => JSON.stringify(await window.__ELEMENT_SOURCE__.resolve(el))" e7 +``` + +The result includes: + +- `source`: the most useful file/line match for the element +- `componentName`: the nearest meaningful React component +- `stack`: ownership stack from the concrete node upward +- `tagName`: the underlying DOM tag + +## Session setup + +```bash +playwright-cli -s=inspect open http://5chan.localhost:1355 +playwright-cli -s=inspect goto http://5chan.localhost:1355/all +playwright-cli -s=inspect eval "window.__ELEMENT_SOURCE__?.ready ?? false" +playwright-cli -s=inspect snapshot +``` + +If `ready` is `false`, wait a moment and evaluate again. If `window.__ELEMENT_SOURCE__?.error` is set, report that error instead of continuing. + +## Resolve strategies + +Prefer snapshot refs because they target the exact live DOM node you just inspected. + +### Snapshot ref + +```bash +playwright-cli -s=inspect eval "async el => JSON.stringify(await window.__ELEMENT_SOURCE__.resolve(el))" e7 +``` + +### Selector + +Use this only when the element is easy to target and a snapshot ref is not practical. + +```bash +playwright-cli -s=inspect eval "JSON.stringify(await window.__ELEMENT_SOURCE__.resolveBySelector('[data-testid=\"composer\"]'))" +``` + +### Screen coordinates + +Useful when you have a screenshot or a visually obvious hotspot. + +```bash +playwright-cli -s=inspect eval "JSON.stringify(await window.__ELEMENT_SOURCE__.resolveAtPoint(320, 420))" +``` + +## Format the ownership stack + +```bash +playwright-cli -s=inspect eval "async el => { const info = await window.__ELEMENT_SOURCE__.resolve(el); return JSON.stringify({ ...info, formattedStack: window.__ELEMENT_SOURCE__.formatStack(info.stack, 5) }); }" e7 +``` + +Use `formattedStack` when you need a short, readable trace for the final report. + +## Profiling follow-up + +When `$profile-browsing` reports a hot route or rerender-heavy area: + +1. Reopen the route in a fresh playwright session. +2. Snapshot the concrete list item, card, modal, or toolbar node that looks relevant. +3. Resolve it with `window.__ELEMENT_SOURCE__.resolve(...)`. +4. Use `source.filePath` as the direct edit target and `stack` to understand parent ownership. + +This is a complement to `react-scan`, not a replacement. `react-scan` tells you which components rerender too often. `inspect-elements` tells you which exact source file produced the node you are looking at. + +## Rules + +- Prefer snapshot refs over brittle selectors. +- Inspect the actual node the user cares about, not a distant wrapper, unless wrappers are the suspected problem. +- If `source` is null but `stack` exists, use the first useful stack frame rather than guessing. +- If both `source` and `stack` are empty, report that the node could not be resolved and pick a nearby parent element instead. diff --git a/.codex/skills/inspect-elements/agents/openai.yaml b/.codex/skills/inspect-elements/agents/openai.yaml new file mode 100644 index 00000000..944982d9 --- /dev/null +++ b/.codex/skills/inspect-elements/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Inspect Elements" + short_description: "Map DOM nodes to source files" + default_prompt: "Use $inspect-elements to map a 5chan page element to its React source file and component stack." diff --git a/.codex/skills/profile-browsing/SKILL.md b/.codex/skills/profile-browsing/SKILL.md index dd26ce6b..0b376cf6 100644 --- a/.codex/skills/profile-browsing/SKILL.md +++ b/.codex/skills/profile-browsing/SKILL.md @@ -121,6 +121,16 @@ Collect structured output from each subagent and merge: | react-scan: component with >30 renders | Missing memoization or unstable references | `useMemo`/`useCallback`, check parent renders | | react-scan: component with >50ms time | Expensive render function | Split component, move work out of render | +## Element-source follow-up + +When `react-scan` identifies a rerender hotspot but you still need the exact file behind a concrete DOM node, hand off to `$inspect-elements`. + +```bash +playwright-cli -s=prof-followup eval "async el => JSON.stringify(await window.__ELEMENT_SOURCE__.resolve(el))" e7 +``` + +Use `source.filePath` as the direct edit target and `stack` to understand which parent components own the node. + ## Step 5: Cleanup After profiling is complete and the report is delivered, verify no orphaned processes were left behind: diff --git a/.cursor/skills/inspect-elements/SKILL.md b/.cursor/skills/inspect-elements/SKILL.md new file mode 100644 index 00000000..06ff3919 --- /dev/null +++ b/.cursor/skills/inspect-elements/SKILL.md @@ -0,0 +1,94 @@ +--- +name: inspect-elements +description: Resolve on-screen 5chan DOM elements to React source files, line numbers, component names, and ownership stacks using the app's dev-only element-source helpers and playwright-cli. Use when Codex needs to inspect a page element, map a snapshot ref to source code, confirm which component rendered a node, or follow up after $profile-browsing finds a rerender hotspot and needs file-level attribution. +--- + +# Inspect Elements + +Use this skill to jump from a concrete DOM node in the running 5chan app to the React file and component stack that produced it. + +## Prerequisites + +- Dev server running at `http://5chan.localhost:1355` +- `playwright-cli` installed +- Use the local dev app, not production. The element-source helpers are only exposed in dev mode. + +## Quick workflow + +1. Open the target route with `playwright-cli`. +2. Run `playwright-cli snapshot` and choose the relevant element ref. +3. Resolve that ref through the app helper: + +```bash +playwright-cli -s=inspect eval "async el => JSON.stringify(await window.__ELEMENT_SOURCE__.resolve(el))" e7 +``` + +The result includes: + +- `source`: the most useful file/line match for the element +- `componentName`: the nearest meaningful React component +- `stack`: ownership stack from the concrete node upward +- `tagName`: the underlying DOM tag + +## Session setup + +```bash +playwright-cli -s=inspect open http://5chan.localhost:1355 +playwright-cli -s=inspect goto http://5chan.localhost:1355/all +playwright-cli -s=inspect eval "window.__ELEMENT_SOURCE__?.ready ?? false" +playwright-cli -s=inspect snapshot +``` + +If `ready` is `false`, wait a moment and evaluate again. If `window.__ELEMENT_SOURCE__?.error` is set, report that error instead of continuing. + +## Resolve strategies + +Prefer snapshot refs because they target the exact live DOM node you just inspected. + +### Snapshot ref + +```bash +playwright-cli -s=inspect eval "async el => JSON.stringify(await window.__ELEMENT_SOURCE__.resolve(el))" e7 +``` + +### Selector + +Use this only when the element is easy to target and a snapshot ref is not practical. + +```bash +playwright-cli -s=inspect eval "JSON.stringify(await window.__ELEMENT_SOURCE__.resolveBySelector('[data-testid=\"composer\"]'))" +``` + +### Screen coordinates + +Useful when you have a screenshot or a visually obvious hotspot. + +```bash +playwright-cli -s=inspect eval "JSON.stringify(await window.__ELEMENT_SOURCE__.resolveAtPoint(320, 420))" +``` + +## Format the ownership stack + +```bash +playwright-cli -s=inspect eval "async el => { const info = await window.__ELEMENT_SOURCE__.resolve(el); return JSON.stringify({ ...info, formattedStack: window.__ELEMENT_SOURCE__.formatStack(info.stack, 5) }); }" e7 +``` + +Use `formattedStack` when you need a short, readable trace for the final report. + +## Profiling follow-up + +When `$profile-browsing` reports a hot route or rerender-heavy area: + +1. Reopen the route in a fresh playwright session. +2. Snapshot the concrete list item, card, modal, or toolbar node that looks relevant. +3. Resolve it with `window.__ELEMENT_SOURCE__.resolve(...)`. +4. Use `source.filePath` as the direct edit target and `stack` to understand parent ownership. + +This is a complement to `react-scan`, not a replacement. `react-scan` tells you which components rerender too often. `inspect-elements` tells you which exact source file produced the node you are looking at. + +## Rules + +- Prefer snapshot refs over brittle selectors. +- Inspect the actual node the user cares about, not a distant wrapper, unless wrappers are the suspected problem. +- If `source` is null but `stack` exists, use the first useful stack frame rather than guessing. +- If both `source` and `stack` are empty, report that the node could not be resolved and pick a nearby parent element instead. diff --git a/.cursor/skills/inspect-elements/agents/openai.yaml b/.cursor/skills/inspect-elements/agents/openai.yaml new file mode 100644 index 00000000..944982d9 --- /dev/null +++ b/.cursor/skills/inspect-elements/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Inspect Elements" + short_description: "Map DOM nodes to source files" + default_prompt: "Use $inspect-elements to map a 5chan page element to its React source file and component stack." diff --git a/.cursor/skills/profile-browsing/SKILL.md b/.cursor/skills/profile-browsing/SKILL.md index dd26ce6b..0b376cf6 100644 --- a/.cursor/skills/profile-browsing/SKILL.md +++ b/.cursor/skills/profile-browsing/SKILL.md @@ -121,6 +121,16 @@ Collect structured output from each subagent and merge: | react-scan: component with >30 renders | Missing memoization or unstable references | `useMemo`/`useCallback`, check parent renders | | react-scan: component with >50ms time | Expensive render function | Split component, move work out of render | +## Element-source follow-up + +When `react-scan` identifies a rerender hotspot but you still need the exact file behind a concrete DOM node, hand off to `$inspect-elements`. + +```bash +playwright-cli -s=prof-followup eval "async el => JSON.stringify(await window.__ELEMENT_SOURCE__.resolve(el))" e7 +``` + +Use `source.filePath` as the direct edit target and `stack` to understand which parent components own the node. + ## Step 5: Cleanup After profiling is complete and the report is delivered, verify no orphaned processes were left behind: diff --git a/package.json b/package.json index 5f67342e..6c0092ec 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ "cz-conventional-changelog": "3.3.0", "decompress": "4.2.1", "electron": "36.9.5", + "element-source": "0.0.4", "husky": "4.3.8", "isomorphic-fetch": "3.0.0", "jsdom": "28.1.0", diff --git a/src/lib/react-scan.ts b/src/lib/react-scan.ts index 7716137f..000494a2 100644 --- a/src/lib/react-scan.ts +++ b/src/lib/react-scan.ts @@ -4,6 +4,75 @@ if (import.meta.env.DEV) { enabled: true, showToolbar: !(window as any).__PROFILING__, }); + + const notReady = async () => ({ + error: 'element-source is not ready yet.', + }); + + const elementSourceApi: any = { + ready: false, + error: null, + resolve: notReady, + resolveBySelector: async () => ({ + error: 'element-source is not ready yet.', + }), + resolveAtPoint: async () => ({ + error: 'element-source is not ready yet.', + }), + formatStack: () => '', + }; + (window as any).__getReactScanReport = getReport; + (window as any).__ELEMENT_SOURCE__ = elementSourceApi; + + import('element-source') + .then(({ formatStack, resolveElementInfo }) => { + const resolve = async (node: unknown) => { + if (!(node instanceof Element)) { + return { + error: 'Expected a DOM Element.', + }; + } + + try { + const info = await resolveElementInfo(node); + return { + ...info, + available: Boolean(info.source || info.stack.length || info.componentName), + }; + } catch (error) { + return { + error: error instanceof Error ? error.message : String(error), + }; + } + }; + + Object.assign(elementSourceApi, { + ready: true, + resolve, + resolveBySelector: async (selector: string) => { + const element = document.querySelector(selector); + if (!(element instanceof Element)) { + return { + error: `No element matched selector: ${selector}`, + }; + } + return resolve(element); + }, + resolveAtPoint: async (x: number, y: number) => { + const element = document.elementFromPoint(x, y); + if (!(element instanceof Element)) { + return { + error: `No element found at point (${x}, ${y})`, + }; + } + return resolve(element); + }, + formatStack: (stack: unknown, maxLines = 3) => (Array.isArray(stack) ? formatStack(stack as any, maxLines) : ''), + }); + }) + .catch((error) => { + elementSourceApi.error = error instanceof Error ? error.message : String(error); + }); }); } diff --git a/yarn.lock b/yarn.lock index 5cdc69f9..275f894e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5671,6 +5671,13 @@ bippy@^0.5.28: dependencies: "@types/react-reconciler" "^0.28.9" +bippy@^0.5.32: + version "0.5.32" + resolved "https://registry.yarnpkg.com/bippy/-/bippy-0.5.32.tgz#60cbed767572e7cecda2f7b7d860f7f0ae6b0874" + integrity sha512-yt1mC8eReTxjfg41YBZdN4PvsDwHFWxltoiQX0Q+Htlbf41aSniopb7ECZits01HwNAvXEh69RGk/ImlswDTEw== + dependencies: + "@types/react-reconciler" "^0.28.9" + bl@^1.0.0: version "1.2.3" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" @@ -7330,6 +7337,13 @@ electron@36.9.5: "@types/node" "^22.7.7" extract-zip "^2.0.1" +element-source@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/element-source/-/element-source-0.0.4.tgz#6594b5bce4cedc0ed99213aa47b532a8803110f8" + integrity sha512-4feZB3FhezD6UBGj6h8OO8axyPaz8kcJrpJzGTCWyLeZ0ldHgiE2z6i7Th0d5V2IlHUFNoXR7Tb2dfbZIgih0A== + dependencies: + bippy "^0.5.32" + elementtree@^0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/elementtree/-/elementtree-0.1.7.tgz#9ac91be6e52fb6e6244c4e54a4ac3ed8ae8e29c0"