stashes 0.1.19 → 0.1.21

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/mcp.js CHANGED
@@ -538,6 +538,7 @@ function killAiProcess(id) {
538
538
  }
539
539
  return false;
540
540
  }
541
+ var toolNameMap = new Map;
541
542
  async function* parseClaudeStream(proc) {
542
543
  const stdout = proc.stdout;
543
544
  if (!stdout || typeof stdout === "number") {
@@ -574,15 +575,58 @@ async function* parseClaudeStream(proc) {
574
575
  } else if (block.type === "thinking" && block.thinking) {
575
576
  yield { type: "thinking", content: block.thinking };
576
577
  } else if (block.type === "tool_use") {
577
- logger.debug("claude", `tool_use: ${block.name}`);
578
- yield { type: "tool_use", content: JSON.stringify({ tool: block.name, input: block.input }) };
578
+ const toolId = block.id;
579
+ const toolName = block.name;
580
+ toolNameMap.set(toolId, toolName);
581
+ logger.debug("claude", `tool_use: ${toolName}`);
582
+ yield {
583
+ type: "tool_use",
584
+ content: JSON.stringify({ tool: toolName, input: block.input }),
585
+ toolName,
586
+ toolInput: block.input,
587
+ toolUseId: toolId
588
+ };
579
589
  }
580
590
  }
581
591
  }
592
+ if (parsed.type === "user" && parsed.message) {
593
+ const message = parsed.message;
594
+ for (const block of message.content || []) {
595
+ if (block.type === "tool_result") {
596
+ const toolUseId = block.tool_use_id;
597
+ const isError = block.is_error || false;
598
+ const toolName = toolNameMap.get(toolUseId) || "unknown";
599
+ let resultContent;
600
+ if (typeof block.content === "string") {
601
+ resultContent = block.content;
602
+ } else if (Array.isArray(block.content)) {
603
+ resultContent = block.content.filter((c) => c.type === "text").map((c) => c.text).join(`
604
+ `);
605
+ } else {
606
+ resultContent = "";
607
+ }
608
+ yield {
609
+ type: "tool_result",
610
+ content: resultContent.substring(0, 500),
611
+ toolName,
612
+ toolUseId,
613
+ isError
614
+ };
615
+ }
616
+ }
617
+ }
618
+ if (parsed.type === "result") {
619
+ yield {
620
+ type: "result",
621
+ content: "",
622
+ resultSubtype: parsed.subtype
623
+ };
624
+ }
582
625
  }
583
626
  }
584
627
  } finally {
585
628
  logger.debug("claude", `stream ended`, { totalChunks: chunkCount });
629
+ toolNameMap.clear();
586
630
  reader.releaseLock();
587
631
  }
588
632
  }
@@ -964,6 +1008,34 @@ async function remove(projectPath, stashId) {
964
1008
  } catch {}
965
1009
  logger.info("manage", `removed stash: ${stashId}`);
966
1010
  }
1011
+ async function show(projectPath, stashId) {
1012
+ const persistence = new PersistenceService(projectPath);
1013
+ let found;
1014
+ for (const project of persistence.listProjects()) {
1015
+ found = persistence.getStash(project.id, stashId);
1016
+ if (found)
1017
+ break;
1018
+ }
1019
+ if (!found)
1020
+ return null;
1021
+ const git = simpleGit4(projectPath);
1022
+ const branch = found.branch || `stashes/${stashId}`;
1023
+ let diff = "";
1024
+ let files = [];
1025
+ try {
1026
+ const base = await git.raw(["merge-base", "HEAD", branch]);
1027
+ diff = await git.raw(["diff", base.trim(), branch, "--stat"]);
1028
+ const fullDiff = await git.raw(["diff", base.trim(), branch]);
1029
+ diff = fullDiff.length > 8000 ? fullDiff.substring(0, 8000) + `
1030
+ ... (truncated)` : fullDiff;
1031
+ const fileList = await git.raw(["diff", "--name-only", base.trim(), branch]);
1032
+ files = fileList.trim().split(`
1033
+ `).filter(Boolean);
1034
+ } catch {
1035
+ diff = "(branch not found or no diff available)";
1036
+ }
1037
+ return { stash: found, diff, files };
1038
+ }
967
1039
  // ../mcp/src/tools/generate.ts
968
1040
  var generateParams = {
969
1041
  prompt: z.string().describe('What UI changes to generate (e.g. "make the hero section bolder")'),
@@ -1088,10 +1160,48 @@ async function handleVary(args, projectPath) {
1088
1160
  };
1089
1161
  }
1090
1162
 
1091
- // ../mcp/src/tools/remove.ts
1163
+ // ../mcp/src/tools/show.ts
1092
1164
  import { z as z4 } from "zod";
