Skip to content

Commit 4936d98

Browse files
feat(designer-mode): add dev-only in-app visual inspector
Add Designer Mode, a dev-only visual inspector for the extension UI gated behind the DESIGNER_MODE build flag (dead-code-eliminated when off, so it never ships in normal builds). It lets designers inspect any component, edit styles live, and send plain-language change requests to an AI coding agent that applies them to source. - Vendored core under ui/helpers/designer-mode/core (Shadow-DOM panel, hover/select overlay, toggle, and localhost relay client) - React fiber inspector adapter (react-adapter.ts) - Gated init in ui/index.js and the DESIGNER_MODE flag wired through builds.yml and .metamaskrc.dist - docs/designer-mode.md covering setup for both developers and designers Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 86afca4 commit 4936d98

16 files changed

Lines changed: 2551 additions & 0 deletions

File tree

.metamaskrc.dist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ BLOCKAID_PUBLIC_KEY=
6060
; Enables the Settings Page - Developer Options
6161
; ENABLE_SETTINGS_PAGE_DEV_OPTIONS=true
6262

63+
; Enables Designer Mode — a dev-only in-app visual inspector (Ctrl+Shift+D).
64+
; Pair with the `designer-mode` skill's relay server. See docs/designer-mode.md.
65+
; DESIGNER_MODE=true
66+
6367
; SENTRY CONFIGURATION
6468
; SENTRY_DSN - For production builds only (do NOT use in development)
6569
; SENTRY_DSN_DEV - For local dev and one-off QA testing (sends to 'test-metamask' project)

builds.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,8 @@ env:
292292
- DECODING_API_URL: 'https://signature-insights.api.cx.metamask.io/v1'
293293
# Determines if feature flagged Settings Page - Developer Options should be used
294294
- ENABLE_SETTINGS_PAGE_DEV_OPTIONS: false
295+
# Enables Designer Mode — a dev-only in-app visual inspector. See docs/designer-mode.md.
296+
- DESIGNER_MODE: false
295297
# Used for debugging changes to the phishing warning page.
296298
# Modified in <root>/development/build/scripts.js:@getPhishingWarningPageUrl
297299
- PHISHING_WARNING_PAGE_URL: null

