stashes 0.1.20 → 0.1.22

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
@@ -622,15 +622,20 @@ class PersistenceService {
622
622
  var {spawn } = globalThis.Bun;
623
623
  var CLAUDE_BIN = "/opt/homebrew/bin/claude";
624
624
  var processes = new Map;
625
- function startAiProcess(id, prompt, cwd) {
625
+ function startAiProcess(id, prompt, cwd, resumeSessionId) {
626
626
  killAiProcess(id);
627
627
  logger.info("claude", `spawning process: ${id}`, {
628
628
  cwd,
629
629
  promptLength: prompt.length,
630
- promptPreview: prompt.substring(0, 100)
630
+ promptPreview: prompt.substring(0, 100),
631
+ resumeSessionId
631
632
  });
633
+ const cmd = [CLAUDE_BIN, "-p", prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
634
+ if (resumeSessionId) {
635
+ cmd.push("--resume", resumeSessionId);
636
+ }
632
637
  const proc = spawn({
633
- cmd: [CLAUDE_BIN, "-p", prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"],
638
+ cmd,
634
639
  stdin: "ignore",
635
640
  stdout: "pipe",
636
641
  stderr: "pipe",
@@ -684,6 +689,9 @@ async function* parseClaudeStream(proc) {
684
689
  } catch {
685
690
  continue;
686
691
  }
692
+ if (parsed.type === "system" && parsed.subtype === "init" && parsed.session_id) {
693
+ yield { type: "session_id", content: "", sessionId: parsed.session_id };
694
+ }
687
695
  if (parsed.type === "assistant" && parsed.message) {
688
696
  const message = parsed.message;
689
697
  for (const block of message.content || []) {
@@ -1323,6 +1331,9 @@ class StashService {
1323
1331
  broadcast;
1324
1332
  previewPool;
1325
1333
  selectedComponent = null;
1334
+ messageQueue = [];
1335
+ isProcessingMessage = false;
1336
+ chatSessions = new Map;
1326
1337
  constructor(projectPath, worktreeManager, persistence, broadcast) {
1327
1338
  this.projectPath = projectPath;
1328
1339
  this.worktreeManager = worktreeManager;
@@ -1375,6 +1386,20 @@ class StashService {
1375
1386
  }
1376
1387
  }
1377
1388
  async message(projectId, chatId, message, referenceStashIds, componentContext) {
1389
+ this.messageQueue.push({ projectId, chatId, message, referenceStashIds, componentContext });
1390
+ if (!this.isProcessingMessage) {
1391
+ await this.processMessageQueue();
1392
+ }
1393
+ }
1394
+ async processMessageQueue() {
1395
+ this.isProcessingMessage = true;
1396
+ while (this.messageQueue.length > 0) {
1397
+ const msg = this.messageQueue.shift();
1398
+ await this.processMessage(msg.projectId, msg.chatId, msg.message, msg.referenceStashIds, msg.componentContext);
1399
+ }
1400
+ this.isProcessingMessage = false;
1401
+ }
1402
+ async processMessage(projectId, chatId, message, referenceStashIds, componentContext) {
1378
1403
  const component = componentContext ? { name: componentContext.name, filePath: this.selectedComponent?.filePath || "" } : this.selectedComponent;
1379
1404
  let sourceCode = "";
1380
1405
  const filePath = component?.filePath || "";
@@ -1394,34 +1419,66 @@ ${refs.join(`
1394
1419
  `)}`;
1395
1420
  }
1396
1421
  }
1397
- const chatPrompt = [
1398
- "You are helping the user explore UI design variations for their project.",
1399
- "You have access to stashes MCP tools to generate, list, show, browse, vary, and apply stashes.",
1400
- "If the user asks you to generate, create, or make variations, use the stashes_generate tool.",
1401
- "If the user asks to vary an existing stash, use the stashes_vary tool.",
1402
- "If the user asks about what a stash changed, its diff, or its contents, use stashes_show to inspect it.",
1403
- 'IMPORTANT: NEVER call stashes_apply unless the user explicitly asks to "apply" or "merge" a stash.',
1404
- 'IMPORTANT: NEVER call stashes_remove unless the user explicitly asks to "delete" or "remove" a stash.',
1405
- "Otherwise, respond conversationally about their project and stashes.",
1406
- "",
1407
- component ? `Component: ${component.name}` : "",
1408
- filePath !== "auto-detect" ? `File: ${filePath}` : "",
1409
- sourceCode ? `
1422
+ const existingSessionId = this.chatSessions.get(chatId);
1423
+ let chatPrompt;
1424
+ if (existingSessionId) {
1425
+ const parts = [message];
1426
+ if (stashContext)
1427
+ parts.push(stashContext);
1428
+ if (component && !existingSessionId)
1429
+ parts.push(`Component: ${component.name}`);
1430
+ chatPrompt = parts.join(`
1431
+ `);
1432
+ } else {
1433
+ chatPrompt = [
1434
+ "You are helping the user explore UI design variations for their project.",
1435
+ "You have access to stashes MCP tools to generate, list, show, browse, vary, and apply stashes.",
1436
+ "If the user asks you to generate, create, or make variations, use the stashes_generate tool.",
1437
+ "If the user asks to vary an existing stash, use the stashes_vary tool.",
1438
+ "If the user asks about what a stash changed, its diff, or its contents, use stashes_show to inspect it.",
1439
+ 'IMPORTANT: NEVER call stashes_apply unless the user explicitly asks to "apply" or "merge" a stash.',
1440
+ 'IMPORTANT: NEVER call stashes_remove unless the user explicitly asks to "delete" or "remove" a stash.',
1441
+ "Otherwise, respond conversationally about their project and stashes.",
1442
+ "",
1443
+ component ? `Component: ${component.name}` : "",
1444
+ filePath !== "auto-detect" ? `File: ${filePath}` : "",
1445
+ sourceCode ? `
1410
1446
  Source:
1411
1447
  \`\`\`
1412
1448
  ${sourceCode.substring(0, 3000)}
1413
1449
  \`\`\`` : "",
1414
- stashContext,
1415
- "",
1416
- `User: ${message}`
1417
- ].filter(Boolean).join(`
1450
+ stashContext,
1451
+ "",
1452
+ `User: ${message}`
1453
+ ].filter(Boolean).join(`
1418
1454
  `);
1419
- const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath);
1420
- let fullResponse = "";
1455
+ }
1456
+ const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath, existingSessionId);
1457
+ let thinkingBuf = "";
1458
+ let textBuf = "";
1459
+ const pendingMessages = [];
1460
+ const now = new Date().toISOString();
1461
+ function flushThinking() {
1462
+ if (!thinkingBuf)
1463
+ return;
1464
+ pendingMessages.push({ id: crypto.randomUUID(), role: "assistant", content: thinkingBuf, type: "thinking", createdAt: now });
1465
+ thinkingBuf = "";
1466
+ }
1467
+ function flushText() {
1468
+ if (!textBuf)
1469
+ return;
1470
+ pendingMessages.push({ id: crypto.randomUUID(), role: "assistant", content: textBuf, type: "text", createdAt: now });
1471
+ textBuf = "";
1472
+ }
1421
1473
  try {
1422
1474
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
1475
+ if (chunk.type === "session_id" && chunk.sessionId) {
1476
+ this.chatSessions.set(chatId, chunk.sessionId);
1477
+ continue;
1478
+ }
1423
1479
  if (chunk.type === "text") {
1424
- fullResponse += chunk.content;
1480
+ flushThinking();
1481
+ textBuf += chunk.content;
1425
1482
  this.broadcast({
1426
1483
  type: "ai_stream",
1427
1484
  content: chunk.content,
@@ -1429,6 +1486,7 @@ ${sourceCode.substring(0, 3000)}
1429
1486
  source: "chat"
1430
1487
  });
1431
1488
  } else if (chunk.type === "thinking") {
1489
+ thinkingBuf += chunk.content;
1432
1490
  this.broadcast({
1433
1491
  type: "ai_stream",
1434
1492
  content: chunk.content,
@@ -1436,36 +1494,66 @@ ${sourceCode.substring(0, 3000)}
1436
1494
  source: "chat"
1437
1495
  });
1438
1496
  } else if (chunk.type === "tool_use") {
1497
+ flushThinking();
1498
+ flushText();
1499
+ let toolName = "unknown";
1500
+ let toolParams = {};
1501
+ try {
1502
+ const parsed = JSON.parse(chunk.content);
1503
+ toolName = parsed.tool ?? "unknown";
1504
+ toolParams = parsed.input ?? {};
1505
+ } catch {}
1506
+ pendingMessages.push({
1507
+ id: crypto.randomUUID(),
1508
+ role: "assistant",
1509
+ content: chunk.content,
1510
+ type: "tool_start",
1511
+ toolName,
1512
+ toolParams,
1513
+ toolStatus: "running",
1514
+ createdAt: now
1515
+ });
1439
1516
  this.broadcast({
1440
1517
  type: "ai_stream",
1441
1518
  content: chunk.content,
1442
1519
  streamType: "tool_start",
1443
1520
  source: "chat",
1444
- toolName: chunk.toolName ?? "unknown",
1445
- toolParams: chunk.toolInput ?? {},
1521
+ toolName,
1522
+ toolParams,
1446
1523
  toolStatus: "running"
1447
1524
  });
1448
1525
  } else if (chunk.type === "tool_result") {
1526
+ let toolResult = chunk.content;
1527
+ let isError = false;
1528
+ try {
1529
+ const parsed = JSON.parse(chunk.content);
1530
+ toolResult = parsed.result ?? chunk.content;
1531
+ isError = !!parsed.is_error;
1532
+ } catch {}
1533
+ pendingMessages.push({
1534
+ id: crypto.randomUUID(),
1535
+ role: "assistant",
1536
+ content: chunk.content,
1537
+ type: "tool_end",
1538
+ toolStatus: isError ? "error" : "completed",
1539
+ toolResult: toolResult.substring(0, 300),
1540
+ createdAt: now
1541
+ });
1449
1542
  this.broadcast({
1450
1543
  type: "ai_stream",
1451
1544
  content: chunk.content,
1452
1545
  streamType: "tool_end",
1453
1546
  source: "chat",
1454
- toolName: chunk.toolName ?? "unknown",
1455
- toolStatus: chunk.isError ? "error" : "completed",
1456
- toolResult: chunk.content.substring(0, 300)
1547
+ toolStatus: isError ? "error" : "completed",
1548
+ toolResult: toolResult.substring(0, 300)
1457
1549
  });
1458
1550
  }
1459
1551
  }
1460
1552
  await aiProcess.process.exited;
1461
- if (fullResponse) {
1462
- this.persistence.saveChatMessage(projectId, chatId, {
1463
- id: crypto.randomUUID(),
1464
- role: "assistant",
1465
- content: fullResponse,
1466
- type: "text",
1467
- createdAt: new Date().toISOString()
1468
- });
1553
+ flushThinking();
1554
+ flushText();
1555
+ for (const msg of pendingMessages) {
1556
+ this.persistence.saveChatMessage(projectId, chatId, msg);
1469
1557
  }
1470
1558
  } catch (err) {
1471
1559
  this.broadcast({
package/dist/mcp.js CHANGED
@@ -504,15 +504,20 @@ class PersistenceService {
504
504
  var {spawn } = globalThis.Bun;
505
505
  var CLAUDE_BIN = "/opt/homebrew/bin/claude";
506
506
  var processes = new Map;
507
- function startAiProcess(id, prompt, cwd) {
507
+ function startAiProcess(id, prompt, cwd, resumeSessionId) {
508
508
  killAiProcess(id);
509
509
  logger.info("claude", `spawning process: ${id}`, {
510
510
  cwd,
511
511
  promptLength: prompt.length,
512
- promptPreview: prompt.substring(0, 100)
512
+ promptPreview: prompt.substring(0, 100),
513
+ resumeSessionId
513
514
  });
515
+ const cmd = [CLAUDE_BIN, "-p", prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
516
+ if (resumeSessionId) {
517
+ cmd.push("--resume", resumeSessionId);
518
+ }
514
519
  const proc = spawn({
515
- cmd: [CLAUDE_BIN, "-p", prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"],
520
+ cmd,
516
521
  stdin: "ignore",
517
522
  stdout: "pipe",
518
523
  stderr: "pipe",
@@ -566,6 +571,9 @@ async function* parseClaudeStream(proc) {
566
571
  } catch {
567
572
  continue;
568
573
  }
574
+ if (parsed.type === "system" && parsed.subtype === "init" && parsed.session_id) {
575
+ yield { type: "session_id", content: "", sessionId: parsed.session_id };
576
+ }
569
577
  if (parsed.type === "assistant" && parsed.message) {
570
578
  const message = parsed.message;
571
579
  for (const block of message.content || []) {
@@ -1519,6 +1527,9 @@ class StashService {
1519
1527
  broadcast;
1520
1528
  previewPool;
1521
1529
  selectedComponent = null;
1530
+ messageQueue = [];
1531
+ isProcessingMessage = false;
1532
+ chatSessions = new Map;
1522
1533
  constructor(projectPath, worktreeManager, persistence, broadcast) {
1523
1534
  this.projectPath = projectPath;
1524
1535
  this.worktreeManager = worktreeManager;
@@ -1571,6 +1582,20 @@ class StashService {
1571
1582
  }
1572
1583
  }
1573
1584
  async message(projectId, chatId, message, referenceStashIds, componentContext) {
1585
+ this.messageQueue.push({ projectId, chatId, message, referenceStashIds, componentContext });
1586
+ if (!this.isProcessingMessage) {
1587
+ await this.processMessageQueue();
1588
+ }
1589
+ }
1590
+ async processMessageQueue() {
1591
+ this.isProcessingMessage = true;
1592
+ while (this.messageQueue.length > 0) {
1593
+ const msg = this.messageQueue.shift();
1594
+ await this.processMessage(msg.projectId, msg.chatId, msg.message, msg.referenceStashIds, msg.componentContext);
1595
+ }
1596
+ this.isProcessingMessage = false;
1597
+ }
1598
+ async processMessage(projectId, chatId, message, referenceStashIds, componentContext) {
1574
1599
  const component = componentContext ? { name: componentContext.name, filePath: this.selectedComponent?.filePath || "" } : this.selectedComponent;
1575
1600
  let sourceCode = "";
1576
1601
  const filePath = component?.filePath || "";
@@ -1590,34 +1615,66 @@ ${refs.join(`
1590
1615
  `)}`;
1591
1616
  }
1592
1617
  }
1593
- const chatPrompt = [
1594
- "You are helping the user explore UI design variations for their project.",
1595
- "You have access to stashes MCP tools to generate, list, show, browse, vary, and apply stashes.",
1596
- "If the user asks you to generate, create, or make variations, use the stashes_generate tool.",
1597
- "If the user asks to vary an existing stash, use the stashes_vary tool.",
1598
- "If the user asks about what a stash changed, its diff, or its contents, use stashes_show to inspect it.",
1599
- 'IMPORTANT: NEVER call stashes_apply unless the user explicitly asks to "apply" or "merge" a stash.',
1600
- 'IMPORTANT: NEVER call stashes_remove unless the user explicitly asks to "delete" or "remove" a stash.',
1601
- "Otherwise, respond conversationally about their project and stashes.",
1602
- "",
1603
- component ? `Component: ${component.name}` : "",
1604
- filePath !== "auto-detect" ? `File: ${filePath}` : "",
1605
- sourceCode ? `
1618
+ const existingSessionId = this.chatSessions.get(chatId);
1619
+ let chatPrompt;
1620
+ if (existingSessionId) {
1621
+ const parts = [message];
1622
+ if (stashContext)
1623
+ parts.push(stashContext);
1624
+ if (component && !existingSessionId)
1625
+ parts.push(`Component: ${component.name}`);
1626
+ chatPrompt = parts.join(`
1627
+ `);
1628
+ } else {
1629
+ chatPrompt = [
1630
+ "You are helping the user explore UI design variations for their project.",
1631
+ "You have access to stashes MCP tools to generate, list, show, browse, vary, and apply stashes.",
1632
+ "If the user asks you to generate, create, or make variations, use the stashes_generate tool.",
1633
+ "If the user asks to vary an existing stash, use the stashes_vary tool.",
1634
+ "If the user asks about what a stash changed, its diff, or its contents, use stashes_show to inspect it.",
1635
+ 'IMPORTANT: NEVER call stashes_apply unless the user explicitly asks to "apply" or "merge" a stash.',
1636
+ 'IMPORTANT: NEVER call stashes_remove unless the user explicitly asks to "delete" or "remove" a stash.',
1637
+ "Otherwise, respond conversationally about their project and stashes.",
1638
+ "",
1639
+ component ? `Component: ${component.name}` : "",
1640
+ filePath !== "auto-detect" ? `File: ${filePath}` : "",
1641
+ sourceCode ? `
1606
1642
  Source:
1607
1643
  \`\`\`
1608
1644
  ${sourceCode.substring(0, 3000)}
1609
1645
  \`\`\`` : "",
1610
- stashContext,
1611
- "",
1612
- `User: ${message}`
1613
- ].filter(Boolean).join(`
1646
+ stashContext,
1647
+ "",
1648
+ `User: ${message}`
1649
+ ].filter(Boolean).join(`
1614
1650
  `);
1615
- const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath);
1616
- let fullResponse = "";
1651
+ }
1652
+ const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath, existingSessionId);
1653
+ let thinkingBuf = "";
1654
+ let textBuf = "";
1655
+ const pendingMessages = [];
1656
+ const now = new Date().toISOString();
1657
+ function flushThinking() {
1658
+ if (!thinkingBuf)
1659
+ return;
1660
+ pendingMessages.push({ id: crypto.randomUUID(), role: "assistant", content: thinkingBuf, type: "thinking", createdAt: now });
1661
+ thinkingBuf = "";
1662
+ }
1663
+ function flushText() {
1664
+ if (!textBuf)
1665
+ return;
1666
+ pendingMessages.push({ id: crypto.randomUUID(), role: "assistant", content: textBuf, type: "text", createdAt: now });
1667
+ textBuf = "";
1668
+ }
1617
1669
  try {
1618
1670
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
1671
+ if (chunk.type === "session_id" && chunk.sessionId) {
1672
+ this.chatSessions.set(chatId, chunk.sessionId);
1673
+ continue;
1674
+ }
1619
1675
  if (chunk.type === "text") {
1620
- fullResponse += chunk.content;
1676
+ flushThinking();
1677
+ textBuf += chunk.content;
1621
1678
  this.broadcast({
1622
1679
  type: "ai_stream",
1623
1680
  content: chunk.content,
@@ -1625,6 +1682,7 @@ ${sourceCode.substring(0, 3000)}
1625
1682
  source: "chat"
1626
1683
  });
1627
1684
  } else if (chunk.type === "thinking") {
1685
+ thinkingBuf += chunk.content;
1628
1686
  this.broadcast({
1629
1687
  type: "ai_stream",
1630
1688
  content: chunk.content,
@@ -1632,36 +1690,66 @@ ${sourceCode.substring(0, 3000)}
1632
1690
  source: "chat"
1633
1691
  });
1634
1692
  } else if (chunk.type === "tool_use") {
1693
+ flushThinking();
1694
+ flushText();
1695
+ let toolName = "unknown";
1696
+ let toolParams = {};
1697
+ try {
1698
+ const parsed = JSON.parse(chunk.content);
1699
+ toolName = parsed.tool ?? "unknown";
1700
+ toolParams = parsed.input ?? {};
1701
+ } catch {}
1702
+ pendingMessages.push({
1703
+ id: crypto.randomUUID(),
1704
+ role: "assistant",
1705
+ content: chunk.content,
1706
+ type: "tool_start",
1707
+ toolName,
1708
+ toolParams,
1709
+ toolStatus: "running",
1710
+ createdAt: now
1711
+ });
1635
1712
  this.broadcast({
1636
1713
  type: "ai_stream",
1637
1714
  content: chunk.content,
1638
1715
  streamType: "tool_start",
1639
1716
  source: "chat",
1640
- toolName: chunk.toolName ?? "unknown",
1641
- toolParams: chunk.toolInput ?? {},
1717
+ toolName,
1718
+ toolParams,
1642
1719
  toolStatus: "running"
1643
1720
  });
1644
1721
  } else if (chunk.type === "tool_result") {
1722
+ let toolResult = chunk.content;
1723
+ let isError = false;
1724
+ try {
1725
+ const parsed = JSON.parse(chunk.content);
1726
+ toolResult = parsed.result ?? chunk.content;
1727
+ isError = !!parsed.is_error;
1728
+ } catch {}
1729
+ pendingMessages.push({
1730
+ id: crypto.randomUUID(),
1731
+ role: "assistant",
1732
+ content: chunk.content,
1733
+ type: "tool_end",
1734
+ toolStatus: isError ? "error" : "completed",
1735
+ toolResult: toolResult.substring(0, 300),
1736
+ createdAt: now
1737
+ });
1645
1738
  this.broadcast({
1646
1739
  type: "ai_stream",
1647
1740
  content: chunk.content,
1648
1741
  streamType: "tool_end",
1649
1742
  source: "chat",
1650
- toolName: chunk.toolName ?? "unknown",
1651
- toolStatus: chunk.isError ? "error" : "completed",
1652
- toolResult: chunk.content.substring(0, 300)
1743
+ toolStatus: isError ? "error" : "completed",
1744
+ toolResult: toolResult.substring(0, 300)
1653
1745
  });
1654
1746
  }
1655
1747
  }
1656
1748
  await aiProcess.process.exited;
1657
- if (fullResponse) {
1658
- this.persistence.saveChatMessage(projectId, chatId, {
1659
- id: crypto.randomUUID(),
1660
- role: "assistant",
1661
- content: fullResponse,
1662
- type: "text",
1663
- createdAt: new Date().toISOString()
1664
- });
1749
+ flushThinking();
1750
+ flushText();
1751
+ for (const msg of pendingMessages) {
1752
+ this.persistence.saveChatMessage(projectId, chatId, msg);
1665
1753
  }
1666
1754
  } catch (err) {
1667
1755
  this.broadcast({