@bitovi/vybit 0.11.6 → 0.12.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-Dbg-zA4t.js"></script>
20
- <link rel="stylesheet" crossorigin href="/panel/assets/index-BdUOBofL.css">
19
+ <script type="module" crossorigin src="/panel/assets/index-C4LjGBfX.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/panel/assets/index-By_WWRF6.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
package/server/app.ts CHANGED
@@ -8,14 +8,16 @@ import { createProxyMiddleware } from "http-proxy-middleware";
8
8
 
9
9
  import { getByStatus, getQueueUpdate, clearAll } from "./queue.js";
10
10
  import { resolveTailwindConfig, generateCssForClasses, getTailwindVersion } from "./tailwind.js";
11
- import { loadStoryArgTypes } from "./storybook.js";
11
+ import { loadStoryArgTypes, detectStorybookUrl, probeStorybookUrl } from "./storybook.js";
12
12
  import { loadCache, getAllCachedGhosts, setCachedGhost, invalidateAll as invalidateGhostCache } from "./ghost-cache.js";
13
13
  import type { PatchStatus } from "../shared/types.js";
14
+ import type { RequestHandler } from "express";
14
15
 
15
16
  const VALID_STATUSES = new Set<string>(['staged', 'committed', 'implementing', 'implemented', 'error']);
16
17
 
