@bitovi/vybit 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,8 +16,8 @@
16
16
  font-size: 12px;
17
17
  }
18
18
  </style>
19
- <script type="module" crossorigin src="/panel/assets/index-BLMep7is.js"></script>
20
- <link rel="stylesheet" crossorigin href="/panel/assets/index-DswT-2F_.css">
19
+ <script type="module" crossorigin src="/panel/assets/index-CzGpZV08.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/panel/assets/index-BdUOBofL.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
package/server/app.ts CHANGED
@@ -4,14 +4,16 @@ import express from "express";
4
4
  import cors from "cors";
5
5
  import { request as makeRequest } from "http";
6
6
  import path from "path";
7
+ import { createProxyMiddleware } from "http-proxy-middleware";
7
8
 
8
9
  import { getByStatus, getQueueUpdate, clearAll } from "./queue.js";
9
10
  import { resolveTailwindConfig, generateCssForClasses, getTailwindVersion } from "./tailwind.js";
11
+ import { loadStoryArgTypes } from "./storybook.js";
10
12
  import type { PatchStatus } from "../shared/types.js";
11
13
 
12
14
  const VALID_STATUSES = new Set<string>(['staged', 'committed', 'implementing', 'implemented', 'error']);
13
15
 
14
- export function createApp(packageRoot: string): express.Express {
16
+ export function createApp(packageRoot: string, storybookUrl: string | null = null): express.Express {
15
17
  const app = express();
16
18
  app.use(cors());
17
19
 
@@ -82,6 +84,67 @@ export function createApp(packageRoot: string): express.Express {
82
84
  res.json({ ok: true });
83
85
  });
84
86
 
87
+ // --- Storybook proxy ---
88
+ if (storybookUrl) {
89
+ // Vite's dev server serves assets at many root-absolute path prefixes:
90
+ // /@vite/client, /@react-refresh, /@id/..., /node_modules/.cache/..., /src/...
91
+ // Rather than enumerate them all, proxy everything that isn't ours.
92
+ const OWN_PATHS = new Set(['/panel', '/overlay.js', '/api', '/patches', '/css', '/tailwind-config']);
93
+ const isOwnPath = (p: string) =>
94
+ OWN_PATHS.has(p) ||
95
+ [...OWN_PATHS].some(own => p.startsWith(own + '/')) ||
96
+ p === '/';
97
+
98
+ app.use('/storybook', createProxyMiddleware({
99
+ target: storybookUrl,
100
+ changeOrigin: true,
101
+ pathRewrite: { '^/storybook': '' },
102
+ }));
103
+
104
+ app.use(createProxyMiddleware({
105
+ target: storybookUrl,
106
+ changeOrigin: true,
107
+ pathFilter: (pathname) => !isOwnPath(pathname),
108
+ }));
109
+
110
+ console.error(`[storybook] Proxying /storybook + Vite asset paths → ${storybookUrl}`);
111
+ }
112
+
113
+ app.get('/api/storybook-status', (_req, res) => {
114
+ res.json({ url: storybookUrl ? '/storybook' : null, directUrl: storybookUrl ?? null });
115
+ });
116
+
117
+ app.get('/api/storybook-argtypes', async (_req, res) => {
118
+ if (!storybookUrl) { res.json({}); return; }
119
+ try {
120
+ const argTypes = await loadStoryArgTypes(storybookUrl);
121
+ res.json(argTypes);
122
+ } catch (err) {
123
+ console.error('[storybook] /api/storybook-argtypes failed:', err);
124
+ res.json({});
125
+ }
126
+ });
127
+
128
+ // Single endpoint: everything the Draw tab panel needs in one request.
129
+ app.get('/api/storybook-data', async (_req, res) => {
130
+ if (!storybookUrl) {
131
+ res.json({ available: false });
132
+ return;
133
+ }
134
+ try {
135
+ const [indexRes, argTypes] = await Promise.all([
136
+ fetch(`${storybookUrl}/index.json`, { signal: AbortSignal.timeout(5000) }),
137
+ loadStoryArgTypes(storybookUrl),
138
+ ]);
139
+ if (!indexRes.ok) { res.json({ available: false }); return; }
140
+ const index = await indexRes.json();
141
+ res.json({ available: true, directUrl: storybookUrl, index, argTypes });
142
+ } catch (err) {
143
+ console.error('[storybook] /api/storybook-data failed:', err);
144
+ res.json({ available: false });
145
+ }
146
+ });
147
+
85
148
  // --- Serve Panel app ---
86
149
  if (process.env.PANEL_DEV) {
87
150
  const panelDevPort = Number(process.env.PANEL_DEV_PORT) || 5174;
package/server/index.ts CHANGED
@@ -16,6 +16,7 @@ import { createApp } from "./app.js";
16
16
  import { setupWebSocket } from "./websocket.js";
17
17
  import { registerMcpTools } from "./mcp-tools.js";
18
18
  import { checkTailwindAvailable } from "./tailwind.js";
19
+ import { detectStorybookUrl } from "./storybook.js";
19
20
 
20
21
  // --- Resolve project root (precedence: --cwd flag, VYBIT_PROJECT_ROOT, cwd) ---
21
22
  const argv = process.argv.slice(2);
@@ -57,8 +58,11 @@ const packageRoot = __dirname.includes(`${path.sep}dist${path.sep}`)
57
58
 
58
59
  const port = Number(process.env.PORT) || 3333;
59
60
 
61
+ // --- Storybook detection ---
62
+ const storybookUrl = await detectStorybookUrl();
63
+
60
64
  // --- HTTP + WebSocket ---
61
- const app = createApp(packageRoot);
65
+ const app = createApp(packageRoot, storybookUrl);
62
66
  const httpServer = createServer(app);
63
67
  const { broadcastPatchUpdate } = setupWebSocket(httpServer);
64
68
 
@@ -28,16 +28,55 @@ const KEEPALIVE_INTERVAL_MS = 60_000;
28
28
  // Prompt builders for implement_next_change
29
29
  // ---------------------------------------------------------------------------
30
30
 
31
+ // ---------------------------------------------------------------------------
32
+ // JSX builder: converts componentArgs to a JSX string like <Button variant="primary">Click me</Button>
33
+ // ---------------------------------------------------------------------------
34
+
35
+ function buildJsx(componentName: string, args?: Record<string, unknown>): string {
36
+ if (!args || Object.keys(args).length === 0) return `<${componentName} />`;
37
+
38
+ const { children, ...rest } = args;
39
+ const props = Object.entries(rest)
40
+ .map(([key, value]) => {
41
+ if (typeof value === 'string') return `${key}="${value}"`;
42
+ if (typeof value === 'boolean') return value ? key : `${key}={false}`;
43
+ return `${key}={${JSON.stringify(value)}}`;
44
+ })
45
+ .join(' ');
46
+
47
+ const propsStr = props ? ` ${props}` : '';
48
+
49
+ if (children != null && children !== '') {
50
+ const childStr = typeof children === 'string' ? children : `{${JSON.stringify(children)}}`;
51
+ return `<${componentName}${propsStr}>${childStr}</${componentName}>`;
52
+ }
53
+ return `<${componentName}${propsStr} />`;
54
+ }
55
+
31
56
  function buildCommitInstructions(commit: Commit, remainingCount: number): string {
32
57
  const classChanges = commit.patches.filter(p => p.kind === 'class-change');
33
58
  const messages = commit.patches.filter(p => p.kind === 'message');
34
59
  const designs = commit.patches.filter(p => p.kind === 'design');
60
+ const componentDrops = commit.patches.filter(p => p.kind === 'component-drop');
35
61
  const moreText = remainingCount > 0
36
62
  ? `${remainingCount} more commit${remainingCount === 1 ? '' : 's'} waiting in the queue after this one.`
37
63
  : 'This is the last commit in the queue. After implementing it, call `implement_next_change` again to wait for future changes.';
38
64
 
39
- let patchList = '';
65
+ // Build a map from patch ID → step number for ghost-chain references
66
+ const patchStepMap = new Map<string, number>();
40
67
  let stepNum = 1;
68
+ for (const patch of commit.patches) {
69
+ patchStepMap.set(patch.id, stepNum);
70
+ stepNum++;
71
+ }
72
+
73
+ let patchList = '';
74
+ const hasMultipleDrops = componentDrops.length > 1;
75
+ if (hasMultipleDrops) {
76
+ patchList += `> ⚠️ **Apply component insertions IN ORDER** — later drops may reference components added by earlier steps.\n\n`;
77
+ }
78
+
79
+ stepNum = 1;
41
80
  for (const patch of commit.patches) {
42
81
  if (patch.kind === 'class-change') {
43
82
  const comp = patch.component?.name ?? 'unknown component';
@@ -66,6 +105,50 @@ ${patch.elementKey ? `\n_Scoped to: ${patch.elementKey}_\n` : ''}
66
105
  - **Canvas size:** ${patch.canvasWidth ?? '?'}×${patch.canvasHeight ?? '?'}px
67
106
  - The design image is included as a separate image content part below — refer to it for the visual intent.
68
107
  ${context ? `- **Context HTML:**\n\`\`\`html\n${context}\n\`\`\`\n` : ''}
108
+ ${patch.canvasComponents && patch.canvasComponents.length > 0 ? `
109
+ **Components to place (positions relative to canvas top-left):**
110
+
111
+ | # | Component | Import | Props | Position | Size |
112
+ |---|-----------|--------|-------|----------|------|
113
+ ${patch.canvasComponents.map((c: any, i: number) => {
114
+ const importPath = c.componentPath ? c.componentPath.replace(/\.tsx?$/, '') : '—';
115
+ const props = c.args ? Object.entries(c.args).map(([k, v]) => typeof v === 'string' ? `${k}="${v}"` : `${k}={${JSON.stringify(v)}}`).join(' ') : '—';
116
+ return `| ${i + 1} | \`${c.componentName}\` | \`${importPath}\` | ${props} | (${c.x}, ${c.y}) | ${c.width}×${c.height}px |`;
117
+ }).join('\n')}
118
+
119
+ ⚠️ Import and render these React components at the indicated positions. Use the design image as a visual reference for the overall layout. Do NOT paste rendered HTML.
120
+ ` : ''}
121
+ `;
122
+ } else if (patch.kind === 'component-drop') {
123
+ const comp = patch.component?.name ?? 'Component';
124
+ const importPath = patch.componentPath
125
+ ? patch.componentPath.replace(/\.tsx?$/, '')
126
+ : null;
127
+ const jsx = buildJsx(comp, patch.componentArgs);
128
+ const parentComp = patch.parentComponent?.name;
129
+ const insertMode = patch.insertMode ?? 'after';
130
+ const context = patch.context ?? '';
131
+
132
+ // Determine insertion target description
133
+ let targetDesc: string;
134
+ if (patch.targetPatchId && patch.targetComponentName) {
135
+ const refStep = patchStepMap.get(patch.targetPatchId);
136
+ targetDesc = refStep
137
+ ? `the \`<${patch.targetComponentName} />\` you added in **step ${refStep}**`
138
+ : `the \`<${patch.targetComponentName} />\` component (from an earlier drop)`;
139
+ } else {
140
+ const tag = patch.target?.tag ?? 'element';
141
+ const classes = patch.target?.classes ? ` class="${patch.target.classes}"` : '';
142
+ targetDesc = `\`<${tag}${classes}>\``;
143
+ }
144
+
145
+ patchList += `### ${stepNum}. Component drop \`${patch.id}\`
146
+ - **Insert:** \`${jsx}\` **${insertMode}** ${targetDesc}
147
+ ${importPath ? `- **Import:** \`import { ${comp} } from '${importPath}'\`` : `- **Component:** \`${comp}\` (resolve import path manually)`}
148
+ ${parentComp ? `\n- **Parent component:** \`${parentComp}\` — edit this component's source file` : ''}
149
+ ${context ? `- **Context HTML:**\n\`\`\`html\n${context}\n\`\`\`\n` : ''}
150
+ ⚠️ Do NOT paste rendered HTML. Import and render the React component with the props shown above.
151
+
69
152
  `;
70
153
  }
71
154
  stepNum++;
@@ -76,12 +159,29 @@ ${context ? `- **Context HTML:**\n\`\`\`html\n${context}\n\`\`\`\n` : ''}
76
159
  if (classChanges.length) summaryParts.push(`${classChanges.length} class change${classChanges.length === 1 ? '' : 's'}`);
77
160
  if (messages.length) summaryParts.push(`${messages.length} message${messages.length === 1 ? '' : 's'}`);
78
161
  if (designs.length) summaryParts.push(`${designs.length} design${designs.length === 1 ? '' : 's'}`);
162
+ if (componentDrops.length) summaryParts.push(`${componentDrops.length} component drop${componentDrops.length === 1 ? '' : 's'}`);
79
163
 
80
164
  const resultsPart = classChanges.map(p => ` { "patchId": "${p.id}", "success": true }`).join(',\n');
81
-
82
- // Design patches also need to be reported in results
83
165
  const designResultsPart = designs.map(p => ` { "patchId": "${p.id}", "success": true }`).join(',\n');
84
- const allResultsPart = [resultsPart, designResultsPart].filter(Boolean).join(',\n');
166
+ const dropResultsPart = componentDrops.map(p => ` { "patchId": "${p.id}", "success": true }`).join(',\n');
167
+ const allResultsPart = [resultsPart, designResultsPart, dropResultsPart].filter(Boolean).join(',\n');
168
+
169
+ // Build step instructions
170
+ const stepInstructions: string[] = [];
171
+ if (classChanges.length || componentDrops.length) {
172
+ let step1 = '1. For each change above, find the source file and apply it.';
173
+ if (componentDrops.length) {
174
+ step1 += '\n For component drops: add the import statement and render the component with the specified props at the indicated position.';
175
+ }
176
+ if (messages.length) {
177
+ step1 += '\n Use the user messages as additional context for understanding intent.';
178
+ }
179
+ stepInstructions.push(step1);
180
+ }
181
+ if (designs.length) {
182
+ stepInstructions.push(`${stepInstructions.length + 1}. For each design sketch, examine the attached image and implement the visual design
183
+ as HTML/CSS ${classChanges.length ? 'alongside the class changes' : 'in the specified component'}. Insert it ${designs[0].insertMode ?? 'after'} the target element.`);
184
+ }
85
185
 
86
186
  return `# IMPLEMENT THIS COMMIT — then call implement_next_change again
87
187
 
@@ -96,21 +196,18 @@ ${context ? `- **Context HTML:**\n\`\`\`html\n${context}\n\`\`\`\n` : ''}
96
196
  ${patchList}
