Drag a div, get a diff
VEX turns browser gestures into source-code changes. Three hard problems hide in that sentence — here is what each one taught me.
Every front-end developer already owns a world-class visual design tool. It ships
with the browser, it renders your actual product with your actual data, and you
can nudge a padding value by one pixel and see the result before your finger
leaves the arrow key.
It is called DevTools, and it has the memory of a goldfish.
You spend eleven minutes in the Styles panel arriving at the perfect card —
darker surface, rounder corners, a shadow with just enough self-esteem — and then
you have to go find the actual line of code that produced it, in a component
whose class names look like css-1x9k2df, and type the whole thing again from
memory. And somewhere in there you press Cmd+R, the industry’s most popular
undo-everything shortcut, and eleven minutes evaporate.
VEX is my attempt to close that loop. You edit the live page in Chrome — select, resize, restyle, duplicate, generate a whole section from a sentence — and the edits are shipped to an AI coding agent that opens your repository and writes the change as code your project would plausibly have written itself.
That is one sentence and three genuinely hard problems. Here they are, in the order they hurt.
Problem 1: which line of code is this pixel?
The browser hands you a DOM node. Your repository contains a .tsx file. Nothing
connects them. The DOM is the output of your code the way smoke is the output of
a fire, and “please edit the fire that made this smoke” is a tricky instruction.
So VEX starts by building the most durable identity it can for the element you clicked. The naive version — grab all the classes — dies immediately in modern front-end land, because half of those classes were invented at build time by a hashing function in a good mood:
const GENERATED_CLASS_PATTERNS = [
/^css-[a-z0-9]{4,}$/, // emotion: css-1a2b3c
/^sc-[a-z]{5,}$/, // styled-components: sc-abcdef
/^_[a-zA-Z0-9]{8,}$/, // CSS modules: _3fG8kLm2a
/^[a-z]+-[a-z0-9]{5,}$/, // generic hash: prefix-a1b2c3d4e
/^[a-zA-Z]{1,3}[0-9]{4,}$/, // short prefix+digits: ab12345
];
css-1x9k2df is not a name. It is a bundler’s mood, and it will have a different
mood tomorrow. Fingerprinting an element with it is how you build a tool that
works beautifully in the demo and breaks the morning after a dependency bump.
What is left is a ladder — try the most human-meaningful identifier first, fall down a rung whenever the result is not unique on the page:
And then there is the cheat, which I enjoy far too much. If the page is a React
dev build, the DOM node carries a __reactFiber$… key. Walk that fiber upward and
you get the component name — and, in dev builds, _debugSource, which is the file
and line number that rendered this element. When it works, VEX stops guessing and
tells the agent src/components/PricingCard.tsx:42. When it doesn’t, the
ladder is still there.
Lesson: build the graceful path first and treat the precise one as a bonus. A tool that only works with
_debugSourceis a React-dev-mode tool wearing a framework-agnostic hat.
Problem 2: record the intent, not the CSS
This is the one that changed the whole design, and it is the part I would argue about in a bar.
The obvious implementation of “apply my visual edit to the code” is: read the element’s final computed style, hand it to the AI, say make it look like this. It works. It also produces the worst code in your repository — a wall of inline styles, in pixels, ignoring the design tokens your team spent a quarter agreeing on. Technically correct. Spiritually a war crime.
So VEX never sends the final state. It records twelve kinds of intent and sends the delta:
| Group | Actions | What is recorded |
|---|---|---|
| Structure | insert, delete, duplicate, move, wrap |
position, reference element, the removed HTML |
| Content | editText, replaceImage |
before → after, upload/URL/generated |
| Appearance | styleChange, resize, copyStyle |
per-property before → after, plus ratios |
| Creation | generateSection, select |
your natural-language prompt + where it lands |
A styleChange is not “background is now rgb(24,24,27)”. It is “background went
from white to near-black, radius went from 8px to 16px, and here is the
component this happened inside”. That is a fundamentally more useful sentence,
because an agent holding a delta plus your project’s context can express it in
your dialect — a Tailwind class if you use Tailwind, a token if you have tokens, a
styled-component prop if that is the house style. An agent holding a final value
can only paste the final value.
Which is why the framework detector runs when you add a project — config files
first (next.config.*, nuxt.config.*, angular.json), then package.json
dependencies, then the lockfile for the package manager, then a sniff for
tailwind.config.* / *.module.css / *.scss. All of it exists to make the
first three lines of every prompt true:
Project: /Users/lukas/code/acme-store
Framework: next
Styling: tailwind
URL: http://localhost:3000/pricing
## Task
Apply the following visual edit:
**Action**: [styleChange] `#pricing > div:nth-of-type(2)`
**Style changes**:
- `background-color`: `rgb(255, 255, 255)` → `rgb(24, 24, 27)`
- `border-radius`: `8px` → `16px`
- `box-shadow`: `none` → `0 8px 24px rgba(0,0,0,0.18)`
## Element Context
**React component**: `PricingCard`
**Source file**: `src/components/PricingCard.tsx:42`
**Accessibility path**: main > section > h2 "Pro"
There is a second prompt shape, because two very different things were wearing the same coat. Dragging a corner is a delta-driven edit: the change is the task, apply it precisely. Typing “add a testimonials section with three quotes” is prompt-driven: your sentence is the task, and the selector is merely the address where the result should be delivered. Same pipeline, inverted emphasis — and separating the two removed an entire class of “the AI did something creative when I asked it to move a button 4px” bugs.
Lesson: the interesting part of an AI feature is rarely the model. It is deciding what you refuse to send it.
Problem 3: ten edits, ten agents, one repository
A batch holds every edit you made before hitting Send. The orchestrator spawns
one agent per action, in parallel — asyncio.gather over the batch, each with
its own agent id, its own trace, its own token bill.
Parallel fan-out is wonderful when the four edits live in four components, and it
is a group project when they all live in Header.tsx. Independent agents editing
one file will happily clobber each other, and no amount of prompting makes two
processes agree about a line number. Right now VEX buys safety with visibility
rather than locking: every step, tool call, token count and dollar amount is
persisted per agent, the batch has a hard stop that interrupts every session it
spawned, and the result is a git diff you review like any other. Batch-level
conflict scheduling — group actions by target file, run those in sequence — is the
obvious next move and is not yet built. I would rather say that out loud than
pretend the problem does not exist.
The nice part of doing the fan-out this way came free. Since every agent already publishes its status to NATS, and the extension is already listening in the page, each agent can render as a cursor — parked on the element it is editing, coloured from a ten-slot palette, turning green when it succeeds and red when it does not.
It is Figma multiplayer, except your collaborators are stochastic and bill by the token.
The takeaways, minus the product
Strip VEX away and four things survive that I would apply to any tool sitting between a UI and a codebase:
- Never fingerprint what your bundler regenerates. Hashed class names, dynamic ids, framework-injected attributes — treat them as noise, and design a fallback ladder rather than a single clever heuristic.
- Send deltas, not final states. The delta preserves intent; the final state destroys it and forces a machine to guess what you meant.
- Separate “do exactly this” from “figure something out”. They look like the same feature and they need opposite prompts. Fusing them is how you get an agent that redesigns your footer because you nudged a margin.
- Instrument before you optimise. Per-agent traces, per-batch cost, a stop button that actually stops things. Autonomy without a rewind button is not a feature, it is an incident.
VEX is early — desktop app plus Chrome extension, macOS build, claude CLI as the
runtime dependency, everything on
GitHub. The pitch has not changed since
the first commit: your browser has always been the best place to decide what
should change. It just needed a way to remember.