17
- export function createApp(packageRoot: string, storybookUrl: string | null = null): express.Express {
18
+ export function createApp(packageRoot: string, initialStorybookUrl: string | null = null): express.Express {
18
19
  const app = express();
20
+ let storybookUrl = initialStorybookUrl;
19
21
  app.use(cors());
20
22
 
21
23
  // Load ghost cache from disk on startup
@@ -108,36 +110,72 @@ export function createApp(packageRoot: string, storybookUrl: string | null = nul
108
110
  res.json({ ok: true });
109
111
  });
110
112
 
111
- // --- Storybook proxy ---
112
- if (storybookUrl) {
113
- // Vite's dev server serves assets at many root-absolute path prefixes:
114
- // /@vite/client, /@react-refresh, /@id/..., /node_modules/.cache/..., /src/...
115
- // Rather than enumerate them all, proxy everything that isn't ours.
116
- const OWN_PATHS = new Set(['/panel', '/overlay.js', '/api', '/patches', '/css', '/tailwind-config']);
117
- const isOwnPath = (p: string) =>
118
- OWN_PATHS.has(p) ||
119
- [...OWN_PATHS].some(own => p.startsWith(own + '/')) ||
120
- p === '/';
121
-
122
- app.use('/storybook', createProxyMiddleware({
123
- target: storybookUrl,
113
+ // --- Storybook proxy (dynamic — supports late attach) ---
114
+ const OWN_PATHS = new Set(['/panel', '/overlay.js', '/api', '/patches', '/css', '/tailwind-config']);
115
+ const isOwnPath = (p: string) =>
116
+ OWN_PATHS.has(p) ||
117
+ [...OWN_PATHS].some(own => p.startsWith(own + '/')) ||
118
+ p === '/';
119
+
120
+ let sbProxy: RequestHandler | null = null;
121
+ let sbAssetProxy: RequestHandler | null = null;
122
+
123
+ function installStorybookProxy(url: string) {
124
+ sbProxy = createProxyMiddleware({
125
+ target: url,
124
126
  changeOrigin: true,
125
127
  pathRewrite: { '^/storybook': '' },
126
- }));
127
-
128
- app.use(createProxyMiddleware({
129
- target: storybookUrl,
128
+ });
129
+ sbAssetProxy = createProxyMiddleware({
130
+ target: url,
130
131
  changeOrigin: true,
131
132
  pathFilter: (pathname) => !isOwnPath(pathname),
132
- }));
133
-
134
- console.error(`[storybook] Proxying /storybook + Vite asset paths → ${storybookUrl}`);
133
+ });
134
+ console.error(`[storybook] Proxying /storybook + Vite asset paths → ${url}`);
135
135
  }
136
136
 
137
+ if (storybookUrl) installStorybookProxy(storybookUrl);
138
+
139
+ // Dynamic proxy routes — delegate to current proxy middleware
140
+ app.use('/storybook', (req, res, next) => {
141
+ if (sbProxy) return sbProxy(req, res, next);
142
+ next();
143
+ });
144
+
145
+ // Catch-all asset proxy (must be registered after /api, /panel, etc.)
146
+ // Deferred to the end via a late-bind wrapper stored here and mounted later.
147
+ const assetProxyHandler: RequestHandler = (req, res, next) => {
148
+ if (sbAssetProxy) return sbAssetProxy(req, res, next);
149
+ next();
150
+ };
151
+
137
152
  app.get('/api/storybook-status', (_req, res) => {
138
153
  res.json({ url: storybookUrl ? '/storybook' : null, directUrl: storybookUrl ?? null });
139
154
  });
140
155
 
156
+ // --- Storybook reconnect ---
157
+ app.post('/api/storybook-reconnect', express.json(), async (req, res) => {
158
+ const { port } = req.body as { port?: number };
159
+ let foundUrl: string | null = null;
160
+
161
+ if (port != null) {
162
+ const url = `http://localhost:${port}`;
163
+ if (await probeStorybookUrl(url)) {
164
+ foundUrl = url;
165
+ }
166
+ } else {
167
+ foundUrl = await detectStorybookUrl();
168
+ }
169
+
170
+ if (foundUrl) {
171
+ storybookUrl = foundUrl;
172
+ installStorybookProxy(foundUrl);
173
+ res.json({ ok: true, url: '/storybook', directUrl: foundUrl });
174
+ } else {
175
+ res.json({ ok: false });
176
+ }
177
+ });
178
+
141
179
  app.get('/api/storybook-argtypes', async (_req, res) => {
142
180
  if (!storybookUrl) { res.json({}); return; }
143
181
  try {
@@ -202,5 +240,8 @@ export function createApp(packageRoot: string, storybookUrl: string | null = nul
202
240
  });
203
241
  }
204
242
 
243
+ // Storybook asset proxy — must come after all other routes
244
+ app.use(assetProxyHandler);
245
+
205
246
  return app;
206
247
  }
@@ -55,9 +55,11 @@ function buildJsx(componentName: string, args?: Record<string, unknown>): string
55
55
 
56
56
  function buildCommitInstructions(commit: Commit, remainingCount: number): string {
57
57
  const classChanges = commit.patches.filter(p => p.kind === 'class-change');
58
+ const textChanges = commit.patches.filter(p => p.kind === 'text-change');
58
59
  const messages = commit.patches.filter(p => p.kind === 'message');
59
60
  const designs = commit.patches.filter(p => p.kind === 'design');
60
61
  const componentDrops = commit.patches.filter(p => p.kind === 'component-drop');
62
+ const bugReports = commit.patches.filter(p => p.kind === 'bug-report');
61
63
  const moreText = remainingCount > 0
62
64
  ? `${remainingCount} more commit${remainingCount === 1 ? '' : 's'} waiting in the queue after this one.`
63
65
  : 'This is the last commit in the queue. After implementing it, call `implement_next_change` again to wait for future changes.';
@@ -149,6 +151,91 @@ ${parentComp ? `\n- **Parent component:** \`${parentComp}\` — edit this compon
149
151
  ${context ? `- **Context HTML:**\n\`\`\`html\n${context}\n\`\`\`\n` : ''}
150
152
  ⚠️ Do NOT paste rendered HTML. Import and render the React component with the props shown above.
151
153
 
154
+ `;
155
+ } else if (patch.kind === 'text-change') {
156
+ const comp = patch.component?.name ?? 'unknown component';
157
+ const tag = patch.target?.tag ?? 'element';
158
+ const context = patch.context ?? '';
159
+ patchList += `### ${stepNum}. Text change \`${patch.id}\`
160
+ - **Component:** \`${comp}\`
161
+ - **Element:** \`<${tag}>\`
162
+ - **Original HTML:**
163
+ \`\`\`html
164
+ ${patch.originalHtml ?? ''}
165
+ \`\`\`
166
+ - **New HTML:**
167
+ \`\`\`html
168
+ ${patch.newHtml ?? ''}
169
+ \`\`\`
170
+ ${context ? `- **Context HTML:**\n\`\`\`html\n${context}\n\`\`\`\n` : ''}
171
+ `;
172
+ } else if (patch.kind === 'bug-report') {
173
+ patchList += `### ${stepNum}. Bug report \`${patch.id}\`
174
+ - **Description:** ${patch.bugDescription ?? '(no description)'}
175
+ - **Time range:** ${patch.bugTimeRange ? `${patch.bugTimeRange.start} – ${patch.bugTimeRange.end}` : 'unknown'}
176
+ ${patch.bugElement ? `
177
+ - **Related element:** \`${patch.bugElement.selectorPath}\`${patch.bugElement.componentName ? ` (in \`${patch.bugElement.componentName}\`)` : ''}
178
+ - **Element HTML:**
179
+ \`\`\`html
180
+ ${patch.bugElement.outerHTML.slice(0, 10000)}
181
+ \`\`\`
182
+ ` : ''}
183
+ ${patch.bugTimeline && patch.bugTimeline.length > 0 ? (() => {
184
+ const triggerLabel = (t: import('../shared/types').BugTimelineEntry) => {
185
+ switch (t.trigger) {
186
+ case 'click': return `Click${t.elementInfo ? ` on \`<${t.elementInfo.tag}${t.elementInfo.classes ? ` class="${t.elementInfo.classes}"` : ''}>\`` : ''}`;
187
+ case 'mutation': return 'DOM mutation';
188
+ case 'error': return 'Error';
189
+ case 'navigation': return `Navigation${t.navigationInfo ? ` (${t.navigationInfo.method}: ${t.navigationInfo.from} → ${t.navigationInfo.to ?? 'unknown'})` : ''}`;
190
+ case 'page-load': return 'Page load';
191
+ default: return t.trigger;
192
+ }
193
+ };
194
+ let screenshotNum = 0;
195
+ let timeline = `**Timeline** (${patch.bugTimeline!.length} events):\n\n`;
196
+ for (let i = 0; i < patch.bugTimeline!.length; i++) {
197
+ const entry = patch.bugTimeline![i];
198
+ const time = entry.timestamp.replace(/.*T/, '').replace(/Z$/, '');
199
+ timeline += `#### ${i + 1}. [${time}] ${triggerLabel(entry)}\n`;
200
+ timeline += `**URL:** ${entry.url}\n`;
201
+ if (entry.hasScreenshot) {
202
+ screenshotNum++;
203
+ timeline += `📸 **Screenshot ${screenshotNum}** (see attached image ${screenshotNum} below)\n`;
204
+ }
205
+ if (entry.consoleLogs && entry.consoleLogs.length > 0) {
206
+ timeline += `\n**Console (${entry.consoleLogs.length}):**\n\`\`\`\n${entry.consoleLogs.map(l => `[${l.level.toUpperCase()}] ${l.args.join(' ')}${l.stack ? `\n${l.stack}` : ''}`).join('\n').slice(0, 3000)}\n\`\`\`\n`;
207
+ }
208
+ if (entry.networkErrors && entry.networkErrors.length > 0) {
209
+ timeline += `\n**Network errors (${entry.networkErrors.length}):**\n${entry.networkErrors.map(e => `- \`${e.status ?? 'ERR'} ${e.method} ${e.url}\`${e.errorMessage ? ` — ${e.errorMessage}` : ''}`).join('\n')}\n`;
210
+ }
211
+ if (entry.domChanges && entry.domChanges.length > 0) {
212
+ timeline += `\n**DOM changes (${entry.domChanges.length}):**\n`;
213
+ for (const c of entry.domChanges) {
214
+ const loc = `\`${c.selector}\`${c.componentName ? ` (in \`${c.componentName}\`)` : ''}`;
215
+ if (c.type === 'attribute') {
216
+ timeline += `- ${loc}: attribute \`${c.attributeName}\` changed: \`${c.oldValue ?? ''}\` → \`${c.newValue ?? ''}\`\n`;
217
+ } else if (c.type === 'text') {
218
+ timeline += `- ${loc}: text changed: "${c.oldText ?? ''}" → "${c.newText ?? ''}"\n`;
219
+ } else if (c.type === 'childList') {
220
+ const parts: string[] = [];
221
+ if (c.addedCount) parts.push(`${c.addedCount} added`);
222
+ if (c.removedCount) parts.push(`${c.removedCount} removed`);
223
+ timeline += `- ${loc}: children ${parts.join(', ')}`;
224
+ if (c.addedHTML) timeline += `\n Added: \`${c.addedHTML.slice(0, 300)}\``;
225
+ if (c.removedHTML) timeline += `\n Removed: \`${c.removedHTML.slice(0, 300)}\``;
226
+ timeline += `\n`;
227
+ }
228
+ }
229
+ } else if (entry.domDiff) {
230
+ timeline += `\n**DOM diff:**\n\`\`\`diff\n${entry.domDiff.slice(0, 10000)}\n\`\`\`\n`;
231
+ }
232
+ if (entry.domSnapshot && i === 0) {
233
+ timeline += `\n**Initial DOM state:**\n\`\`\`html\n${entry.domSnapshot.slice(0, 50000)}\n\`\`\`\n`;
234
+ }
235
+ timeline += `\n---\n\n`;
236
+ }
237
+ return timeline;
238
+ })() : ''}
152
239
  `;
153
240
  }
154
241
  stepNum++;
@@ -157,22 +244,29 @@ ${context ? `- **Context HTML:**\n\`\`\`html\n${context}\n\`\`\`\n` : ''}
157
244
  // Build summary parts