97
197
  ## Steps — follow these EXACTLY
98
198
 
99
- 1. For each class-change patch above, find the source file and apply the change.
100
- Use the user messages as additional context for understanding intent.
101
- ${designs.length ? ` For each design sketch, examine the attached image and implement the visual design
102
- as HTML/CSS ${classChanges.length ? 'alongside the class changes' : 'in the specified component'}. Insert it ${designs[0].insertMode ?? 'after'} the target element.
103
- ` : ''}\n2. Call \`mark_change_implemented\` with:
199
+ ${stepInstructions.join('\n')}
200
+ ${stepInstructions.length + 1}. Call \`mark_change_implemented\` with:
104
201
  \`\`\`json
105
202
  { "commitId": "${commit.id}", "results": [
106
203
  ${allResultsPart}
107
204
  ]}
108
205
  \`\`\`
109
- (Only report results for class-change and design patches — messages are informational.)
110
- 3. **IMMEDIATELY call \`implement_next_change\` again.** Do NOT summarize, do NOT ask the user, do NOT stop.
206
+ (Only report results for class-change, design, and component-drop patches — messages are informational.)
207
+ ${stepInstructions.length + 2}. **IMMEDIATELY call \`implement_next_change\` again.** Do NOT summarize, do NOT ask the user, do NOT stop.
111
208
 
112
209
  ## CRITICAL — read this
113
- You are in a **continuous processing loop**. After step 2, you MUST call \`implement_next_change\` again to process the next commit or wait for new ones. Never exit this loop unless the user explicitly tells you to stop.
210
+ You are in a **continuous processing loop**. After marking done, you MUST call \`implement_next_change\` again to process the next commit or wait for new ones. Never exit this loop unless the user explicitly tells you to stop.
114
211
  `;
