@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.
- package/README.md +3 -2
- package/overlay/dist/overlay.js +5480 -2569
- package/package.json +4 -1
- package/panel/dist/assets/DesignMode-MoLVGcFD.js +511 -0
- package/panel/dist/assets/index-By_WWRF6.css +1 -0
- package/panel/dist/assets/index-C4LjGBfX.js +63 -0
- package/panel/dist/index.html +2 -2
- package/server/app.ts +63 -22
- package/server/mcp-tools.ts +117 -3
- package/server/queue.ts +38 -0
- package/server/storybook.ts +5 -2
- package/server/websocket.ts +11 -1
- package/shared/types.ts +251 -3
- package/panel/dist/assets/DesignMode-BD4jT4QR.js +0 -511
- package/panel/dist/assets/index-BdUOBofL.css +0 -1
- package/panel/dist/assets/index-Dbg-zA4t.js +0 -62
package/panel/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
font-size: 12px;
|
|
17
17
|
}
|
|
18
18
|
</style>
|
|
19
|
-
<script type="module" crossorigin src="/panel/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/panel/assets/index-
|
|
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,
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
target:
|
|
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
|
-
|
|
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
|
}
|
package/server/mcp-tools.ts
CHANGED
|
@@ -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
|
|
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;
|
package/server/storybook.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/server/websocket.ts
CHANGED
|
@@ -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);
|