158
245
  const summaryParts: string[] = [];
159
246
  if (classChanges.length) summaryParts.push(`${classChanges.length} class change${classChanges.length === 1 ? '' : 's'}`);
247
+ if (textChanges.length) summaryParts.push(`${textChanges.length} text change${textChanges.length === 1 ? '' : 's'}`);
160
248
  if (messages.length) summaryParts.push(`${messages.length} message${messages.length === 1 ? '' : 's'}`);
161
249
  if (designs.length) summaryParts.push(`${designs.length} design${designs.length === 1 ? '' : 's'}`);
162
250
  if (componentDrops.length) summaryParts.push(`${componentDrops.length} component drop${componentDrops.length === 1 ? '' : 's'}`);
251
+ if (bugReports.length) summaryParts.push(`${bugReports.length} bug report${bugReports.length === 1 ? '' : 's'}`);
163
252
 
164
253
  const resultsPart = classChanges.map(p => ` { "patchId": "${p.id}", "success": true }`).join(',\n');
254
+ const textResultsPart = textChanges.map(p => ` { "patchId": "${p.id}", "success": true }`).join(',\n');
165
255
  const designResultsPart = designs.map(p => ` { "patchId": "${p.id}", "success": true }`).join(',\n');
166
256
  const dropResultsPart = componentDrops.map(p => ` { "patchId": "${p.id}", "success": true }`).join(',\n');