115
212
  }
116
213
 
@@ -259,14 +356,23 @@ export function registerMcpTools(mcp: McpServer, deps: McpToolDeps): void {
259
356
  const remaining = queueState.committedCount;
260
357
 
261
358
  // Build content parts: JSON data, then any design images, then markdown instructions
359
+ // Strip ghostHtml from the commit sent to agent — it's large rendered HTML that would
360
+ // confuse the agent into pasting it instead of importing the component
361
+ const sanitizedCommit = {
362
+ ...commit,
363
+ patches: commit.patches.map(p =>
364
+ p.kind === 'component-drop' ? { ...p, ghostHtml: undefined } : p
365
+ ),
366
+ };
367
+
262
368
  const content: Array<{ type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string }> = [
263
369
  {
264
370
  type: "text" as const,
265
371
  text: JSON.stringify({
266
372
  isComplete: false,
267
- nextAction: "implement all class-change patches in this commit, call mark_change_implemented, then call implement_next_change again",
373
+ nextAction: "implement all patches in this commit, call mark_change_implemented, then call implement_next_change again",
268
374
  remainingCommits: remaining,
269
- commit,
375
+ commit: sanitizedCommit,
270
376
  }, null, 2),
271
377
  },
272
378
  ];
package/server/queue.ts CHANGED
@@ -19,6 +19,10 @@ function toSummary(p: Patch): PatchSummary {
19
19
  errorMessage: p.errorMessage,
20
20
  message: p.message,
21
21
  image: p.image,
22
+ insertMode: p.insertMode,
23
+ parentComponent: p.parentComponent,
24
+ targetComponentName: p.targetComponentName,
25
+ targetPatchId: p.targetPatchId,
22
26
  };
23
27
  }