1165
+ var showParams = {
1166
+ stashId: z4.string().describe('The stash ID to inspect (e.g. "stash_a1b2c3d4")')
1167
+ };
1168
+ async function handleShow(args, projectPath) {
1169
+ const { stashId } = args;
1170
+ initLogFile(projectPath);
1171
+ const result = await show(projectPath, stashId);
1172
+ if (!result) {
1173
+ return {
1174
+ content: [{
1175
+ type: "text",
1176
+ text: `No stash found with ID "${stashId}".`
1177
+ }]
1178
+ };
1179
+ }
1180
+ const { stash, diff, files } = result;
1181
+ return {
1182
+ content: [{
1183
+ type: "text",
1184
+ text: JSON.stringify({
1185
+ id: stash.id,
1186
+ number: stash.number,
1187
+ prompt: stash.prompt,
1188
+ status: stash.status,
1189
+ branch: stash.branch,
1190
+ componentPath: stash.componentPath ?? null,
1191
+ screenshotPath: stash.screenshotUrl,
1192
+ relatedTo: stash.relatedTo,
1193
+ createdAt: stash.createdAt,
1194
+ filesChanged: files,
1195
+ diff
1196
+ }, null, 2)
1197
+ }]
1198
+ };
1199
+ }
1200
+
1201
+ // ../mcp/src/tools/remove.ts
1202
+ import { z as z5 } from "zod";
1093
1203
  var removeParams = {
1094
- stashId: z4.string().describe("The stash ID to remove")
1204
+ stashId: z5.string().describe("The stash ID to remove")
1095
1205
  };