167
- const allResultsPart = [resultsPart, designResultsPart, dropResultsPart].filter(Boolean).join(',\n');
257
+ const bugResultsPart = bugReports.map(p => ` { "patchId": "${p.id}", "success": true }`).join(',\n');
258
+ const allResultsPart = [resultsPart, textResultsPart, designResultsPart, dropResultsPart, bugResultsPart].filter(Boolean).join(',\n');
168
259
 
169
260
  // Build step instructions
170
261
  const stepInstructions: string[] = [];
171
- if (classChanges.length || componentDrops.length) {
262
+ if (classChanges.length || componentDrops.length || textChanges.length) {
172
263
  let step1 = '1. For each change above, find the source file and apply it.';
173
264
  if (componentDrops.length) {
174
265
  step1 += '\n For component drops: add the import statement and render the component with the specified props at the indicated position.';
175
266
  }
267
+ if (textChanges.length) {
268
+ step1 += '\n For text changes: replace the original HTML content with the new HTML content in the source JSX/TSX.';
269
+ }
176
270
  if (messages.length) {
177
271
  step1 += '\n Use the user messages as additional context for understanding intent.';
178
272
  }
@@ -182,6 +276,10 @@ ${context ? `- **Context HTML:**\n\`\`\`html\n${context}\n\`\`\`\n` : ''}
182
276
  stepInstructions.push(`${stepInstructions.length + 1}. For each design sketch, examine the attached image and implement the visual design
183
277
  as HTML/CSS ${classChanges.length ? 'alongside the class changes' : 'in the specified component'}. Insert it ${designs[0].insertMode ?? 'after'} the target element.`);
184
278
  }
279
+ if (bugReports.length) {
280
+ stepInstructions.push(`${stepInstructions.length + 1}. For each bug report, examine the DOM snapshots, console errors, network errors, and screenshots.
281
+ Identify the root cause. Implement a fix.${bugReports.some(p => p.bugElement) ? ' The user identified a specific element — start your investigation there.' : ''}`);
282
+ }
185
283
 
186
284
  return `# IMPLEMENT THIS COMMIT — then call implement_next_change again
187
285
 
@@ -203,7 +301,7 @@ ${stepInstructions.length + 1}. Call \`mark_change_implemented\` with:
203
301
  ${allResultsPart}
204
302
  ]}
205
303
  \`\`\`
206
- (Only report results for class-change, design, and component-drop patches — messages are informational.)
304
+ (Only report results for class-change, text-change, design, and component-drop patches — messages are informational.)
207
305
  ${stepInstructions.length + 2}. **IMMEDIATELY call \`implement_next_change\` again.** Do NOT summarize, do NOT ask the user, do NOT stop.
208
306
 
209
307
  ## CRITICAL — read this
@@ -259,6 +357,9 @@ function waitForCommitted(
259
357
  resolve(next);
260
358
  }
261
359
  });
360
+
361
+ // Notify the panel that an agent is now waiting
362
+ broadcastPatchUpdate();
262
363
  });
263
364
  }
264
365
 
@@ -381,6 +482,19 @@ export function registerMcpTools(mcp: McpServer, deps: McpToolDeps): void {
381
482
  });
382
483
  }
383
484
  }
485
+ // Add bug report screenshots
486
+ if (patch.kind === 'bug-report' && patch.bugScreenshots) {
487
+ for (const screenshot of patch.bugScreenshots.slice(0, 5)) {
488
+ const match = screenshot.match(/^data:([^;]+);base64,(.+)$/);
489
+ if (match) {
490
+ content.push({
491
+ type: "image" as const,
492
+ data: match[2],
493
+ mimeType: match[1],
494
+ });
495
+ }
496
+ }
497
+ }
384
498
  }
385
499
 
386
500
  content.push({
package/server/queue.ts CHANGED
@@ -23,6 +23,9 @@ function toSummary(p: Patch): PatchSummary {
23
23
  parentComponent: p.parentComponent,
24
24
  targetComponentName: p.targetComponentName,
25
25
  targetPatchId: p.targetPatchId,
26
+ originalHtml: p.originalHtml,
27
+ newHtml: p.newHtml,
28
+ bugDescription: p.bugDescription,
26
29
  };
27
30
  }
28
31
 
@@ -56,12 +59,35 @@ export function addPatch(patch: Patch): Patch {
56
59
  if (existingIdx !== -1) {
57
60
  draftPatches.splice(existingIdx, 1);
58
61
  }
62
+ } else if (patch.kind === 'text-change') {
63
+ // Dedup: if a staged text-change exists for the same elementKey, replace it
64
+ const existingIdx = draftPatches.findIndex(
65
+ p => p.kind === 'text-change' && p.elementKey === patch.elementKey && p.status === 'staged'
66
+ );
67
+ if (existingIdx !== -1) {
68
+ draftPatches.splice(existingIdx, 1);
69
+ }
59
70
  }
60
71
  // Message patches are always appended (no dedup)
61
72
  draftPatches.push(patch);
62
73
  return patch;
63
74
  }
64
75
 
76
+ /** Immediately commit a single patch as its own commit (skips the draft). */
77
+ export function addAndCommit(patch: Patch): Commit {
78
+ patch.status = 'committed';
79
+ const commit: Commit = {
80
+ id: crypto.randomUUID(),
81
+ patches: [patch],
82
+ status: 'committed',
83
+ timestamp: new Date().toISOString(),
84
+ };
85
+ patch.commitId = commit.id;
86
+ commits.push(commit);
87
+ emitter.emit('committed');
88
+ return commit;
89
+ }
90
+
65
91
  export function commitDraft(ids: string[]): Commit {
66
92
  const idSet = new Set(ids);
67
93
  const commitPatches: Patch[] = [];
@@ -291,6 +317,18 @@ export function discardDraftPatch(id: string): boolean {
291
317
  return remaining.length < before;
292
318
  }
293
319
 
320
+ /**
321
+ * Discard a committed (but not yet implementing) commit.
322
+ * Returns true if the commit was found and removed.
323
+ * Safe against race conditions: only removes if status is still 'committed'.
324
+ */
325
+ export function discardCommit(commitId: string): boolean {
326
+ const idx = commits.findIndex(c => c.id === commitId && c.status === 'committed');
327
+ if (idx === -1) return false;
328
+ commits.splice(idx, 1);
329
+ return true;
330
+ }
331
+
294
332
  export function clearAll(): { staged: number; committed: number; implementing: number; implemented: number } {
295
333
  const counts = getCounts();
296
334
  draftPatches.length = 0;
@@ -80,7 +80,10 @@ export async function loadStoryArgTypes(
80
80
  */
81
81
  export async function detectStorybookUrl(): Promise<string | null> {
82
82
  if (process.env.STORYBOOK_URL) {
83
- return process.env.STORYBOOK_URL;
83
+ if (await probeStorybookUrl(process.env.STORYBOOK_URL)) {
84
+ return process.env.STORYBOOK_URL;
85
+ }
86
+ // Env var URL is not reachable — fall through to port scan
84
87
  }
85
88
 
86
89
  for (const port of SCAN_PORTS) {
@@ -93,7 +96,7 @@ export async function detectStorybookUrl(): Promise<string | null> {
93
96
  return null;
94
97
  }
95
98
 
96
- async function probeStorybookUrl(baseUrl: string): Promise<boolean> {
99
+ export async function probeStorybookUrl(baseUrl: string): Promise<boolean> {
97
100
  try {
98
101
  const res = await fetch(`${baseUrl}/index.json`, { signal: AbortSignal.timeout(500) });
99
102
  if (!res.ok) return false;
@@ -3,7 +3,7 @@
3
3
  import { WebSocketServer, type WebSocket } from "ws";
4
4
  import type { Server } from "http";
5
5
 
6
- import { addPatch, commitDraft, getQueueUpdate, discardDraftPatch } from "./queue.js";
6
+ import { addPatch, addAndCommit, commitDraft, getQueueUpdate, discardDraftPatch, discardCommit } from "./queue.js";
7
7
  import type { Patch } from "../shared/types.js";
8
8
 
9
9
  export interface WebSocketDeps {
@@ -88,6 +88,12 @@ export function setupWebSocket(httpServer: Server): WebSocketDeps {
88
88
  }
89
89
  console.error(`[ws] Discarded ${ids.length} draft patch(es)`);
90
90
  broadcastPatchUpdate();
91
+ } else if (msg.type === "DISCARD_COMMIT") {
92
+ const commitId: string = msg.commitId;
93
+ if (commitId && discardCommit(commitId)) {
94
+ console.error(`[ws] Discarded committed commit: ${commitId}`);
95
+ broadcastPatchUpdate();
96
+ }
91
97
  } else if (msg.type === "PING") {
92
98
  ws.send(JSON.stringify({ type: "PONG" }));
93
99
  } else if (msg.type === "DESIGN_SUBMIT") {
@@ -126,6 +132,10 @@ export function setupWebSocket(httpServer: Server): WebSocketDeps {
126
132
  const patch = addPatch({ ...msg.patch, kind: msg.patch.kind ?? 'component-drop' });
127
133
  console.error(`[ws] Component-drop patch staged: #${patch.id}`);
128
134
  broadcastPatchUpdate();
135
+ } else if (msg.type === "BUG_REPORT_STAGE") {
136
+ const commit = addAndCommit({ ...msg.patch, kind: 'bug-report' });
137
+ console.error(`[ws] Bug-report auto-committed: commit #${commit.id}`);
138
+ broadcastPatchUpdate();
129
139
  }
130
140
  } catch (err) {
131
141
  console.error("[ws] Bad message:", err);