docs/designer-mode.md

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# Designer Mode
2+
3+
Designer Mode is a **dev-only**, in-app visual inspector for the MetaMask
4+
extension UI. It lets a designer point at any component in the running
5+
extension, see its component name / source file / computed styles, tweak values
6+
live, and send a plain-language change request straight to an AI coding agent
7+
that applies the change to the source code.
8+
9+
It is gated behind the `DESIGNER_MODE` build flag and is **never included in
10+
normal or production builds** — the inspector code is only imported when the
11+
flag is on, so it is dead-code-eliminated otherwise.
12+
13+
---
14+
15+
## Getting started
16+
17+
Designer Mode runs inside a developer's local build and pairs with an AI coding
18+
agent. How you get set up depends on your role.
19+
20+
### For developers
21+
22+
The inspector only appears when the UI was built with `DESIGNER_MODE=true`.
23+
24+
1. Add the flag to your **`.metamaskrc`** (not just `.metamaskrc.dist`):
25+
26+
```
27+
DESIGNER_MODE=true
28+
```
29+
30+
2. **Fully restart** the dev build (the flag is read at startup, so a watch
31+
rebuild from a file save will not pick it up):
32+
33+
```bash
34+
yarn start
35+
```
36+
37+
3. Load / reload the unpacked extension in Chrome (`dist/chrome`) and open the
38+
expanded view (`chrome-extension://<id>/home.html`) — the full-page view is
39+
the best surface to inspect.
40+
41+
If it is working, the DevTools console logs:
42+
43+
- `[designer-mode] flag on — loading inspector…`
44+
- `[designer-mode] inspector active — press Ctrl+Shift+D`
45+
46+
…and the floating 🎨 button appears in the bottom-right corner.
47+
48+
#### Driving the agent loop (optional but recommended)
49+
50+
For the full "edit → apply to code" loop, you need the `designer-mode` agent
51+
skill. Agent skills are **not committed to this repo** (per ADR #57) — they are
52+
synced on demand into the gitignored `.claude/`, `.cursor/`, and `.agents/`
53+
folders. If you don't already have `.claude/skills/designer-mode/`, install it:
54+
55+
```bash
56+
yarn install # refreshes the shared MetaMask/skills cache
57+
yarn skills # syncs the skills into .claude/ , .cursor/ , .agents/
58+
```
59+
60+
See the [AI Agent Skills](../README.md#ai-agent-skills-yarn-skills) section of
61+
the README for configuration (domain selection, private overlay, etc.). If the
62+
skill isn't found after `yarn skills`, your skills source may need the private
63+
Consensys overlay configured in `.skills.local`.
64+
65+
Once installed, run the `designer-mode` skill in your AI coding assistant and say
66+
**"enter design mode"**. The skill starts the relay server and listens for
67+
requests. Without it, the panel still opens and inspects components — it will
68+
just show **"Not connected"** and the Send button stays disabled.
69+
70+
### For designers
71+
72+
You don't install or build anything yourself — Designer Mode runs inside a
73+
developer's local build. To get set up, pair with a developer on your team and
74+
ask them to:
75+
76+
1. Start the extension with the **`DESIGNER_MODE=true`** flag (see "For
77+
developers" above).
78+
2. Install and run the **`designer-mode` agent skill** in their AI coding
79+
assistant (so your change requests actually get applied to the code).
80+
3. Load the unpacked extension in Chrome and open the **expanded view** — the
81+
full-page window is the best surface to inspect.
82+
83+
Then keep that developer in the loop while you work: each change you send goes to
84+
the agent on their machine, and they may need to approve steps along the way.
85+
86+
> If you don't see the 🎨 button, ask the developer to confirm the build was
87+
> started with `DESIGNER_MODE=true` (and fully restarted after adding the flag).
88+
> If the panel says **"Not connected"**, ask them to start the `designer-mode`
89+
> skill — you can still inspect, but sending changes is disabled until it's running.
90+
91+
---
92+
93+
## How it works
94+
95+
```
96+
┌─────────────────────────┐ ┌──────────────────┐ ┌─────────────────┐
97+
│ MetaMask extension UI │ HTTP │ Relay server │ │ AI agent │
98+
│ (DESIGNER_MODE=true) │ ─────► │ localhost:3334 │ ─────► │ (designer-mode │
99+
│ inspector panel 🎨 │ ◄───── │ │ ◄───── │ skill) │
100+
└─────────────────────────┘ └──────────────────┘ └─────────────────┘
101+
designer dev machine developer
102+
```
103+
104+
1. The **inspector panel** runs inside the extension UI (enabled with the build flag).
105+
2. It talks to a small **relay server** on `http://localhost:3334`.
106+
3. An **AI agent** (running the `designer-mode` skill) listens to the relay,
107+
applies the requested change to source, and replies back into the panel.
108+
109+
> Because the extension UI and the relay run on the same machine, the default
110+
> `http://localhost:3334` works with no configuration. `http://localhost` is a
111+
> trustworthy origin, so the `chrome-extension://` page can reach it without any
112+
> manifest or CSP changes.
113+
114+
---
115+
116+
## Using the inspector
117+
118+
### 1. Open the inspector
119+
120+
- Click the floating **🎨 Designer Mode** button (bottom-right), or
121+
- Press **Ctrl+Shift+D** (works on Windows/Linux and macOS).
122+
123+
The button can be dragged anywhere if it is in your way.
124+
125+
### 2. Inspect a component
126+
127+
- **Hover** over any element to highlight it and see its component name.
128+
- **Click** to lock the selection — the panel then shows full details:
129+
- **Component** name, `data-testid`, and source file (`file.tsx:line`)
130+
- **Layout** (display, position, size, flex)
131+
- **Spacing** (margin/padding cross editors)
132+
- **Typography** (font, size, weight, line height, color)
133+
- **Fill & Stroke** (background, border, radius, shadow)
134+
- **Design Tokens** and **CSS classes** applied to the element
135+
- Press **Esc** (or **Unlock**) to release the selection.
136+
137+
### 3. Edit values live
138+
139+
- Click any value to edit it inline; changes apply to the live UI immediately so
140+
you can preview them.
141+
- For numeric values, use **↑ / ↓** to nudge by 1 (hold **Shift** for 10).
142+
- Use the color swatches to pick colors, and add/remove classes or tokens via
143+
the pill lists.
144+
- Every edit you make is collected into a **pending edits** list shown above the
145+
message box.
146+
147+
> Live edits are a **preview only**. They are not written to the codebase until
148+
> you send them to the agent (next step). Refreshing the extension resets them.
149+
150+
### 4. Send a change request
151+
152+
At the bottom of the panel:
153+
154+
- The **status dot** shows whether the agent is connected (green = connected).
155+
- Type a plain-language description of what you want (for example, *"make this
156+
button bigger and use the primary brand color"*), and/or rely on your pending
157+
inline edits.
158+
- Press **Enter** or click the **** send button.
159+
160+
The agent receives the component info, your inline edits, and your message,
161+
applies the change to the source code, and replies in the panel's message
162+
thread. You can keep iterating — each send is a new request.
163+
164+
> **Copy for AI**: if you would rather paste the details somewhere yourself, the
165+
> **Copy for AI** button in the header copies a formatted summary of the selected
166+
> component and your edits to the clipboard.
167+
168+
---
169+
170+
## Troubleshooting
171+
172+
| Symptom | Likely cause / fix |
173+
| --- | --- |
174+
| No 🎨 button | Build was not started with `DESIGNER_MODE=true`, or the build wasn't fully restarted after adding the flag. Check the console for the `[designer-mode]` logs. |
175+
| `[designer-mode] failed to init` in console | The inspector threw on init — share the error with a developer. |
176+
| Panel shows **"Not connected"** | The relay server isn't running. A developer needs to run the `designer-mode` agent skill ("enter design mode"). Inspection still works; sending is disabled. |
177+
| Send button disabled | Same as above — the agent/relay is not connected. |
178+
| Sent a request but no reply | The agent may be busy or waiting for approval. Check with the developer running the skill. |
179+
180+
---
181+
182+
## Notes
183+
184+
- Designer Mode is for **development only**. The `DESIGNER_MODE` flag defaults to
185+
`false` in `builds.yml`, and the inspector is excluded from any build where the
186+
flag is off.
187+
- Implementation lives in `ui/helpers/designer-mode/`; the gated entry point is in
188+
`ui/index.js`.
189+
- The agent side is documented in the `designer-mode` skill
190+
(`.claude/skills/designer-mode/`), which bundles the relay server.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import type { InspectorAdapter, DesignerModeOptions, ComponentInfo } from './types';
2+
import { OverlayController } from './overlay';
3+
import { PanelController } from './panel';
4+
import { ToggleController } from './toggle';
5+
import { RelayClient } from './relay';
6+
7+
export class DesignerModeCore {
8+
private adapter: InspectorAdapter;
9+
10+
private options: DesignerModeOptions;
11+
12+
private relay: RelayClient;
13+
14+
private overlay: OverlayController;
15+
16+
private panel: PanelController;
17+
18+
private toggleCtrl: ToggleController | null = null;
19+
20+
private host: HTMLElement | null = null;
21+
22+
private isActive = false;
23+
24+
private selectedEl: HTMLElement | null = null;
25+
26+
private boundKeyDown: (e: KeyboardEvent) => void;
27+
28+
constructor(options: DesignerModeOptions & { adapter: InspectorAdapter }) {
29+
this.adapter = options.adapter;
30+
this.options = options;
31+
this.relay = new RelayClient(options.relayUrl);
32+
this.overlay = new OverlayController(this.adapter);
33+
this.panel = new PanelController(this.relay, {
34+
onClose: () => this.setActive(false),
35+
onUnlock: () => {
36+
this.overlay.unlock();
37+
this.selectedEl = null;
38+
this.panel.showCompact();
39+
},
40+
});
41+
this.boundKeyDown = this.handleKeyDown.bind(this);
42+
}
43+
44+
mount() {
45+
if (this.host) {return;}
46+
this.host = document.createElement('div');
47+
this.host.setAttribute('data-designer-mode', 'root');
48+
document.body.appendChild(this.host);
49+
50+
this.overlay.mount(this.host);
51+
this.panel.mount(this.host);
52+
53+
if (this.options.defaultActive !== false) {
54+
// show toggle button
55+
this.toggleCtrl = new ToggleController();
56+
this.toggleCtrl.setOnToggle(() => this.setActive(!this.isActive));
57+
this.toggleCtrl.mount(this.host);
58+
}
59+
60+
this.overlay.setOnSelect((info, el) => {
61+
if (!info) { this.panel.hide(); this.selectedEl = null; return; }
62+
this.selectedEl = el;
63+
this.panel.show(info, el);
64+
});
65+
66+
this.overlay.setOnHover((info, el) => {
67+
if (!info || !el) { this.panel.hideHover(); return; }
68+
this.panel.showHover(info, el);
69+
});
70+
71+
document.addEventListener('keydown', this.boundKeyDown, true);
72+
73+
if (this.options.persistState) {
74+
const saved = localStorage.getItem('designer-mode-active');
75+
if (saved === 'true') {this.setActive(true);}
76+
}
77+
}
78+
79+
unmount() {
80+
this.overlay.deactivate();
81+
this.panel.unmount();
82+
this.toggleCtrl?.unmount();
83+
this.host?.remove();
84+
this.host = null;
85+
document.removeEventListener('keydown', this.boundKeyDown, true);
86+
}
87+
88+
toggle() { this.setActive(!this.isActive); }
89+
90+
setActive(active: boolean) {
91+
this.isActive = active;
92+
if (active) {
93+
this.overlay.activate();
94+
if (this.toggleCtrl) {
95+
const tp = this.toggleCtrl.getPosition();
96+
this.panel.setPosition(tp.right, tp.bottom);
97+
}
98+
this.panel.showCompact();
99+
} else {
100+
this.overlay.deactivate();
101+
this.panel.hide();
102+
}
103+
this.toggleCtrl?.setActive(active);
104+
if (this.options.persistState) {
105+
localStorage.setItem('designer-mode-active', String(active));
106+
}
107+
}
108+
109+
isMounted() { return Boolean(this.host?.isConnected); }
110+
111+
private handleKeyDown(e: KeyboardEvent) {
112+
const modifier = e.ctrlKey || e.metaKey;
113+
if (modifier && e.shiftKey && e.key === 'D') {
114+
e.preventDefault();
115+
this.toggle();
116+
}
117+
}
118+
119+
static async autoInit(options: DesignerModeOptions = {}) {
120+
const { detectFramework } = await import('./utils');
121+
const framework = await detectFramework();
122+
const { createAdapter } = await import('./detect');
123+
const adapter = createAdapter(framework);
124+
const core = new DesignerModeCore({ ...options, adapter });
125+
core.mount();
126+
return core;
127+
}
128+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Framework, InspectorAdapter } from './types';
2+
import { buildFallbackInfo } from './utils';
3+
4+
export function createAdapter(framework: Framework): InspectorAdapter {
5+
// Lazy import to avoid bundling all adapters
6+
// In practice, the user imports the specific adapter they need
7+
// This is used for auto-detection in the extension / auto-init
8+
return {
9+
getComponentInfo(el: HTMLElement) {
10+
return buildFallbackInfo(el);
11+
},
12+
onActivate() {
13+
// No activation work needed for the fallback adapter.
14+
},
15+
onDeactivate() {
16+
// No teardown needed for the fallback adapter.
17+
},
18+
};
19+
}
20+
21+
export { detectFramework } from './utils';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export { DesignerModeCore } from './core';
2+
export { RelayClient } from './relay';
3+
export { OverlayController } from './overlay';
4+
export { PanelController } from './panel';
5+
export { ToggleController } from './toggle';
6+
export { formatAgentPrompt, formatForClipboard } from './prompt';
7+
export { extractComputedStyles, buildDomPath, getDirectTextContent, serializeProps, detectFramework, buildComponentInfo, buildFallbackInfo, extractComponentNameFromPath } from './utils';
8+
export type { ComponentInfoFields } from './utils';
9+
export { createAdapter } from './detect';
10+
export type { ComponentInfo, ComputedStyleSnapshot, ChangesetEntry, DesignerModeOptions, InspectorAdapter, Framework, TokenPattern, RelayStatus } from './types';

0 commit comments

Comments
 (0)