24
28
 
@@ -0,0 +1,110 @@
1
+ import path from 'path';
2
+
3
+ const SCAN_PORTS = [6006, 6007, 6008, 6009, 6010];
4
+
5
+ interface StorybookIndexEntry {
6
+ title: string;
7
+ importPath: string;
8
+ }
9
+
10
+ export interface ArgTypeInfo {
11
+ control?: string | { type: string };
12
+ options?: string[];
13
+ description?: string;
14
+ defaultValue?: unknown;
15
+ }
16
+
17
+ /**
18
+ * Dynamically import each story file referenced by the Storybook index and
19
+ * collect the argTypes declared on the default export (Meta object).
20
+ * Returns a map of componentName → argTypes.
21
+ */
22
+ export async function loadStoryArgTypes(
23
+ storybookUrl: string
24
+ ): Promise<Record<string, Record<string, ArgTypeInfo>>> {
25
+ try {
26
+ const res = await fetch(`${storybookUrl}/index.json`, {
27
+ signal: AbortSignal.timeout(5000),
28
+ });
29
+ if (!res.ok) return {};
30
+ const index = (await res.json()) as {
31
+ entries?: Record<string, StorybookIndexEntry>;
32
+ stories?: Record<string, StorybookIndexEntry>;
33
+ };
34
+ const entries = Object.values(index.entries ?? index.stories ?? {});
35
+
36
+ // One story file per unique importPath; map it to the component name
37
+ const seen = new Map<string, string>(); // importPath → componentName
38
+ for (const entry of entries) {
39
+ if (!seen.has(entry.importPath)) {
40
+ const componentName = entry.title.split('/').at(-1) ?? entry.title;
41
+ seen.set(entry.importPath, componentName);
42
+ }
43
+ }
44
+
45
+ const cwd = path.resolve(process.cwd());
46
+ const result: Record<string, Record<string, ArgTypeInfo>> = {};
47
+
48
+ for (const [importPath, componentName] of seen) {
49
+ // Security: only allow relative paths that stay within cwd
50
+ if (!importPath.startsWith('./')) continue;
51
+ const fullPath = path.resolve(cwd, importPath);
52
+ if (!fullPath.startsWith(cwd + '/')) continue;
53
+
54
+ try {
55
+ // Append a cache-buster so Node's ESM module cache doesn't hold onto
56
+ // stale argTypes after the story file is edited.
57
+ const mod = await import(`${fullPath}?t=${Date.now()}`);
58
+ const meta = mod.default as
59
+ | { argTypes?: Record<string, ArgTypeInfo> }
60
+ | undefined;
61
+ if (meta?.argTypes) {
62
+ result[componentName] = meta.argTypes;
63
+ }
64
+ } catch (err) {
65
+ console.error(`[storybook] Failed to import ${importPath}:`, err);
66
+ }
67
+ }
68
+
69
+ return result;
70
+ } catch (err) {
71
+ console.error('[storybook] loadStoryArgTypes failed:', err);
72
+ return {};
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Returns the Storybook base URL or null if not found.
78
+ * Priority:
79
+ * 1. STORYBOOK_URL env var
80
+ * 2. Port scan 6006–6010
81
+ */
82
+ export async function detectStorybookUrl(): Promise<string | null> {
83
+ if (process.env.STORYBOOK_URL) {
84
+ console.error(`[storybook] Using STORYBOOK_URL=${process.env.STORYBOOK_URL} (from env)`);
85
+ return process.env.STORYBOOK_URL;
86
+ }
87
+
88
+ for (const port of SCAN_PORTS) {
89
+ const url = `http://localhost:${port}`;
90
+ if (await probeStorybookUrl(url)) {
91
+ console.error(`[storybook] Auto-detected at ${url}`);
92
+ return url;
93
+ }
94
+ }
95
+
96
+ console.error('[storybook] Not detected — Draw tab will show "Start Storybook" prompt');
97
+ return null;
98
+ }
99
+
100
+ async function probeStorybookUrl(baseUrl: string): Promise<boolean> {
101
+ try {
102
+ const res = await fetch(`${baseUrl}/index.json`, { signal: AbortSignal.timeout(500) });
103
+ if (!res.ok) return false;
104
+ const data = await res.json() as Record<string, unknown>;
105
+ // Verify it's actually Storybook: v6 uses `stories`, v7+ uses `entries`
106
+ return typeof data.v === 'number' && (data.entries != null || data.stories != null);
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
@@ -50,6 +50,10 @@ export function setupWebSocket(httpServer: Server): WebSocketDeps {
50
50
  // Route messages with a "to" field to all clients of that role
51
51
  if (msg.to) {
52
52
  broadcastTo(msg.to, msg, ws);
53
+ // Dual-route component arm/disarm to design iframe too
54
+ if ((msg.type === "COMPONENT_ARM" || msg.type === "COMPONENT_DISARM") && msg.to === "overlay") {
55
+ broadcastTo("design", msg, ws);
56
+ }
53
57
  return;
54
58
  }
55
59
 
@@ -103,6 +107,7 @@ export function setupWebSocket(httpServer: Server): WebSocketDeps {
103
107
  insertMode: msg.insertMode,
104
108
  canvasWidth: msg.canvasWidth,
105
109
  canvasHeight: msg.canvasHeight,
110
+ canvasComponents: msg.canvasComponents,
106
111
  };
107
112
  addPatch(patch);
108
113
  broadcastPatchUpdate();
@@ -114,6 +119,10 @@ export function setupWebSocket(httpServer: Server): WebSocketDeps {
114
119
  console.error(`[ws] Design patch staged: ${patch.id}`);
115
120
  } else if (msg.type === "DESIGN_CLOSE") {
116
121
  broadcastTo("overlay", { type: "DESIGN_CLOSE" }, ws);
122
+ } else if (msg.type === "COMPONENT_DROPPED") {
123
+ const patch = addPatch({ ...msg.patch, kind: msg.patch.kind ?? 'component-drop' });
124
+ console.error(`[ws] Component-drop patch staged: #${patch.id}`);
125
+ broadcastPatchUpdate();
117
126
  }
118
127
  } catch (err) {
119
128
  console.error("[ws] Bad message:", err);
package/shared/types.ts CHANGED
@@ -3,7 +3,20 @@
3
3
 
4
4
  export type ContainerName = 'modal' | 'popover' | 'sidebar' | 'popup';
5
5
 
6
- export type PatchKind = 'class-change' | 'message' | 'design';
6
+ /** A component placed on the Fabric.js design canvas */
7
+ export interface CanvasComponent {
8
+ componentName: string;
9
+ componentPath?: string; // e.g. './src/components/Button.tsx'
10
+ storyId?: string;
11
+ args?: Record<string, unknown>;
12
+ // Position/size on the canvas (px, relative to canvas top-left)
13
+ x: number;
14
+ y: number;
15
+ width: number;
16
+ height: number;
17
+ }
18
+
19
+ export type PatchKind = 'class-change' | 'message' | 'design' | 'component-drop';
7
20
 
8
21
  export type PatchStatus = 'staged' | 'committed' | 'implementing' | 'implemented' | 'error';
9
22
 
@@ -30,6 +43,15 @@ export interface Patch {
30
43
  insertMode?: string; // before | after | first-child | last-child
31
44
  canvasWidth?: number;
32
45
  canvasHeight?: number;
46
+ canvasComponents?: CanvasComponent[]; // Components placed on the design canvas
47
+ // Component-drop fields (used when kind === 'component-drop'):
48
+ ghostHtml?: string; // HTML of the dropped component (overlay preview only — stripped from MCP response)
49
+ componentStoryId?: string; // Storybook story ID
50
+ componentPath?: string; // Source file of the component, e.g. './src/components/Button.tsx'
51
+ componentArgs?: Record<string, unknown>; // Props the user configured before dropping
52
+ parentComponent?: { name: string }; // React component that contains the drop target
53
+ targetPatchId?: string; // If target is a ghost from an earlier drop, references that patch
54
+ targetComponentName?: string; // Name of the ghost component being referenced
33
55
  // Commit reference:
34
56
  commitId?: string; // Set when committed into a Commit
35
57
  }
@@ -57,6 +79,12 @@ export interface PatchSummary {
57
79
  errorMessage?: string;
58
80
  message?: string;
59
81
  image?: string;
82
+ canvasComponents?: CanvasComponent[];
83
+ // Component-drop display fields:
84
+ insertMode?: string;
85
+ parentComponent?: { name: string };
86
+ targetComponentName?: string;
87
+ targetPatchId?: string;
60
88
  }
61
89
 
62
90
  export interface CommitSummary {
@@ -270,6 +298,7 @@ export interface DesignSubmitMessage {
270
298
  insertMode: InsertMode;
271
299
  canvasWidth: number;
272
300
  canvasHeight: number;
301
+ canvasComponents?: CanvasComponent[];
273
302
  }
274
303
 
275
304
  /** Design iframe → Overlay: close the canvas wrapper */
@@ -282,6 +311,39 @@ export interface ClosePanelMessage {
282
311
  type: 'CLOSE_PANEL';
283
312
  }
284
313
 
314
+ // ---------------------------------------------------------------------------
315
+ // Component arm-and-place messages
316
+ // ---------------------------------------------------------------------------
317
+
318
+ /** Panel → Overlay: user armed a component for placement from the Draw tab */
319
+ export interface ComponentArmMessage {
320
+ type: 'COMPONENT_ARM';
321
+ to: 'overlay';
322
+ componentName: string;
323
+ storyId: string;
324
+ ghostHtml: string;
325
+ componentPath?: string; // Source file path from Storybook index, e.g. './src/components/Button.tsx'
326
+ args?: Record<string, unknown>; // Current prop values from ArgsForm
327
+ }
328
+
329
+ /** Panel → Overlay: user cancelled the armed state (panel click or escape) */
330
+ export interface ComponentDisarmMessage {
331
+ type: 'COMPONENT_DISARM';
332
+ to: 'overlay';
333
+ }
334
+
335
+ /** Overlay → Panel: overlay has disarmed (user placed or pressed Escape in app) */
336
+ export interface ComponentDisarmedMessage {
337
+ type: 'COMPONENT_DISARMED';
338
+ to: 'panel';
339
+ }
340
+
341
+ /** Overlay → Server: component was placed, stage a patch */
342
+ export interface ComponentDroppedMessage {
343
+ type: 'COMPONENT_DROPPED';
344
+ patch: Patch;
345
+ }
346
+
285
347
  // ---------------------------------------------------------------------------
286
348
  // Union types
287
349
  // ---------------------------------------------------------------------------
@@ -296,8 +358,10 @@ export type PanelToOverlay =
296
358
  | SwitchContainerMessage
297
359
  | InsertDesignCanvasMessage
298
360
  | CaptureScreenshotMessage
299
- | ClosePanelMessage;
300
- export type OverlayToServer = PatchStagedMessage;
361
+ | ClosePanelMessage
362
+ | ComponentArmMessage
363
+ | ComponentDisarmMessage;
364
+ export type OverlayToServer = PatchStagedMessage | ComponentDroppedMessage;
301
365
  export type PanelToServer = PatchCommitMessage | MessageStageMessage;
302
366
  export type ClientToServer =
303
367
  | RegisterMessage
@@ -306,6 +370,7 @@ export type ClientToServer =
306
370
  | MessageStageMessage
307
371
  | DesignSubmitMessage
308
372
  | DesignCloseMessage
373
+ | ComponentDroppedMessage
309
374
  | PingMessage;
310
375
  export type ServerToClient =
311
376
  | PongMessage
@@ -336,5 +401,9 @@ export type AnyMessage =
336
401
  | DesignSubmitMessage
337
402
  | DesignCloseMessage
338
403
  | ClosePanelMessage
404
+ | ComponentArmMessage
405
+ | ComponentDisarmMessage
406
+ | ComponentDisarmedMessage
407
+ | ComponentDroppedMessage
339
408
  | PingMessage
340
409
  | PongMessage;