Summary
Let visitors point at the actual UI they are talking about, instead of describing it in words. The widget today is a text box plus pagePath / pageUrl. An element picker is the single biggest jump in agent accuracy for visual/layout feedback (“this button”, “this card on mobile”).
This is table stakes for Marker.io, BugHerd, Usersnap, and similar visual feedback tools. feedback2code’s differentiator is that we turn that pin into a PR, so the selector + snippet should go all the way into the agent prompt and the PR body.
Current behavior
The embed is a cross-origin iframe, not an in-page DOM overlay:
- Parent site loads
/widget/{wid} (app/widget/[wid]/route.ts).
buildParentEmbedScript in lib/widget-embed.ts injects a full-viewport iframe from /embed/frame?w=….
- The iframe posts
{ f2c: "f2c-frame-ready" }; the parent replies with { f2c: "parent", href, pathname } only.
- Submit payload to
POST /f is { w, text, pageUrl, pagePath, parentOrigin, turnstileToken }.
WidgetFeedback stores body, pageUrl, pagePath (prisma/schema.prisma). Those two location fields are already passed into buildOpencodeFeedbackPrompt and buildFeedbackPrBody.
The iframe cannot read the parent DOM. Any picker must run in the parent script (or a same-origin overlay injected there) and talk to the iframe over postMessage.
Problem
Clients rarely know CSS class names. They say “the orange button on the hero”. The agent already gets pagePath (good) but still has to guess which node. Wrong file / wrong component is the expensive failure mode (wasted sandbox + quota).
Proposed design
UX
- In the widget panel, a control: Pick element (or “Point at the page”).
- Clicking it:
- Collapses or
pointer-events: nones the iframe so clicks hit the parent page.
- Parent overlay highlights hovered nodes (outline + label: tag, id, a short class, or accessible name).
- Click selects; Esc / second click on empty cancels.
- Selected target appears in the panel as a chip:
button.cta / Nav › Pricing plus a tiny screenshot thumbnail if we capture one.
- Submit still requires text (keep the 2000-char body). Picker is optional context, not a replacement for the request.
Data to capture (parent → iframe → POST /f)
Keep the payload bounded and reviewable:
| Field |
Why |
selector |
Stable-ish CSS path (prefer id, data-testid, aria-label; fall back to nth-of-type path). Cap length (~500). |
elementTag / elementText |
Short visible text (truncated) so the PR is human-readable. |
boundingRect |
{ x, y, w, h, vw, vh } so the agent knows layout context / likely breakpoint. |
outerHtml |
Truncated (~2–4 KB), sanitized (no <script>, no inline event handlers). |
screenshot (v2) |
Optional cropped PNG of the element; do not block v1 on this. |
Store on WidgetFeedback (new nullable JSON column, e.g. elementTarget Json?) rather than many scalar columns.
Agent + PR
- Prompt (
lib/feedback-agent/prompt.ts): add a block “Visitor selected this element…” with selector, text, and truncated HTML. Instruct the agent to start from that node / corresponding component, not a repo-wide hunt.
- PR body (
lib/feedback-agent/feedback-pr-copy.ts): a table row Selected element + fenced selector, same as we already do for page path / submitted URL.
Parent ↔ iframe protocol
Extend the existing f2c message namespace (do not use * origin for secrets; keep origin checks as today):
- iframe → parent:
{ f2c: "pick-start" } / { f2c: "pick-cancel" }
- parent → iframe:
{ f2c: "pick-result", target: { … } } / { f2c: "pick-cancelled" }
While picking, shrink the iframe hit target (e.g. height to the FAB) so the page is clickable. Restore on cancel/select.
Assumptions
- v1 is selector + text + truncated HTML, not full-page screenshots. Screenshots are a follow-up (storage, PII, bandwidth).
- Picker is on by default for every widget. No plan gate for v1 — accuracy is the product.
- We generate selectors ourselves; we do not depend on the host app exposing
data-f2c attributes (nice-to-have later in docs).
- Shadow DOM / cross-origin iframes inside the client site: skip or pick the host element; document the limitation.
- Same-origin vs cross-origin parent: parent script already runs in the client page, so picker works even when the iframe origin is
feedback2code.dev.
Security / privacy
- Treat
outerHtml as untrusted. Strip scripts, on*, srcdoc, javascript: URLs before persist and before prompt injection.
- Do not capture password/inputs (
input[type=password], payment fields). If the hit target is in a sensitive field, select the nearest non-sensitive ancestor and label it as such.
- Selector + HTML go into GitHub PRs (often public). Warn in configure UI: “Element HTML may appear on the PR.”
- Turnstile and domain allowlist (
lib/widget-resolve.ts) unchanged.
Acceptance criteria
Out of scope
- Annotated screenshots, arrows, consoles logs, network HAR (Marker.io parity).
- Click-maps / heatmaps.
- Multi-element select.
Implementation notes
Primary files:
lib/widget-embed.ts — parent script (picker + postMessage) and iframe UI
app/f/route.ts — validate/store payload
prisma/schema.prisma — WidgetFeedback.elementTarget
lib/feedback-agent/prompt.ts, lib/feedback-agent/feedback-pr-copy.ts
components/repo/repo-feedbacks-panel.tsx — show chip on the dashboard
Widget JS is cached max-age=300 (app/widget/[wid]/route.ts); iframe is no-store. Plan for a protocol version on messages so mixed old/new parents fail closed.
See also
- Widget theme (accent/position will share the parent-script config channel)
- Page-path allowlist (picker should not run on disabled paths)
- Iterate on feedback PRs (a follow-up could re-use the same pinned element)
Summary
Let visitors point at the actual UI they are talking about, instead of describing it in words. The widget today is a text box plus
pagePath/pageUrl. An element picker is the single biggest jump in agent accuracy for visual/layout feedback (“this button”, “this card on mobile”).This is table stakes for Marker.io, BugHerd, Usersnap, and similar visual feedback tools. feedback2code’s differentiator is that we turn that pin into a PR, so the selector + snippet should go all the way into the agent prompt and the PR body.
Current behavior
The embed is a cross-origin iframe, not an in-page DOM overlay:
/widget/{wid}(app/widget/[wid]/route.ts).buildParentEmbedScriptinlib/widget-embed.tsinjects a full-viewport iframe from/embed/frame?w=….{ f2c: "f2c-frame-ready" }; the parent replies with{ f2c: "parent", href, pathname }only.POST /fis{ w, text, pageUrl, pagePath, parentOrigin, turnstileToken }.WidgetFeedbackstoresbody,pageUrl,pagePath(prisma/schema.prisma). Those two location fields are already passed intobuildOpencodeFeedbackPromptandbuildFeedbackPrBody.The iframe cannot read the parent DOM. Any picker must run in the parent script (or a same-origin overlay injected there) and talk to the iframe over
postMessage.Problem
Clients rarely know CSS class names. They say “the orange button on the hero”. The agent already gets
pagePath(good) but still has to guess which node. Wrong file / wrong component is the expensive failure mode (wasted sandbox + quota).Proposed design
UX
pointer-events: nones the iframe so clicks hit the parent page.button.cta/Nav › Pricingplus a tiny screenshot thumbnail if we capture one.Data to capture (parent → iframe →
POST /f)Keep the payload bounded and reviewable:
selectorid,data-testid,aria-label; fall back to nth-of-type path). Cap length (~500).elementTag/elementTextboundingRect{ x, y, w, h, vw, vh }so the agent knows layout context / likely breakpoint.outerHtml<script>, no inline event handlers).screenshot(v2)Store on
WidgetFeedback(new nullable JSON column, e.g.elementTarget Json?) rather than many scalar columns.Agent + PR
lib/feedback-agent/prompt.ts): add a block “Visitor selected this element…” with selector, text, and truncated HTML. Instruct the agent to start from that node / corresponding component, not a repo-wide hunt.lib/feedback-agent/feedback-pr-copy.ts): a table row Selected element + fenced selector, same as we already do for page path / submitted URL.Parent ↔ iframe protocol
Extend the existing
f2cmessage namespace (do not use*origin for secrets; keep origin checks as today):{ f2c: "pick-start" }/{ f2c: "pick-cancel" }{ f2c: "pick-result", target: { … } }/{ f2c: "pick-cancelled" }While picking, shrink the iframe hit target (e.g. height to the FAB) so the page is clickable. Restore on cancel/select.
Assumptions
data-f2cattributes (nice-to-have later in docs).feedback2code.dev.Security / privacy
outerHtmlas untrusted. Strip scripts,on*,srcdoc, javascript: URLs before persist and before prompt injection.input[type=password], payment fields). If the hit target is in a sensitive field, select the nearest non-sensitive ancestor and label it as such.lib/widget-resolve.ts) unchanged.Acceptance criteria
Out of scope
Implementation notes
Primary files:
lib/widget-embed.ts— parent script (picker + postMessage) and iframe UIapp/f/route.ts— validate/store payloadprisma/schema.prisma—WidgetFeedback.elementTargetlib/feedback-agent/prompt.ts,lib/feedback-agent/feedback-pr-copy.tscomponents/repo/repo-feedbacks-panel.tsx— show chip on the dashboardWidget JS is cached
max-age=300(app/widget/[wid]/route.ts); iframe isno-store. Plan for a protocol version on messages so mixed old/new parents fail closed.See also