stashes 0.1.11 → 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 CHANGED
@@ -733,11 +733,15 @@ async function generate(opts) {
733
733
  }
734
734
  }
735
735
  const completedStashes = [];
736
- const stashPromises = selectedDirectives.map(async (directive) => {
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) => {
737
739
  const stashId = `stash_${crypto.randomUUID().substring(0, 8)}`;
740
+ const stashNumber = maxNumber + idx + 1;
738
741
  const worktree = await worktreeManager.createForGeneration(stashId);
739
742
  const stash = {
740
743
  id: stashId,
744
+ number: stashNumber,
741
745
  projectId,
742
746
  prompt,
743
747
  componentPath: component?.filePath,
@@ -751,7 +755,7 @@ async function generate(opts) {
751
755
  createdAt: new Date().toISOString()
752
756
  };
753
757
  persistence.saveStash(stash);
754
- emit(onProgress, { type: "generating", stashId });
758
+ emit(onProgress, { type: "generating", stashId, number: stashNumber });
755
759
  let stashPrompt;
756
760
  if (component?.filePath) {
757
761
  stashPrompt = buildStashPrompt({ name: component.exportName || component.filePath, filePath: component.filePath, domSelector: "" }, sourceCode, prompt, directive);
@@ -856,9 +860,12 @@ async function vary(opts) {
856
860
  if (!sourceStash)
857
861
  throw new Error(`Source stash ${sourceStashId} not found`);
858
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;
859
865
  const worktree = await worktreeManager.createForVary(stashId, sourceStash.branch);
860
866
  const stash = {
861
867
  id: stashId,
868
+ number: stashNumber,
862
869
  projectId,
863
870
  prompt,
864
871
  componentPath: sourceStash.componentPath,
@@ -872,7 +879,7 @@ async function vary(opts) {
872
879
  createdAt: new Date().toISOString()
873
880
  };
874
881
  persistence.saveStash(stash);
875
- emit2(onProgress, { type: "generating", stashId });
882
+ emit2(onProgress, { type: "generating", stashId, number: stashNumber });
876
883
  const varyPrompt = `The user wants to vary the current UI. Apply this change: ${prompt}`;
877
884
  const aiProcess = startAiProcess(stashId, varyPrompt, worktree.path);
878
885
  try {
@@ -1217,8 +1224,8 @@ class StashService {
1217
1224
  });
1218
1225
  }
1219
1226
  }
1220
- async chat(projectId, message, referenceStashIds) {
1221
- const component = this.selectedComponent;
1227
+ async message(projectId, message, referenceStashIds, componentContext) {
1228
+ const component = componentContext ? { name: componentContext.name, filePath: this.selectedComponent?.filePath || "" } : this.selectedComponent;
1222
1229
  let sourceCode = "";
1223
1230
  const filePath = component?.filePath || "";
1224
1231
  if (filePath && filePath !== "auto-detect") {
@@ -1238,8 +1245,11 @@ ${refs.join(`
1238
1245
  }
1239
1246
  }
1240
1247
  const chatPrompt = [
1241
- "The user is asking about their UI project. Answer concisely.",
1242
- "Do NOT modify any files.",
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.",
1243
1253
  "",
1244
1254
  component ? `Component: ${component.name}` : "",
1245
1255
  filePath !== "auto-detect" ? `File: ${filePath}` : "",
@@ -1250,7 +1260,7 @@ ${sourceCode.substring(0, 3000)}
1250
1260
  \`\`\`` : "",
1251
1261
  stashContext,
1252
1262
  "",
1253
- `User question: ${message}`
1263
+ `User: ${message}`
1254
1264
  ].filter(Boolean).join(`
1255
1265
  `);
1256
1266
  const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath);
@@ -1259,7 +1269,55 @@ ${sourceCode.substring(0, 3000)}
1259
1269
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
1260
1270
  if (chunk.type === "text") {
1261
1271
  fullResponse += chunk.content;
1262
- this.broadcast({ type: "ai_stream", content: chunk.content, streamType: "text", source: "chat" });
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
+ });
1263
1321
  }
1264
1322
  }
1265
1323
  await aiProcess.process.exited;
@@ -1275,7 +1333,7 @@ ${sourceCode.substring(0, 3000)}
1275
1333
  } catch (err) {
1276
1334
  this.broadcast({
1277
1335
  type: "ai_stream",
1278
- content: `Chat error: ${err instanceof Error ? err.message : String(err)}`,
1336
+ content: `Error: ${err instanceof Error ? err.message : String(err)}`,
1279
1337
  streamType: "text",
1280
1338
  source: "chat"
1281
1339
  });
@@ -1288,7 +1346,12 @@ ${sourceCode.substring(0, 3000)}
1288
1346
  case "generating":
1289
1347
  case "screenshotting":
1290
1348
  case "ready":
1291
- this.broadcast({ type: "stash:status", stashId: event.stashId, status: event.type === "ready" ? "ready" : event.type });
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
+ });
1292
1355
  if (event.type === "ready" && "screenshotPath" in event && event.screenshotPath) {
1293
1356
  this.broadcast({ type: "stash:screenshot", stashId: event.stashId, url: event.screenshotPath });
1294
1357
  }
@@ -1296,9 +1359,11 @@ ${sourceCode.substring(0, 3000)}
1296
1359
  case "error":
1297
1360
  this.broadcast({ type: "stash:error", stashId: event.stashId, error: event.error });
1298
1361
  break;
1299
- case "ai_stream":
1300
- this.broadcast({ type: "ai_stream", content: event.content, streamType: event.streamType });
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 });
1301
1365
  break;
1366
+ }
1302
1367
  }
1303
1368
  }
1304
1369
  async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT, referenceStashIds) {
@@ -1392,28 +1457,17 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
1392
1457
  case "select_component":
1393
1458
  stashService.setSelectedComponent(event.component);
1394
1459
  break;
1395
- case "chat":
1460
+ case "message":
1396
1461
  persistence.saveChatMessage(event.projectId, {
1397
1462
  id: crypto.randomUUID(),
1398
1463
  role: "user",
1399
1464
  content: event.message,
1400
1465
  type: "text",
1401
- createdAt: new Date().toISOString()
1402
- });
1403
- await stashService.chat(event.projectId, event.message, event.referenceStashIds);
1404
- break;
1405
- case "generate":
1406
- persistence.saveChatMessage(event.projectId, {
1407
- id: crypto.randomUUID(),
1408
- role: "user",
1409
- content: event.prompt,
1410
- type: "text",
1411
- createdAt: new Date().toISOString()
1466
+ createdAt: new Date().toISOString(),
1467
+ referenceStashIds: event.referenceStashIds,
1468
+ componentContext: event.componentContext
1412
1469
  });
1413
- await stashService.generate(event.projectId, event.prompt, event.stashCount, event.referenceStashIds);
1414
- break;
1415
- case "vary":
1416
- await stashService.vary(event.sourceStashId, event.prompt);
1470
+ await stashService.message(event.projectId, event.message, event.referenceStashIds, event.componentContext);
1417
1471
  break;
1418
1472
  case "interact":
1419
1473
  await stashService.switchPreview(event.stashId, event.sortedStashIds);
package/dist/mcp.js CHANGED
@@ -669,11 +669,15 @@ async function generate(opts) {
669
669
  }
670
670
  }
671
671
  const completedStashes = [];
672
- const stashPromises = selectedDirectives.map(async (directive) => {
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) => {
673
675
  const stashId = `stash_${crypto.randomUUID().substring(0, 8)}`;
676
+ const stashNumber = maxNumber + idx + 1;
674
677
  const worktree = await worktreeManager.createForGeneration(stashId);
675
678
  const stash = {
676
679
  id: stashId,
680
+ number: stashNumber,
677
681
  projectId,
678
682
  prompt,
679
683
  componentPath: component?.filePath,
@@ -687,7 +691,7 @@ async function generate(opts) {
687
691
  createdAt: new Date().toISOString()
688
692
  };
689
693
  persistence.saveStash(stash);
690
- emit(onProgress, { type: "generating", stashId });
694
+ emit(onProgress, { type: "generating", stashId, number: stashNumber });
691
695
  let stashPrompt;
692
696
  if (component?.filePath) {
693
697
  stashPrompt = buildStashPrompt({ name: component.exportName || component.filePath, filePath: component.filePath, domSelector: "" }, sourceCode, prompt, directive);
@@ -792,9 +796,12 @@ async function vary(opts) {
792
796
  if (!sourceStash)
793
797
  throw new Error(`Source stash ${sourceStashId} not found`);
794
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;
795
801
  const worktree = await worktreeManager.createForVary(stashId, sourceStash.branch);
796
802
  const stash = {
797
803
  id: stashId,
804
+ number: stashNumber,
798
805
  projectId,
799
806
  prompt,
800
807
  componentPath: sourceStash.componentPath,
@@ -808,7 +815,7 @@ async function vary(opts) {
808
815
  createdAt: new Date().toISOString()
809
816
  };
810
817
  persistence.saveStash(stash);
811
- emit2(onProgress, { type: "generating", stashId });
818
+ emit2(onProgress, { type: "generating", stashId, number: stashNumber });
812
819
  const varyPrompt = `The user wants to vary the current UI. Apply this change: ${prompt}`;
813
820
  const aiProcess = startAiProcess(stashId, varyPrompt, worktree.path);
814
821
  try {
@@ -1347,8 +1354,8 @@ class StashService {
1347
1354
  });
1348
1355
  }
1349
1356
  }
1350
- async chat(projectId, message, referenceStashIds) {
1351
- const component = this.selectedComponent;
1357
+ async message(projectId, message, referenceStashIds, componentContext) {
1358
+ const component = componentContext ? { name: componentContext.name, filePath: this.selectedComponent?.filePath || "" } : this.selectedComponent;
1352
1359
  let sourceCode = "";
1353
1360
  const filePath = component?.filePath || "";
1354
1361
  if (filePath && filePath !== "auto-detect") {
@@ -1368,8 +1375,11 @@ ${refs.join(`
1368
1375
  }
1369
1376
  }
1370
1377
  const chatPrompt = [
1371
- "The user is asking about their UI project. Answer concisely.",
1372
- "Do NOT modify any files.",
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.",
1373
1383
  "",
1374
1384
  component ? `Component: ${component.name}` : "",
1375
1385
  filePath !== "auto-detect" ? `File: ${filePath}` : "",
@@ -1380,7 +1390,7 @@ ${sourceCode.substring(0, 3000)}
1380
1390
  \`\`\`` : "",
1381
1391
  stashContext,
1382
1392
  "",
1383
- `User question: ${message}`
1393
+ `User: ${message}`
1384
1394
  ].filter(Boolean).join(`
1385
1395
  `);
1386
1396
  const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath);
@@ -1389,7 +1399,55 @@ ${sourceCode.substring(0, 3000)}
1389
1399
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
1390
1400
  if (chunk.type === "text") {
1391
1401
  fullResponse += chunk.content;
1392
- this.broadcast({ type: "ai_stream", content: chunk.content, streamType: "text", source: "chat" });
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
+ });
1393
1451
  }
1394
1452
  }
1395
1453
  await aiProcess.process.exited;
@@ -1405,7 +1463,7 @@ ${sourceCode.substring(0, 3000)}
1405
1463
  } catch (err) {
1406
1464
  this.broadcast({
1407
1465
  type: "ai_stream",
1408
- content: `Chat error: ${err instanceof Error ? err.message : String(err)}`,
1466
+ content: `Error: ${err instanceof Error ? err.message : String(err)}`,
1409
1467
  streamType: "text",
1410
1468
  source: "chat"
1411
1469
  });
@@ -1418,7 +1476,12 @@ ${sourceCode.substring(0, 3000)}
1418
1476
  case "generating":
1419
1477
  case "screenshotting":
1420
1478
  case "ready":
1421
- this.broadcast({ type: "stash:status", stashId: event.stashId, status: event.type === "ready" ? "ready" : event.type });
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
+ });
1422
1485
  if (event.type === "ready" && "screenshotPath" in event && event.screenshotPath) {
1423
1486
  this.broadcast({ type: "stash:screenshot", stashId: event.stashId, url: event.screenshotPath });
1424
1487
  }
@@ -1426,9 +1489,11 @@ ${sourceCode.substring(0, 3000)}
1426
1489
  case "error":
1427
1490
  this.broadcast({ type: "stash:error", stashId: event.stashId, error: event.error });
1428
1491
  break;
1429
- case "ai_stream":
1430
- this.broadcast({ type: "ai_stream", content: event.content, streamType: event.streamType });
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 });
1431
1495
  break;
1496
+ }
1432
1497
  }
1433
1498
  }
1434
1499
  async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT, referenceStashIds) {
@@ -1522,28 +1587,17 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
1522
1587
  case "select_component":
1523
1588
  stashService.setSelectedComponent(event.component);
1524
1589
  break;
1525
- case "chat":
1590
+ case "message":
1526
1591
  persistence.saveChatMessage(event.projectId, {
1527
1592
  id: crypto.randomUUID(),
1528
1593
  role: "user",
1529
1594
  content: event.message,
1530
1595
  type: "text",
1531
- createdAt: new Date().toISOString()
1532
- });
1533
- await stashService.chat(event.projectId, event.message, event.referenceStashIds);
1534
- break;
1535
- case "generate":
1536
- persistence.saveChatMessage(event.projectId, {
1537
- id: crypto.randomUUID(),
1538
- role: "user",
1539
- content: event.prompt,
1540
- type: "text",
1541
- createdAt: new Date().toISOString()
1596
+ createdAt: new Date().toISOString(),
1597
+ referenceStashIds: event.referenceStashIds,
1598
+ componentContext: event.componentContext
1542
1599
  });
1543
- await stashService.generate(event.projectId, event.prompt, event.stashCount, event.referenceStashIds);
1544
- break;
1545
- case "vary":
1546
- await stashService.vary(event.sourceStashId, event.prompt);
1600
+ await stashService.message(event.projectId, event.message, event.referenceStashIds, event.componentContext);
1547
1601
  break;
1548
1602
  case "interact":
1549
1603
  await stashService.switchPreview(event.stashId, event.sortedStashIds);