1096
1206
  async function handleRemove(args, projectPath) {
1097
1207
  const { stashId } = args;
@@ -1482,9 +1592,12 @@ ${refs.join(`
1482
1592
  }
1483
1593
  const chatPrompt = [
1484
1594
  "You are helping the user explore UI design variations for their project.",
1485
- "You have access to stashes MCP tools to generate, list, browse, vary, and apply stashes.",
1595
+ "You have access to stashes MCP tools to generate, list, show, browse, vary, and apply stashes.",
1486
1596
  "If the user asks you to generate, create, or make variations, use the stashes_generate tool.",
1487
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.',
1488
1601
  "Otherwise, respond conversationally about their project and stashes.",
1489
1602
  "",
1490
1603
  component ? `Component: ${component.name}` : "",
@@ -1500,11 +1613,27 @@ ${sourceCode.substring(0, 3000)}
1500
1613
  ].filter(Boolean).join(`
1501
1614
  `);
1502
1615
  const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath);
1503
- let fullResponse = "";
1616
+ let thinkingBuf = "";
1617
+ let textBuf = "";
1618
+ const pendingMessages = [];
1619
+ const now = new Date().toISOString();
1620
+ function flushThinking() {
1621
+ if (!thinkingBuf)
1622
+ return;
1623
+ pendingMessages.push({ id: crypto.randomUUID(), role: "assistant", content: thinkingBuf, type: "thinking", createdAt: now });
1624
+ thinkingBuf = "";
1625
+ }
1626
+ function flushText() {
1627
+ if (!textBuf)
1628
+ return;
1629
+ pendingMessages.push({ id: crypto.randomUUID(), role: "assistant", content: textBuf, type: "text", createdAt: now });
1630
+ textBuf = "";
1631
+ }
1504
1632
  try {
1505
1633
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
1506
1634
  if (chunk.type === "text") {
1507
- fullResponse += chunk.content;
1635
+ flushThinking();
1636
+ textBuf += chunk.content;
1508
1637
  this.broadcast({
1509
1638
  type: "ai_stream",
1510
1639
  content: chunk.content,
@@ -1512,6 +1641,7 @@ ${sourceCode.substring(0, 3000)}
1512
1641
  source: "chat"
1513
1642
  });
1514
1643
  } else if (chunk.type === "thinking") {
1644
+ thinkingBuf += chunk.content;
1515
1645
  this.broadcast({
1516
1646
  type: "ai_stream",
1517
1647
  content: chunk.content,
@@ -1519,13 +1649,25 @@ ${sourceCode.substring(0, 3000)}
1519
1649
  source: "chat"
1520
1650
  });
1521
1651
  } else if (chunk.type === "tool_use") {
1652
+ flushThinking();
1653
+ flushText();
1522
1654
  let toolName = "unknown";
1523
1655
  let toolParams = {};
1524
1656
  try {
1525
1657
  const parsed = JSON.parse(chunk.content);
1526
- toolName = parsed.tool || parsed.name || "unknown";
1527
- toolParams = parsed.input || parsed.params || {};
1658
+ toolName = parsed.tool ?? "unknown";
1659
+ toolParams = parsed.input ?? {};
1528
1660
  } catch {}
1661
+ pendingMessages.push({
1662
+ id: crypto.randomUUID(),
1663
+ role: "assistant",
1664
+ content: chunk.content,
1665
+ type: "tool_start",
1666
+ toolName,
1667
+ toolParams,
1668
+ toolStatus: "running",
1669
+ createdAt: now
1670
+ });
1529
1671
  this.broadcast({
1530
1672
  type: "ai_stream",
1531
1673
  content: chunk.content,
@@ -1536,35 +1678,37 @@ ${sourceCode.substring(0, 3000)}
1536
1678
  toolStatus: "running"
1537
1679
  });
1538
1680
  } else if (chunk.type === "tool_result") {
1539
- let toolName = "unknown";
1540
- let toolResult = "";
1681
+ let toolResult = chunk.content;
1682
+ let isError = false;
1541
1683
  try {
1542
1684
  const parsed = JSON.parse(chunk.content);
1543
- toolName = parsed.tool || parsed.name || "unknown";
1544
- toolResult = typeof parsed.result === "string" ? parsed.result.substring(0, 200) : JSON.stringify(parsed.result).substring(0, 200);
1545
- } catch {
1546
- toolResult = chunk.content.substring(0, 200);
1547
- }
1685
+ toolResult = parsed.result ?? chunk.content;
1686
+ isError = !!parsed.is_error;
1687
+ } catch {}
1688
+ pendingMessages.push({
1689
+ id: crypto.randomUUID(),
1690
+ role: "assistant",
1691
+ content: chunk.content,
1692
+ type: "tool_end",
1693
+ toolStatus: isError ? "error" : "completed",
1694
+ toolResult: toolResult.substring(0, 300),
1695
+ createdAt: now
1696
+ });
1548
1697
  this.broadcast({
1549
1698
  type: "ai_stream",
1550
1699
  content: chunk.content,
1551
1700
  streamType: "tool_end",
1552
1701
  source: "chat",
1553
- toolName,
1554
- toolStatus: "completed",
1555
- toolResult
1702
+ toolStatus: isError ? "error" : "completed",
1703
+ toolResult: toolResult.substring(0, 300)
1556
1704
  });
1557
1705
  }
1558
1706
  }
1559
1707
  await aiProcess.process.exited;
1560
- if (fullResponse) {
1561
- this.persistence.saveChatMessage(projectId, chatId, {
1562
- id: crypto.randomUUID(),
1563
- role: "assistant",
1564
- content: fullResponse,
1565
- type: "text",
1566
- createdAt: new Date().toISOString()
1567
- });
1708
+ flushThinking();
1709
+ flushText();
1710
+ for (const msg of pendingMessages) {
1711
+ this.persistence.saveChatMessage(projectId, chatId, msg);
1568
1712
  }
1569
1713
  } catch (err) {
1570
1714
  this.broadcast({
@@ -2196,7 +2340,8 @@ var server = new McpServer({
2196
2340
  });
2197
2341
  server.tool("stashes_generate", "Generate multiple AI-powered UI design explorations (stashes) for a given prompt. Each stash applies a different creative direction.", generateParams, async (args) => handleGenerate(args, projectPath));
2198
2342
  server.tool("stashes_list", "List all existing stashes in the current project. Shows ID, prompt, status, branch, and screenshot path.", listParams, async (args) => handleList(args, projectPath));
2199
- server.tool("stashes_apply", "Merge a stash branch into the current git branch. Applies the AI-generated UI changes and cleans up all worktrees.", applyParams, async (args) => handleApply(args, projectPath));
2343
+ server.tool("stashes_show", "Show detailed information about a specific stash including its git diff, changed files, prompt, and metadata. Use this to inspect what a stash changed without applying it.", showParams, async (args) => handleShow(args, projectPath));
2344
+ server.tool("stashes_apply", "Merge a stash branch into the current git branch. Applies the AI-generated UI changes and cleans up all worktrees. ONLY use when the user explicitly asks to apply or merge.", applyParams, async (args) => handleApply(args, projectPath));
2200
2345
  server.tool("stashes_vary", "Create a variation of an existing stash by applying additional changes on top of it.", varyParams, async (args) => handleVary(args, projectPath));
2201
2346
  server.tool("stashes_remove", "Delete a stash \u2014 removes its persistence entry and git branch.", removeParams, async (args) => handleRemove(args, projectPath));
2202
2347
  server.tool("stashes_browse", "Start the Stashes web server and open the browser for interactive browsing of generated stashes.", browseParams, async (args) => handleBrowse(args, projectPath));