Skip to content

feat: point-and-change — hold Option to see (and change) which layer owns any pixel - #631

Merged
crs48 merged 7 commits into
mainfrom
claude/0399-point-and-change-xnet-editing-itself
Jul 26, 2026
Merged

feat: point-and-change — hold Option to see (and change) which layer owns any pixel#631
crs48 merged 7 commits into
mainfrom
claude/0399-point-and-change-xnet-editing-itself

Conversation

@crs48

@crs48 crs48 commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Implements W1–W3 of exploration 0399. Marked [-] — 25/30 items; the four deferred ones are listed in the doc with reasons.

Stacked on #630 (explorations 0397–0399), so it targets that branch. Retarget to main once #630 merges.

What you can do now

Hold and hover: the element under the pointer is outlined and labelled with the layer that owns it and one sentence on what changing it would move. -click opens a panel that leads with that sentence, then offers the smallest control that can make the change.

Pointed at Verdict Control
Something painted from a theme token Lane 1 · global scope the token's value, one Undo
A registered panel Lane 1 · surface scope that slot's own palette commands
A plugin surface Lane 2 explanation only (see deferred)
packages/* / apps/* source Lane 3 explanation only (dev-loop is node-side)
packages/{sync,crypto,identity,data} refused, with a reason none

Verified live in the running app, not just in tests: all five verdicts, plus apply-and-undo writing and clearing a real token.

Three decisions worth reviewing

The stamp is a JSX runtime shim, not a Babel plugin. A JSXOpeningElement visitor never fires here — @vitejs/plugin-react doesn't transform JSX under the automatic runtime; esbuild does, earlier. By the time Babel runs, every <div> is already a jsxDEV(...) call. So jsxImportSource points at a shim that reads the source argument esbuild already passes. Side benefit: it doesn't depend on React storing it, so it survives React 19 dropping _debugSource.

Lane ≠ blast radius. A Lane 1 token edit is the cheapest mechanism available and repaints the whole app; a Lane 3 edit drags a PR through CI and may move one button. Resolution carries both, and the UI shows the scope sentence — otherwise "make this blue" quietly restyles everything.

No per-element override store. packages/ui/src/theme/tokens.ts is the token contract as plain functions; ThemeProvider delegates to it. Keyed by custom-property name, never by element — a per-element map would be a second source of truth and permanently fork the app's appearance from its stylesheet (the documented Plasmic failure mode).

Safety

  • The stamp can never ship. Three guard tiers in scripts/guard-no-source-stamp.mjs: the config must gate on command === 'serve', no source file may import the shim by name, and no built asset may contain the attribute. Verified against a real production build (0 hits) and verified to go red when deliberately broken. Wired into CI.
  • Kernel packages are refused twice — in the browser resolver and again in assertEditable(), which re-derives from the source ref. A test forges an allowed: true kernel resolution; it is still refused.
  • previewWorktree() refuses the session's own port. Sharing it is how a broken edit takes down the surface you're editing from.
  • PRs open as drafts and nothing merges.
  • Injection boundary: lane3Prompt() takes a Resolution (source location) and the user's instruction. The pointed element's rendered text — workspace content, possibly authored by someone else — has no field to travel through.

Four bugs found while building, all now regression-tested

  1. A bare function passed to setUndo — React treats it as an updater and called it, un-applying the change immediately.
  2. Token attribution compared HSL triples (0 0% 98%) against computed rgb(...); they can never match, so Lane 1 silently did nothing.
  3. Tailwind's preflight sets border-color on every element, so an unguarded border check made nearly the whole tree claim Lane 1 and hid Lanes 2 and 3. Attribution now requires a drawn border, and text colour requires text the element owns.
  4. The overlay is mounted at the app root (to cover onboarding and loading screens), which put it outside App's ThemeProvideruseTheme() threw and took the whole app down. That's the self-bricking hazard in miniature; a test now renders the panel with no provider.

Checks

turbo run typecheck 97/97 · vitest run 11,267 passed / 3 skipped · lint clean · guard green · changeset for @xnetjs/devkit + @xnetjs/ui (both minor) · changelog fragment added.

Also points 0190 at this doc — its dev-loop body was built and had no front end.

🤖 Generated with Claude Code

xNet Test and others added 5 commits July 26, 2026 13:09
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: xNet Test <test@xnet.dev>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: xNet Test <test@xnet.dev>
…olver

W1 of exploration 0399. Hold Option: the element under the pointer is
outlined and labelled with the layer that owns it and one sentence saying
what a change there would move.

- packages/devkit/src/blast-radius.ts — resolveLane() routes a pointed
  element to the lowest lane that can satisfy a change (token/slot/plugin/
  source), keeping LANE (mechanism) separate from SCOPE (how much moves).
  Kernel packages are refused as returned data, not a throw.
- apps/web/src/dev/source-stamp.ts + jsx-dev-runtime-stamp.ts — the stamp
  itself. A Babel visitor cannot work here: esbuild lowers JSX before
  plugin-react's Babel pass runs, so JSXOpeningElement never fires. Instead
  the dev JSX runtime is shimmed and esbuild's own source argument is folded
  into props, which also survives React 19 dropping _debugSource.
- scripts/guard-no-source-stamp.mjs — three tiers: the config must gate on
  command === 'serve', nothing may import the shim by name, and no built
  asset may carry the attribute. Wired into CI.
- W1 ships with no edit affordance by design; a test asserts it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: xNet Test <test@xnet.dev>
W2 of exploration 0399. Option-click an element and the panel leads with the
blast-radius sentence, then offers the smallest control that can make the
change: the token's value, or the slot's own registered commands.

- packages/ui/src/theme/tokens.ts holds the token-override contract as plain
  functions, keyed by custom-property name (never by element, which would be
  a second source of truth). ThemeProvider delegates to it, so there is one
  implementation with two entry points.
- That second entry point is load-bearing: the overlay is mounted at the app
  root to cover onboarding and loading screens, which puts it outside App's
  ThemeProvider. Calling useTheme() there threw and took the whole app down.
  A test now renders the panel with no provider to lock that in.
- inspect/lane1.ts runs the ALREADY REGISTERED slot commands, the same ones
  the palette and drag handles use. Every applier returns its own inverse,
  and the inverse reports whether it ran rather than clearing the Undo button
  and implying success.
- Fixes two bugs found while testing: a bare function passed to setUndo was
  treated by React as an updater and invoked the reversal immediately; and
  token attribution compared HSL component triples against computed rgb()
  strings, which can never match and silently disabled Lane 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: xNet Test <test@xnet.dev>
…ew, draft PR

W3 of exploration 0399, node side. Everything that must be true or shown before
an agent edits xNet's own source from a gesture.

- probeDevEnvironment() follows 0393's ladder: checkout, pnpm, gh. gh is probed
  but excluded from 'ready' — a user can run tasks and keep local checkpoints
  without ever opening a PR.
- assertEditable() re-derives the lane and the kernel rule from the source ref
  rather than trusting the incoming verdict. A check that only runs on the side
  that can be bypassed is not a check; a test forges an allowed kernel
  resolution and it is still refused.
- previewWorktree() refuses to bind the editing session's port and resolves only
  on a confirmed readiness line for the port it asked for. Sharing the port is
  how a broken edit takes down the surface you are editing from.
- reviewWorktree() returns the diff, the file list and the gate result, and is
  prReady only when the gate passed AND something changed — a green gate over an
  empty diff is a task that did nothing.
- openPullRequest() now opens a draft by default and never merges.

Also fixes a false positive found by live verification: Tailwind's preflight
sets border-color on every element, so attributing it made nearly the whole tree
claim to be a Lane 1 token change and hid Lanes 2 and 3. Paint attribution now
requires a drawn border and, for text colour, text the element owns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-631 July 26, 2026 21:30 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jul 26, 2026
@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

No visual differences detected in the changed UI.

CI run

github-actions Bot added a commit that referenced this pull request Jul 26, 2026
@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #631.

github-actions Bot added a commit that referenced this pull request Jul 26, 2026
Base automatically changed from claude/agent-native-exploration-8b5c44 to main July 26, 2026 22:03
@crs48
crs48 temporarily deployed to pr-631 July 26, 2026 22:03 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jul 26, 2026
github-actions Bot added a commit that referenced this pull request Jul 26, 2026
github-actions Bot added a commit that referenced this pull request Jul 26, 2026
…ency

Adding @xnetjs/devkit to apps/web meant a non-frozen pnpm install, which
churned the lockfile far beyond the new entry — it flipped jiti@1.21.7 to
2.6.1 across most resolutions and dropped 131 lines. That re-resolution broke
better-sqlite3's native build in CI and shifted a generated dts chunk hash in
packages/data, failing all three test shards and typecheck on a change that
touches neither.

The leaf is browser-safe and needs no package boundary: alias it to source in
vite.config.ts and mirror the mapping in tsconfig paths, the same shape
@xnetjs/react already uses. Lockfile and apps/web/package.json return to
main's state exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-631 July 26, 2026 22:16 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jul 26, 2026
github-actions Bot added a commit that referenced this pull request Jul 26, 2026
github-actions Bot added a commit that referenced this pull request Jul 26, 2026
@crs48
crs48 merged commit f8e4038 into main Jul 26, 2026
22 checks passed
@crs48
crs48 deleted the claude/0399-point-and-change-xnet-editing-itself branch July 26, 2026 22:25
github-actions Bot added a commit that referenced this pull request Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant