Skip to content

feat(fork): design change requests carry component props and ask for scope judgment - #79

Merged
NoahHendrickson merged 3 commits into
customfrom
fork/design-mode-component-props
Aug 9, 2026
Merged

feat(fork): design change requests carry component props and ask for scope judgment#79
NoahHendrickson merged 3 commits into
customfrom
fork/design-mode-component-props

Conversation

@NoahHendrickson

Copy link
Copy Markdown
Owner

Problem

Design mode's change requests told the agent to scope every edit to its call site and to skip anything that would touch a shared component. Selecting a Button and nudging its padding therefore produced one-off className overrides that drift from the design system — and the agent couldn't see which variant it was looking at, so it had no basis to do better.

Fix

Two halves of one concern: give the agent license to make the component-vs-one-off call, and give it the evidence to ground that call.

Scope guardrail (engine/vendor/shared/guardrails.ts): SCOPE_GUARDRAIL now asks the agent to judge whether the user means the component everywhere or just this instance, prefer project tokens and suggested utilities over hard-coded values, and state which scope it chose instead of pausing to ask. forkDesignModeCssOrigin.test.ts pins the wording against upstream re-syncs, the same way NO_PREVIEW_GUARDRAIL is pinned.

Component props snapshot: the desktop resolver reads the rendering component's primitive props off the composite fiber whose displayName matches the component name react-grab reported — no name match, no props, so props are never attributed to the wrong component. Requests now read:

## 1. <button> — src/components/Button.tsx:24:5
Rendered by: `<Button>` — props: `variant="ghost" size="sm" disabled`
- padding-inline: 12px → 16px — change `px-3` → `px-4`

The snapshot is primitives-only (strings/numbers/booleans, children excluded, 12 entries max, 64 chars per string, control characters rejected) and is validated three times on its way to the request: normalizeResolvedProps on the preload side of the page-shared global, its guest twin readDesignProps (new designProps.ts, the cssOrigin.ts fence-crossing pattern) before stamping data-t3-props, and again when the request builder parses that page-controlled attribute back. It only ever rides beside a component name.

bippy is imported by its root export on purpose: bippy/core's published d.ts exports mangled names, and the root's hook-install side effect is guarded double work beside react-grab's bundled copy (verified in both dists). Same pinned version (0.5.41). Desktop-preview-only by architecture, exactly like component names already are.

Verified

  • apps/desktop: 23 resolver tests (7 new for props normalization and carrying), typecheck clean
  • apps/web: 10 new designProps tests, manifest + design-mode bundle guard tests, lint-cleanliness guard, engine-island tsc, app typecheck — all green

Fable 5 via Claude Code.

🤖 Generated with Claude Code

…scope judgment

Design mode's change requests told the agent to scope every edit to its
call site and skip anything touching a shared component — so selecting a
Button and nudging its padding produced one-off className overrides that
drift from the design system, and the agent could not see which variant
it was even looking at.

Two halves, one concern:

- SCOPE_GUARDRAIL now asks the agent to judge component-wide vs one-off
  intent from the request's context, prefer project tokens over
  hard-coded values, and disclose which scope it chose. A guard test
  pins the wording against upstream re-syncs, like NO_PREVIEW's.

- The desktop resolver snapshots the rendering component's primitive
  props off the composite fiber whose displayName matches the reported
  component name (bippy, same pinned version react-grab bundles). The
  snapshot is validated on both sides of the page-shared global, rides
  the element as data-t3-props, is re-validated when the request builder
  reads the page-controlled attribute back, and renders as
  "Rendered by `<Button>` — props: `variant="ghost" size="sm"`".

Fable 5 via Claude Code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 9, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thermo-nuclear code quality review — do not approve as-is.

Three structural issues fail the approval bar: non-atomic stale DOM stamps for the new props evidence, avoidable source-duplication of the props normalizer, and a first-party bippy root import that pulls DevTools-hook side effects for two fiber helpers.

SCOPE_GUARDRAIL rewrite + guard pin look fine. Triple validation at boundaries earns its keep — the problem is forking the implementation, not calling it more than once. request.ts stays under 1k; no file-size blocker.

Ordered by review priority; details on the inline threads.

Open in Web View Automation 

Sent by Cursor Automation: Thermo-nuclear PR review

Comment thread apps/web/src/custom/designMode/engine/nativeSource.ts
Comment thread apps/web/src/custom/designMode/designProps.ts
Comment thread apps/desktop/src/preview/DesignSourceResolver.ts
Comment thread apps/desktop/src/preview/DesignSourceResolver.ts
…ps atomically

The preview-pick-preload entry only marked react-grab as alwaysBundle, so the
new bippy import stayed external. That webview runs sandboxed, where require()
cannot resolve npm packages — the packed preload threw before installing either
the picker or the design source resolver, taking the whole preview-pick path
down in packaged builds. Verified against the artifact: require("bippy") is
gone, only Electron's synthesized require remains. A guard test now pins the
bundling rule so a config sweep cannot silently reintroduce it.

Props stamps are also atomic with the resolution now: a resolve that comes back
nameless clears both data-t3-component and data-t3-props instead of leaving the
previous component's vocabulary on the element as current evidence.
@NoahHendrickson

Copy link
Copy Markdown
Owner Author

Rebased onto `custom` (now carrying #77 and #78) and addressed the open review threads in `dfdfe3761`.

P1 — bippy left external in the packaged preload. Real, and it would have taken the whole preview-pick/design-mode path down in desktop builds. Added `bippy` to the `alwaysBundle` predicate for the `preview-pick-preload` entry, matching the `@clerk/electron` entry directly above. Verified against the artifact both ways with `vp pack`:

external requires in `preview-pick-preload.cjs`
before `require("bippy")`, `require("electron")`
after `require("electron")`

Electron synthesizes that last one for sandboxed preloads, so it is the expected floor. Pinned with a guard test in `forkDesignMode.test.ts`, confirmed to fail when the predicate is reverted.

Props stamps were not atomic. A resolve that came back nameless left both `data-t3-component` and `data-t3-props` in place, so the request could render a previous component's vocabulary as current evidence — mild for the name alone, material once props feed the scope judgment. The if (componentName) branch now has an else that clears both. Also replaced the (raw as { props?: unknown }) cast with a readProps helper that narrows off unknown the way readComponentName does.

Declined, with reasoning in-thread: sharing the props normalizer across apps/desktop and apps/web (would require a new packages/ entry — full-stack scope this PR should not absorb, and it would leave the existing normalizeFilePath/normalizeComponentName twins half-converted), and dropping the bippy dependency (react-grab exposes the host fiber, not the composite one componentName was derived from, so reading context.fiber.memoizedProps would yield the DOM node's props — details and the dist trace are in that thread, including a correction to my first reply).

Manifest now watches apps/desktop/vite.config.ts and apps/desktop/package.json, since a config or dep sweep on either breaks the packaged preload silently.

Verification: vp test run src/custom/designMode src/__fork_guards__ from apps/web → 46 files / 395 tests pass; typecheck clean on web and desktop; vp pack clean.

Model: Claude Opus 5 (1M context), harness: Claude Code.

@NoahHendrickson
NoahHendrickson merged commit 1ce1588 into custom Aug 9, 2026
10 checks passed
@NoahHendrickson
NoahHendrickson deleted the fork/design-mode-component-props branch August 9, 2026 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant