stashes 0.1.10 → 0.1.12
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/dist/cli.js +111 -33
- package/dist/mcp.js +111 -33
- package/dist/web/assets/index-Cj1_hTJg.js +96 -0
- package/dist/web/assets/index-DYv09NYI.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BjonyU1P.css +0 -1
- package/dist/web/assets/index-R4H2ja1O.js +0 -62
package/dist/cli.js
CHANGED
|
@@ -312,7 +312,29 @@ class WorktreeManager {
|
|
|
312
312
|
await this.git.raw(["worktree", "prune"]);
|
|
313
313
|
}
|
|
314
314
|
logger.info("worktree", `creating pool preview: ${stashId}`, { branch, path: previewPath });
|
|
315
|
-
|
|
315
|
+
try {
|
|
316
|
+
await this.git.raw(["worktree", "add", previewPath, branch]);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
319
|
+
if (msg.includes("is already used by worktree")) {
|
|
320
|
+
logger.warn("worktree", `branch ${branch} locked by stale worktree, cleaning up`);
|
|
321
|
+
const staleDir = join3(this.projectPath, ".stashes", "worktrees", stashId);
|
|
322
|
+
const legacyPreview = join3(this.projectPath, ".stashes", "preview");
|
|
323
|
+
for (const dir of [staleDir, legacyPreview]) {
|
|
324
|
+
if (existsSync3(dir)) {
|
|
325
|
+
try {
|
|
326
|
+
await this.git.raw(["worktree", "remove", "--force", dir]);
|
|
327
|
+
} catch {
|
|
328
|
+
rmSync(dir, { recursive: true, force: true });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
await this.git.raw(["worktree", "prune"]);
|
|
333
|
+
await this.git.raw(["worktree", "add", previewPath, branch]);
|
|
334
|
+
} else {
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
316
338
|
this.symlinkDeps(previewPath);
|
|
317
339
|
logger.info("worktree", `pool preview created: ${stashId}`);
|
|
318
340
|
return previewPath;
|
|
@@ -711,11 +733,15 @@ async function generate(opts) {
|
|
|
711
733
|
}
|
|
712
734
|
}
|
|
713
735
|
const completedStashes = [];
|
|
714
|
-
const
|
|
736
|
+
const existingStashes = persistence.listStashes(projectId);
|
|
737
|
+
const maxNumber = existingStashes.reduce((max, s) => Math.max(max, s.number ?? 0), 0);
|
|
738
|
+
const stashPromises = selectedDirectives.map(async (directive, idx) => {
|
|
715
739
|
const stashId = `stash_${crypto.randomUUID().substring(0, 8)}`;
|
|
740
|
+
const stashNumber = maxNumber + idx + 1;
|
|
716
741
|
const worktree = await worktreeManager.createForGeneration(stashId);
|
|
717
742
|
const stash = {
|
|
718
743
|
id: stashId,
|
|
744
|
+
number: stashNumber,
|
|
719
745
|
projectId,
|
|
720
746
|
prompt,
|
|
721
747
|
componentPath: component?.filePath,
|
|
@@ -729,7 +755,7 @@ async function generate(opts) {
|
|
|
729
755
|
createdAt: new Date().toISOString()
|
|
730
756
|
};
|
|
731
757
|
persistence.saveStash(stash);
|
|
732
|
-
emit(onProgress, { type: "generating", stashId });
|
|
758
|
+
emit(onProgress, { type: "generating", stashId, number: stashNumber });
|
|
733
759
|
let stashPrompt;
|
|
734
760
|
if (component?.filePath) {
|
|
735
761
|
stashPrompt = buildStashPrompt({ name: component.exportName || component.filePath, filePath: component.filePath, domSelector: "" }, sourceCode, prompt, directive);
|
|
@@ -834,9 +860,12 @@ async function vary(opts) {
|
|
|
834
860
|
if (!sourceStash)
|
|
835
861
|
throw new Error(`Source stash ${sourceStashId} not found`);
|
|
836
862
|
const stashId = `stash_${crypto.randomUUID().substring(0, 8)}`;
|
|
863
|
+
const existingStashes = persistence.listStashes(projectId);
|
|
864
|
+
const stashNumber = existingStashes.reduce((max, s) => Math.max(max, s.number ?? 0), 0) + 1;
|
|
837
865
|
const worktree = await worktreeManager.createForVary(stashId, sourceStash.branch);
|
|
838
866
|
const stash = {
|
|
839
867
|
id: stashId,
|
|
868
|
+
number: stashNumber,
|
|
840
869
|
projectId,
|
|
841
870
|
prompt,
|
|
842
871
|
componentPath: sourceStash.componentPath,
|
|
@@ -850,7 +879,7 @@ async function vary(opts) {
|
|
|
850
879
|
createdAt: new Date().toISOString()
|
|
851
880
|
};
|
|
852
881
|
persistence.saveStash(stash);
|
|
853
|
-
emit2(onProgress, { type: "generating", stashId });
|
|
882
|
+
emit2(onProgress, { type: "generating", stashId, number: stashNumber });
|
|
854
883
|
const varyPrompt = `The user wants to vary the current UI. Apply this change: ${prompt}`;
|
|
855
884
|
const aiProcess = startAiProcess(stashId, varyPrompt, worktree.path);
|
|
856
885
|
try {
|
|
@@ -1195,8 +1224,8 @@ class StashService {
|
|
|
1195
1224
|
});
|
|
1196
1225
|
}
|
|
1197
1226
|
}
|
|
1198
|
-
async
|
|
1199
|
-
const component = this.selectedComponent;
|
|
1227
|
+
async message(projectId, message, referenceStashIds, componentContext) {
|
|
1228
|
+
const component = componentContext ? { name: componentContext.name, filePath: this.selectedComponent?.filePath || "" } : this.selectedComponent;
|
|
1200
1229
|
let sourceCode = "";
|
|
1201
1230
|
const filePath = component?.filePath || "";
|
|
1202
1231
|
if (filePath && filePath !== "auto-detect") {
|
|
@@ -1216,8 +1245,11 @@ ${refs.join(`
|
|
|
1216
1245
|
}
|
|
1217
1246
|
}
|
|
1218
1247
|
const chatPrompt = [
|
|
1219
|
-
"
|
|
1220
|
-
"
|
|
1248
|
+
"You are helping the user explore UI design variations for their project.",
|
|
1249
|
+
"You have access to stashes MCP tools to generate, list, browse, vary, and apply stashes.",
|
|
1250
|
+
"If the user asks you to generate, create, or make variations, use the stashes_generate tool.",
|
|
1251
|
+
"If the user asks to vary an existing stash, use the stashes_vary tool.",
|
|
1252
|
+
"Otherwise, respond conversationally about their project and stashes.",
|
|
1221
1253
|
"",
|
|
1222
1254
|
component ? `Component: ${component.name}` : "",
|
|
1223
1255
|
filePath !== "auto-detect" ? `File: ${filePath}` : "",
|
|
@@ -1228,7 +1260,7 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1228
1260
|
\`\`\`` : "",
|
|
1229
1261
|
stashContext,
|
|
1230
1262
|
"",
|
|
1231
|
-
`User
|
|
1263
|
+
`User: ${message}`
|
|
1232
1264
|
].filter(Boolean).join(`
|
|
1233
1265
|
`);
|
|
1234
1266
|
const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath);
|
|
@@ -1237,7 +1269,55 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1237
1269
|
for await (const chunk of parseClaudeStream(aiProcess.process)) {
|
|
1238
1270
|
if (chunk.type === "text") {
|
|
1239
1271
|
fullResponse += chunk.content;
|
|
1240
|
-
this.broadcast({
|
|
1272
|
+
this.broadcast({
|
|
1273
|
+
type: "ai_stream",
|
|
1274
|
+
content: chunk.content,
|
|
1275
|
+
streamType: "text",
|
|
1276
|
+
source: "chat"
|
|
1277
|
+
});
|
|
1278
|
+
} else if (chunk.type === "thinking") {
|
|
1279
|
+
this.broadcast({
|
|
1280
|
+
type: "ai_stream",
|
|
1281
|
+
content: chunk.content,
|
|
1282
|
+
streamType: "thinking",
|
|
1283
|
+
source: "chat"
|
|
1284
|
+
});
|
|
1285
|
+
} else if (chunk.type === "tool_use") {
|
|
1286
|
+
let toolName = "unknown";
|
|
1287
|
+
let toolParams = {};
|
|
1288
|
+
try {
|
|
1289
|
+
const parsed = JSON.parse(chunk.content);
|
|
1290
|
+
toolName = parsed.tool || parsed.name || "unknown";
|
|
1291
|
+
toolParams = parsed.input || parsed.params || {};
|
|
1292
|
+
} catch {}
|
|
1293
|
+
this.broadcast({
|
|
1294
|
+
type: "ai_stream",
|
|
1295
|
+
content: chunk.content,
|
|
1296
|
+
streamType: "tool_start",
|
|
1297
|
+
source: "chat",
|
|
1298
|
+
toolName,
|
|
1299
|
+
toolParams,
|
|
1300
|
+
toolStatus: "running"
|
|
1301
|
+
});
|
|
1302
|
+
} else if (chunk.type === "tool_result") {
|
|
1303
|
+
let toolName = "unknown";
|
|
1304
|
+
let toolResult = "";
|
|
1305
|
+
try {
|
|
1306
|
+
const parsed = JSON.parse(chunk.content);
|
|
1307
|
+
toolName = parsed.tool || parsed.name || "unknown";
|
|
1308
|
+
toolResult = typeof parsed.result === "string" ? parsed.result.substring(0, 200) : JSON.stringify(parsed.result).substring(0, 200);
|
|
1309
|
+
} catch {
|
|
1310
|
+
toolResult = chunk.content.substring(0, 200);
|
|
1311
|
+
}
|
|
1312
|
+
this.broadcast({
|
|
1313
|
+
type: "ai_stream",
|
|
1314
|
+
content: chunk.content,
|
|
1315
|
+
streamType: "tool_end",
|
|
1316
|
+
source: "chat",
|
|
1317
|
+
toolName,
|
|
1318
|
+
toolStatus: "completed",
|
|
1319
|
+
toolResult
|
|
1320
|
+
});
|
|
1241
1321
|
}
|
|
1242
1322
|
}
|
|
1243
1323
|
await aiProcess.process.exited;
|
|
@@ -1253,7 +1333,7 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1253
1333
|
} catch (err) {
|
|
1254
1334
|
this.broadcast({
|
|
1255
1335
|
type: "ai_stream",
|
|
1256
|
-
content: `
|
|
1336
|
+
content: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
1257
1337
|
streamType: "text",
|
|
1258
1338
|
source: "chat"
|
|
1259
1339
|
});
|
|
@@ -1266,7 +1346,12 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1266
1346
|
case "generating":
|
|
1267
1347
|
case "screenshotting":
|
|
1268
1348
|
case "ready":
|
|
1269
|
-
this.broadcast({
|
|
1349
|
+
this.broadcast({
|
|
1350
|
+
type: "stash:status",
|
|
1351
|
+
stashId: event.stashId,
|
|
1352
|
+
status: event.type === "ready" ? "ready" : event.type,
|
|
1353
|
+
..."number" in event ? { number: event.number } : {}
|
|
1354
|
+
});
|
|
1270
1355
|
if (event.type === "ready" && "screenshotPath" in event && event.screenshotPath) {
|
|
1271
1356
|
this.broadcast({ type: "stash:screenshot", stashId: event.stashId, url: event.screenshotPath });
|
|
1272
1357
|
}
|
|
@@ -1274,9 +1359,11 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1274
1359
|
case "error":
|
|
1275
1360
|
this.broadcast({ type: "stash:error", stashId: event.stashId, error: event.error });
|
|
1276
1361
|
break;
|
|
1277
|
-
case "ai_stream":
|
|
1278
|
-
|
|
1362
|
+
case "ai_stream": {
|
|
1363
|
+
const streamType = event.streamType === "tool_use" ? "tool_start" : event.streamType;
|
|
1364
|
+
this.broadcast({ type: "ai_stream", content: event.content, streamType });
|
|
1279
1365
|
break;
|
|
1366
|
+
}
|
|
1280
1367
|
}
|
|
1281
1368
|
}
|
|
1282
1369
|
async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT, referenceStashIds) {
|
|
@@ -1370,28 +1457,17 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
|
|
|
1370
1457
|
case "select_component":
|
|
1371
1458
|
stashService.setSelectedComponent(event.component);
|
|
1372
1459
|
break;
|
|
1373
|
-
case "
|
|
1460
|
+
case "message":
|
|
1374
1461
|
persistence.saveChatMessage(event.projectId, {
|
|
1375
1462
|
id: crypto.randomUUID(),
|
|
1376
1463
|
role: "user",
|
|
1377
1464
|
content: event.message,
|
|
1378
1465
|
type: "text",
|
|
1379
|
-
createdAt: new Date().toISOString()
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
break;
|
|
1383
|
-
case "generate":
|
|
1384
|
-
persistence.saveChatMessage(event.projectId, {
|
|
1385
|
-
id: crypto.randomUUID(),
|
|
1386
|
-
role: "user",
|
|
1387
|
-
content: event.prompt,
|
|
1388
|
-
type: "text",
|
|
1389
|
-
createdAt: new Date().toISOString()
|
|
1466
|
+
createdAt: new Date().toISOString(),
|
|
1467
|
+
referenceStashIds: event.referenceStashIds,
|
|
1468
|
+
componentContext: event.componentContext
|
|
1390
1469
|
});
|
|
1391
|
-
await stashService.
|
|
1392
|
-
break;
|
|
1393
|
-
case "vary":
|
|
1394
|
-
await stashService.vary(event.sourceStashId, event.prompt);
|
|
1470
|
+
await stashService.message(event.projectId, event.message, event.referenceStashIds, event.componentContext);
|
|
1395
1471
|
break;
|
|
1396
1472
|
case "interact":
|
|
1397
1473
|
await stashService.switchPreview(event.stashId, event.sortedStashIds);
|
|
@@ -1407,9 +1483,11 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
|
|
|
1407
1483
|
break;
|
|
1408
1484
|
}
|
|
1409
1485
|
} catch (err) {
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1486
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1487
|
+
logger.error("ws", `handler failed for ${event.type}`, { error: errorMsg });
|
|
1488
|
+
if ("stashId" in event && event.stashId) {
|
|
1489
|
+
broadcast({ type: "stash:error", stashId: event.stashId, error: errorMsg });
|
|
1490
|
+
}
|
|
1413
1491
|
}
|
|
1414
1492
|
},
|
|
1415
1493
|
close(ws) {
|
package/dist/mcp.js
CHANGED
|
@@ -248,7 +248,29 @@ class WorktreeManager {
|
|
|
248
248
|
await this.git.raw(["worktree", "prune"]);
|
|
249
249
|
}
|
|
250
250
|
logger.info("worktree", `creating pool preview: ${stashId}`, { branch, path: previewPath });
|
|
251
|
-
|
|
251
|
+
try {
|
|
252
|
+
await this.git.raw(["worktree", "add", previewPath, branch]);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
255
|
+
if (msg.includes("is already used by worktree")) {
|
|
256
|
+
logger.warn("worktree", `branch ${branch} locked by stale worktree, cleaning up`);
|
|
257
|
+
const staleDir = join2(this.projectPath, ".stashes", "worktrees", stashId);
|
|
258
|
+
const legacyPreview = join2(this.projectPath, ".stashes", "preview");
|
|
259
|
+
for (const dir of [staleDir, legacyPreview]) {
|
|
260
|
+
if (existsSync2(dir)) {
|
|
261
|
+
try {
|
|
262
|
+
await this.git.raw(["worktree", "remove", "--force", dir]);
|
|
263
|
+
} catch {
|
|
264
|
+
rmSync(dir, { recursive: true, force: true });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
await this.git.raw(["worktree", "prune"]);
|
|
269
|
+
await this.git.raw(["worktree", "add", previewPath, branch]);
|
|
270
|
+
} else {
|
|
271
|
+
throw err;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
252
274
|
this.symlinkDeps(previewPath);
|
|
253
275
|
logger.info("worktree", `pool preview created: ${stashId}`);
|
|
254
276
|
return previewPath;
|
|
@@ -647,11 +669,15 @@ async function generate(opts) {
|
|
|
647
669
|
}
|
|
648
670
|
}
|
|
649
671
|
const completedStashes = [];
|
|
650
|
-
const
|
|
672
|
+
const existingStashes = persistence.listStashes(projectId);
|
|
673
|
+
const maxNumber = existingStashes.reduce((max, s) => Math.max(max, s.number ?? 0), 0);
|
|
674
|
+
const stashPromises = selectedDirectives.map(async (directive, idx) => {
|
|
651
675
|
const stashId = `stash_${crypto.randomUUID().substring(0, 8)}`;
|
|
676
|
+
const stashNumber = maxNumber + idx + 1;
|
|
652
677
|
const worktree = await worktreeManager.createForGeneration(stashId);
|
|
653
678
|
const stash = {
|
|
654
679
|
id: stashId,
|
|
680
|
+
number: stashNumber,
|
|
655
681
|
projectId,
|
|
656
682
|
prompt,
|
|
657
683
|
componentPath: component?.filePath,
|
|
@@ -665,7 +691,7 @@ async function generate(opts) {
|
|
|
665
691
|
createdAt: new Date().toISOString()
|
|
666
692
|
};
|
|
667
693
|
persistence.saveStash(stash);
|
|
668
|
-
emit(onProgress, { type: "generating", stashId });
|
|
694
|
+
emit(onProgress, { type: "generating", stashId, number: stashNumber });
|
|
669
695
|
let stashPrompt;
|
|
670
696
|
if (component?.filePath) {
|
|
671
697
|
stashPrompt = buildStashPrompt({ name: component.exportName || component.filePath, filePath: component.filePath, domSelector: "" }, sourceCode, prompt, directive);
|
|
@@ -770,9 +796,12 @@ async function vary(opts) {
|
|
|
770
796
|
if (!sourceStash)
|
|
771
797
|
throw new Error(`Source stash ${sourceStashId} not found`);
|
|
772
798
|
const stashId = `stash_${crypto.randomUUID().substring(0, 8)}`;
|
|
799
|
+
const existingStashes = persistence.listStashes(projectId);
|
|
800
|
+
const stashNumber = existingStashes.reduce((max, s) => Math.max(max, s.number ?? 0), 0) + 1;
|
|
773
801
|
const worktree = await worktreeManager.createForVary(stashId, sourceStash.branch);
|
|
774
802
|
const stash = {
|
|
775
803
|
id: stashId,
|
|
804
|
+
number: stashNumber,
|
|
776
805
|
projectId,
|
|
777
806
|
prompt,
|
|
778
807
|
componentPath: sourceStash.componentPath,
|
|
@@ -786,7 +815,7 @@ async function vary(opts) {
|
|
|
786
815
|
createdAt: new Date().toISOString()
|
|
787
816
|
};
|
|
788
817
|
persistence.saveStash(stash);
|
|
789
|
-
emit2(onProgress, { type: "generating", stashId });
|
|
818
|
+
emit2(onProgress, { type: "generating", stashId, number: stashNumber });
|
|
790
819
|
const varyPrompt = `The user wants to vary the current UI. Apply this change: ${prompt}`;
|
|
791
820
|
const aiProcess = startAiProcess(stashId, varyPrompt, worktree.path);
|
|
792
821
|
try {
|
|
@@ -1325,8 +1354,8 @@ class StashService {
|
|
|
1325
1354
|
});
|
|
1326
1355
|
}
|
|
1327
1356
|
}
|
|
1328
|
-
async
|
|
1329
|
-
const component = this.selectedComponent;
|
|
1357
|
+
async message(projectId, message, referenceStashIds, componentContext) {
|
|
1358
|
+
const component = componentContext ? { name: componentContext.name, filePath: this.selectedComponent?.filePath || "" } : this.selectedComponent;
|
|
1330
1359
|
let sourceCode = "";
|
|
1331
1360
|
const filePath = component?.filePath || "";
|
|
1332
1361
|
if (filePath && filePath !== "auto-detect") {
|
|
@@ -1346,8 +1375,11 @@ ${refs.join(`
|
|
|
1346
1375
|
}
|
|
1347
1376
|
}
|
|
1348
1377
|
const chatPrompt = [
|
|
1349
|
-
"
|
|
1350
|
-
"
|
|
1378
|
+
"You are helping the user explore UI design variations for their project.",
|
|
1379
|
+
"You have access to stashes MCP tools to generate, list, browse, vary, and apply stashes.",
|
|
1380
|
+
"If the user asks you to generate, create, or make variations, use the stashes_generate tool.",
|
|
1381
|
+
"If the user asks to vary an existing stash, use the stashes_vary tool.",
|
|
1382
|
+
"Otherwise, respond conversationally about their project and stashes.",
|
|
1351
1383
|
"",
|
|
1352
1384
|
component ? `Component: ${component.name}` : "",
|
|
1353
1385
|
filePath !== "auto-detect" ? `File: ${filePath}` : "",
|
|
@@ -1358,7 +1390,7 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1358
1390
|
\`\`\`` : "",
|
|
1359
1391
|
stashContext,
|
|
1360
1392
|
"",
|
|
1361
|
-
`User
|
|
1393
|
+
`User: ${message}`
|
|
1362
1394
|
].filter(Boolean).join(`
|
|
1363
1395
|
`);
|
|
1364
1396
|
const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath);
|
|
@@ -1367,7 +1399,55 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1367
1399
|
for await (const chunk of parseClaudeStream(aiProcess.process)) {
|
|
1368
1400
|
if (chunk.type === "text") {
|
|
1369
1401
|
fullResponse += chunk.content;
|
|
1370
|
-
this.broadcast({
|
|
1402
|
+
this.broadcast({
|
|
1403
|
+
type: "ai_stream",
|
|
1404
|
+
content: chunk.content,
|
|
1405
|
+
streamType: "text",
|
|
1406
|
+
source: "chat"
|
|
1407
|
+
});
|
|
1408
|
+
} else if (chunk.type === "thinking") {
|
|
1409
|
+
this.broadcast({
|
|
1410
|
+
type: "ai_stream",
|
|
1411
|
+
content: chunk.content,
|
|
1412
|
+
streamType: "thinking",
|
|
1413
|
+
source: "chat"
|
|
1414
|
+
});
|
|
1415
|
+
} else if (chunk.type === "tool_use") {
|
|
1416
|
+
let toolName = "unknown";
|
|
1417
|
+
let toolParams = {};
|
|
1418
|
+
try {
|
|
1419
|
+
const parsed = JSON.parse(chunk.content);
|
|
1420
|
+
toolName = parsed.tool || parsed.name || "unknown";
|
|
1421
|
+
toolParams = parsed.input || parsed.params || {};
|
|
1422
|
+
} catch {}
|
|
1423
|
+
this.broadcast({
|
|
1424
|
+
type: "ai_stream",
|
|
1425
|
+
content: chunk.content,
|
|
1426
|
+
streamType: "tool_start",
|
|
1427
|
+
source: "chat",
|
|
1428
|
+
toolName,
|
|
1429
|
+
toolParams,
|
|
1430
|
+
toolStatus: "running"
|
|
1431
|
+
});
|
|
1432
|
+
} else if (chunk.type === "tool_result") {
|
|
1433
|
+
let toolName = "unknown";
|
|
1434
|
+
let toolResult = "";
|
|
1435
|
+
try {
|
|
1436
|
+
const parsed = JSON.parse(chunk.content);
|
|
1437
|
+
toolName = parsed.tool || parsed.name || "unknown";
|
|
1438
|
+
toolResult = typeof parsed.result === "string" ? parsed.result.substring(0, 200) : JSON.stringify(parsed.result).substring(0, 200);
|
|
1439
|
+
} catch {
|
|
1440
|
+
toolResult = chunk.content.substring(0, 200);
|
|
1441
|
+
}
|
|
1442
|
+
this.broadcast({
|
|
1443
|
+
type: "ai_stream",
|
|
1444
|
+
content: chunk.content,
|
|
1445
|
+
streamType: "tool_end",
|
|
1446
|
+
source: "chat",
|
|
1447
|
+
toolName,
|
|
1448
|
+
toolStatus: "completed",
|
|
1449
|
+
toolResult
|
|
1450
|
+
});
|
|
1371
1451
|
}
|
|
1372
1452
|
}
|
|
1373
1453
|
await aiProcess.process.exited;
|
|
@@ -1383,7 +1463,7 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1383
1463
|
} catch (err) {
|
|
1384
1464
|
this.broadcast({
|
|
1385
1465
|
type: "ai_stream",
|
|
1386
|
-
content: `
|
|
1466
|
+
content: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
1387
1467
|
streamType: "text",
|
|
1388
1468
|
source: "chat"
|
|
1389
1469
|
});
|
|
@@ -1396,7 +1476,12 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1396
1476
|
case "generating":
|
|
1397
1477
|
case "screenshotting":
|
|
1398
1478
|
case "ready":
|
|
1399
|
-
this.broadcast({
|
|
1479
|
+
this.broadcast({
|
|
1480
|
+
type: "stash:status",
|
|
1481
|
+
stashId: event.stashId,
|
|
1482
|
+
status: event.type === "ready" ? "ready" : event.type,
|
|
1483
|
+
..."number" in event ? { number: event.number } : {}
|
|
1484
|
+
});
|
|
1400
1485
|
if (event.type === "ready" && "screenshotPath" in event && event.screenshotPath) {
|
|
1401
1486
|
this.broadcast({ type: "stash:screenshot", stashId: event.stashId, url: event.screenshotPath });
|
|
1402
1487
|
}
|
|
@@ -1404,9 +1489,11 @@ ${sourceCode.substring(0, 3000)}
|
|
|
1404
1489
|
case "error":
|
|
1405
1490
|
this.broadcast({ type: "stash:error", stashId: event.stashId, error: event.error });
|
|
1406
1491
|
break;
|
|
1407
|
-
case "ai_stream":
|
|
1408
|
-
|
|
1492
|
+
case "ai_stream": {
|
|
1493
|
+
const streamType = event.streamType === "tool_use" ? "tool_start" : event.streamType;
|
|
1494
|
+
this.broadcast({ type: "ai_stream", content: event.content, streamType });
|
|
1409
1495
|
break;
|
|
1496
|
+
}
|
|
1410
1497
|
}
|
|
1411
1498
|
}
|
|
1412
1499
|
async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT, referenceStashIds) {
|
|
@@ -1500,28 +1587,17 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
|
|
|
1500
1587
|
case "select_component":
|
|
1501
1588
|
stashService.setSelectedComponent(event.component);
|
|
1502
1589
|
break;
|
|
1503
|
-
case "
|
|
1590
|
+
case "message":
|
|
1504
1591
|
persistence.saveChatMessage(event.projectId, {
|
|
1505
1592
|
id: crypto.randomUUID(),
|
|
1506
1593
|
role: "user",
|
|
1507
1594
|
content: event.message,
|
|
1508
1595
|
type: "text",
|
|
1509
|
-
createdAt: new Date().toISOString()
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
break;
|
|
1513
|
-
case "generate":
|
|
1514
|
-
persistence.saveChatMessage(event.projectId, {
|
|
1515
|
-
id: crypto.randomUUID(),
|
|
1516
|
-
role: "user",
|
|
1517
|
-
content: event.prompt,
|
|
1518
|
-
type: "text",
|
|
1519
|
-
createdAt: new Date().toISOString()
|
|
1596
|
+
createdAt: new Date().toISOString(),
|
|
1597
|
+
referenceStashIds: event.referenceStashIds,
|
|
1598
|
+
componentContext: event.componentContext
|
|
1520
1599
|
});
|
|
1521
|
-
await stashService.
|
|
1522
|
-
break;
|
|
1523
|
-
case "vary":
|
|
1524
|
-
await stashService.vary(event.sourceStashId, event.prompt);
|
|
1600
|
+
await stashService.message(event.projectId, event.message, event.referenceStashIds, event.componentContext);
|
|
1525
1601
|
break;
|
|
1526
1602
|
case "interact":
|
|
1527
1603
|
await stashService.switchPreview(event.stashId, event.sortedStashIds);
|
|
@@ -1537,9 +1613,11 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
|
|
|
1537
1613
|
break;
|
|
1538
1614
|
}
|
|
1539
1615
|
} catch (err) {
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1616
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1617
|
+
logger.error("ws", `handler failed for ${event.type}`, { error: errorMsg });
|
|
1618
|
+
if ("stashId" in event && event.stashId) {
|
|
1619
|
+
broadcast({ type: "stash:error", stashId: event.stashId, error: errorMsg });
|
|
1620
|
+
}
|
|
1543
1621
|
}
|
|
1544
1622
|
},
|
|
1545
1623
|
close(ws) {
|