stashes 0.1.50 → 0.1.52

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
@@ -26,8 +26,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
26
26
  import { z } from "zod";
27
27
 
28
28
  // ../core/dist/generation.js
29
- import { readFileSync as readFileSync2, existsSync as existsSync6 } from "fs";
30
- import { join as join6 } from "path";
29
+ import { readFileSync as readFileSync2, existsSync as existsSync7 } from "fs";
30
+ import { join as join7 } from "path";
31
31
  var {spawn: spawn3 } = globalThis.Bun;
32
32
  import simpleGit3 from "simple-git";
33
33
  // ../shared/dist/constants/index.js
@@ -529,37 +529,68 @@ class PersistenceService {
529
529
 
530
530
  // ../core/dist/ai-process.js
531
531
  var {spawn } = globalThis.Bun;
532
+ import { writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
533
+ import { join as join4 } from "path";
534
+ import { tmpdir } from "os";
532
535
  var CLAUDE_BIN = "/opt/homebrew/bin/claude";
536
+ var DEFAULT_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
537
+ function getPlaywrightMcpConfigPath() {
538
+ const configDir = join4(tmpdir(), "stashes-mcp");
539
+ const configPath = join4(configDir, "playwright.json");
540
+ if (!existsSync4(configPath)) {
541
+ mkdirSync3(configDir, { recursive: true });
542
+ writeFileSync2(configPath, JSON.stringify({
543
+ mcpServers: {
544
+ playwright: { command: "npx", args: ["@playwright/mcp@latest"] }
545
+ }
546
+ }), "utf-8");
547
+ }
548
+ return configPath;
549
+ }
533
550
  var processes = new Map;
534
- function startAiProcess(id, prompt, cwd, resumeSessionId, model) {
535
- killAiProcess(id);
536
- logger.info("claude", `spawning process: ${id}`, {
537
- cwd,
538
- promptLength: prompt.length,
539
- promptPreview: prompt.substring(0, 100),
540
- resumeSessionId,
541
- model
551
+ function startAiProcess(idOrOpts, prompt, cwd, resumeSessionId, model) {
552
+ const opts = typeof idOrOpts === "string" ? { id: idOrOpts, prompt, cwd, resumeSessionId, model } : idOrOpts;
553
+ const bare = opts.bare ?? true;
554
+ const tools = opts.tools ?? DEFAULT_TOOLS;
555
+ killAiProcess(opts.id);
556
+ logger.info("claude", `spawning process: ${opts.id}`, {
557
+ cwd: opts.cwd,
558
+ promptLength: opts.prompt.length,
559
+ promptPreview: opts.prompt.substring(0, 100),
560
+ resumeSessionId: opts.resumeSessionId,
561
+ model: opts.model,
562
+ bare,
563
+ tools: bare ? tools.join(",") : "all"
542
564
  });
543
- const cmd = [CLAUDE_BIN, "-p", prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
544
- if (resumeSessionId) {
545
- cmd.push("--resume", resumeSessionId);
565
+ const cmd = [CLAUDE_BIN, "-p", opts.prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
566
+ if (bare) {
567
+ cmd.push("--bare");
568
+ if (tools.length > 0) {
569
+ cmd.push("--tools", tools.join(","));
570
+ }
571
+ if (opts.mcpConfigPath) {
572
+ cmd.push("--mcp-config", opts.mcpConfigPath);
573
+ }
546
574
  }
547
- if (model) {
548
- cmd.push("--model", model);
575
+ if (opts.resumeSessionId) {
576
+ cmd.push("--resume", opts.resumeSessionId);
577
+ }
578
+ if (opts.model) {
579
+ cmd.push("--model", opts.model);
549
580
  }
550
581
  const proc = spawn({
551
582
  cmd,
552
583
  stdin: "ignore",
553
584
  stdout: "pipe",
554
585
  stderr: "pipe",
555
- cwd,
586
+ cwd: opts.cwd,
556
587
  env: { ...process.env }
557
588
  });
558
589
  proc.exited.then((code) => {
559
- logger.info("claude", `process exited: ${id}`, { exitCode: code });
590
+ logger.info("claude", `process exited: ${opts.id}`, { exitCode: code });
560
591
  });
561
- const aiProcess = { process: proc, id };
562
- processes.set(id, aiProcess);
592
+ const aiProcess = { process: proc, id: opts.id };
593
+ processes.set(opts.id, aiProcess);
563
594
  return aiProcess;
564
595
  }
565
596
  function killAiProcess(id) {
@@ -574,8 +605,8 @@ function killAiProcess(id) {
574
605
  }
575
606
  return false;
576
607
  }
577
- var toolNameMap = new Map;
578
608
  async function* parseClaudeStream(proc) {
609
+ const toolNameMap = new Map;
579
610
  const stdout = proc.stdout;
580
611
  if (!stdout || typeof stdout === "number") {
581
612
  throw new Error("Process stdout is not a readable stream");
@@ -671,22 +702,22 @@ async function* parseClaudeStream(proc) {
671
702
  }
672
703
 
673
704
  // ../core/dist/smart-screenshot.js
674
- import { join as join5 } from "path";
675
- import { mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
705
+ import { join as join6 } from "path";
706
+ import { mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
676
707
  import simpleGit2 from "simple-git";
677
708
 
678
709
  // ../core/dist/screenshot.js
679
710
  var {spawn: spawn2 } = globalThis.Bun;
680
- import { join as join4 } from "path";
681
- import { mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
711
+ import { join as join5 } from "path";
712
+ import { mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
682
713
  var SCREENSHOTS_DIR = ".stashes/screenshots";
683
714
  async function captureScreenshot(port, projectPath, stashId) {
684
- const screenshotsDir = join4(projectPath, SCREENSHOTS_DIR);
685
- if (!existsSync4(screenshotsDir)) {
686
- mkdirSync3(screenshotsDir, { recursive: true });
715
+ const screenshotsDir = join5(projectPath, SCREENSHOTS_DIR);
716
+ if (!existsSync5(screenshotsDir)) {
717
+ mkdirSync4(screenshotsDir, { recursive: true });
687
718
  }
688
719
  const filename = `${stashId}.png`;
689
- const outputPath = join4(screenshotsDir, filename);
720
+ const outputPath = join5(screenshotsDir, filename);
690
721
  const playwrightScript = `
691
722
  const { chromium } = require('playwright');
692
723
  (async () => {
@@ -743,14 +774,8 @@ ${truncatedDiff}`;
743
774
  }
744
775
  function buildScreenshotPrompt(port, diff, screenshotDir, stashId) {
745
776
  return [
746
- "You are a screenshot assistant. Your ONLY job is to take screenshots of a running web app using Playwright MCP tools.",
747
- "",
748
- "IMPORTANT RULES:",
749
- "- ONLY use these Playwright MCP tools: mcp__playwright__browser_navigate, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_click, mcp__playwright__browser_snapshot",
750
- "- Do NOT use any other tools (no Bash, Read, Grep, Agent, ToolSearch, etc.)",
751
- "- Do NOT start any sessions or call useai tools",
752
- "- Do NOT read or analyze code files \u2014 the diff below tells you everything",
753
- "- Be fast \u2014 you have a strict time limit",
777
+ "You are a screenshot assistant. Take screenshots of a running web app using Playwright MCP tools.",
778
+ "Be fast \u2014 you have a strict time limit.",
754
779
  "",
755
780
  `## The app is running at: http://localhost:${port}`,
756
781
  "",
@@ -774,7 +799,7 @@ function buildScreenshotPrompt(port, diff, screenshotDir, stashId) {
774
799
  "{",
775
800
  ' "screenshots": [',
776
801
  " {",
777
- ` "path": "${join5(screenshotDir, `${stashId}.png`)}",`,
802
+ ` "path": "${join6(screenshotDir, `${stashId}.png`)}",`,
778
803
  ' "label": "Short description of what is shown",',
779
804
  ' "route": "/the-url-path",',
780
805
  ' "isPrimary": true',
@@ -815,9 +840,9 @@ async function fallbackScreenshot(port, projectPath, stashId) {
815
840
  }
816
841
  async function captureSmartScreenshots(opts) {
817
842
  const { projectPath, stashId, stashBranch, parentBranch, worktreePath, port, model = "sonnet", timeout = DEFAULT_TIMEOUT } = opts;
818
- const screenshotDir = join5(projectPath, SCREENSHOTS_DIR2);
819
- if (!existsSync5(screenshotDir)) {
820
- mkdirSync4(screenshotDir, { recursive: true });
843
+ const screenshotDir = join6(projectPath, SCREENSHOTS_DIR2);
844
+ if (!existsSync6(screenshotDir)) {
845
+ mkdirSync5(screenshotDir, { recursive: true });
821
846
  }
822
847
  const diff = await getStashDiff(worktreePath, parentBranch);
823
848
  if (!diff) {
@@ -827,7 +852,15 @@ async function captureSmartScreenshots(opts) {
827
852
  const processId = `screenshot-ai-${stashId}`;
828
853
  const prompt = buildScreenshotPrompt(port, diff, screenshotDir, stashId);
829
854
  const modelFlag = model === "sonnet" ? "sonnet" : "haiku";
830
- const aiProcess = startAiProcess(processId, prompt, worktreePath, undefined, modelFlag);
855
+ const aiProcess = startAiProcess({
856
+ id: processId,
857
+ prompt,
858
+ cwd: worktreePath,
859
+ model: modelFlag,
860
+ bare: true,
861
+ tools: [],
862
+ mcpConfigPath: getPlaywrightMcpConfigPath()
863
+ });
831
864
  let textOutput = "";
832
865
  let timedOut = false;
833
866
  const timeoutId = setTimeout(() => {
@@ -859,7 +892,7 @@ async function captureSmartScreenshots(opts) {
859
892
  let primaryUrl = "";
860
893
  for (const shot of result.screenshots) {
861
894
  const filename = shot.path.split("/").pop() || "";
862
- if (!existsSync5(shot.path)) {
895
+ if (!existsSync6(shot.path)) {
863
896
  logger.warn("smart-screenshot", `Screenshot file not found: ${shot.path}`);
864
897
  continue;
865
898
  }
@@ -953,8 +986,8 @@ async function generate(opts) {
953
986
  const selectedDirectives = directives.slice(0, count);
954
987
  let sourceCode = "";
955
988
  if (component?.filePath) {
956
- const sourceFile = join6(projectPath, component.filePath);
957
- if (existsSync6(sourceFile)) {
989
+ const sourceFile = join7(projectPath, component.filePath);
990
+ if (existsSync7(sourceFile)) {
958
991
  sourceCode = readFileSync2(sourceFile, "utf-8");
959
992
  }
960
993
  }
@@ -990,7 +1023,12 @@ async function generate(opts) {
990
1023
  } else {
991
1024
  stashPrompt = buildFreeformStashPrompt(prompt, directive);
992
1025
  }
993
- const aiProcess = startAiProcess(stashId, stashPrompt, worktree.path);
1026
+ const aiProcess = startAiProcess({
1027
+ id: stashId,
1028
+ prompt: stashPrompt,
1029
+ cwd: worktree.path,
1030
+ bare: false
1031
+ });
994
1032
  try {
995
1033
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
996
1034
  emit(onProgress, {
@@ -999,14 +1037,63 @@ async function generate(opts) {
999
1037
  content: chunk.content,
1000
1038
  streamType: chunk.type
1001
1039
  });
1040
+ if (chunk.type === "tool_use" && chunk.toolName) {
1041
+ const knownTools = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
1042
+ if (knownTools.includes(chunk.toolName)) {
1043
+ const filePath = chunk.toolInput?.file_path ?? chunk.toolInput?.path ?? chunk.toolInput?.command ?? undefined;
1044
+ const lines = chunk.toolInput?.content ? chunk.toolInput.content.split(`
1045
+ `).length : undefined;
1046
+ emit(onProgress, {
1047
+ type: "activity",
1048
+ stashId,
1049
+ action: chunk.toolName,
1050
+ file: filePath,
1051
+ lines,
1052
+ timestamp: Date.now()
1053
+ });
1054
+ }
1055
+ } else if (chunk.type === "thinking") {
1056
+ emit(onProgress, {
1057
+ type: "activity",
1058
+ stashId,
1059
+ action: "thinking",
1060
+ content: chunk.content.substring(0, 200),
1061
+ timestamp: Date.now()
1062
+ });
1063
+ } else if (chunk.type === "text") {
1064
+ emit(onProgress, {
1065
+ type: "activity",
1066
+ stashId,
1067
+ action: "text",
1068
+ content: chunk.content.substring(0, 200),
1069
+ timestamp: Date.now()
1070
+ });
1071
+ }
1002
1072
  }
1003
1073
  await aiProcess.process.exited;
1004
1074
  const wtGit = simpleGit3(worktree.path);
1075
+ let hasChanges = false;
1005
1076
  try {
1006
1077
  await wtGit.add("-A");
1007
- await wtGit.commit(`stashes: stash ${stashId}`);
1008
- } catch {}
1078
+ const status = await wtGit.status();
1079
+ if (status.staged.length > 0) {
1080
+ await wtGit.commit(`stashes: stash ${stashId}`);
1081
+ hasChanges = true;
1082
+ logger.info("generation", `committed changes for ${stashId}`, { files: status.staged.length });
1083
+ } else {
1084
+ logger.warn("generation", `AI produced no file changes for ${stashId} \u2014 skipping`);
1085
+ }
1086
+ } catch (commitErr) {
1087
+ logger.warn("generation", `commit failed for ${stashId}`, {
1088
+ error: commitErr instanceof Error ? commitErr.message : String(commitErr)
1089
+ });
1090
+ }
1009
1091
  await worktreeManager.removeGeneration(stashId);
1092
+ if (!hasChanges) {
1093
+ persistence.saveStash({ ...stash, status: "error", error: "AI generation produced no file changes" });
1094
+ emit(onProgress, { type: "error", stashId, error: "AI generation produced no file changes" });
1095
+ return;
1096
+ }
1010
1097
  const generatedStash = { ...stash, status: "screenshotting" };
1011
1098
  completedStashes.push(generatedStash);
1012
1099
  persistence.saveStash(generatedStash);
@@ -1137,7 +1224,12 @@ async function vary(opts) {
1137
1224
  persistence.saveStash(stash);
1138
1225
  emit2(onProgress, { type: "generating", stashId, number: stashNumber });
1139
1226
  const varyPrompt = `The user wants to vary the current UI. Apply this change: ${prompt}`;
1140
- const aiProcess = startAiProcess(stashId, varyPrompt, worktree.path);
1227
+ const aiProcess = startAiProcess({
1228
+ id: stashId,
1229
+ prompt: varyPrompt,
1230
+ cwd: worktree.path,
1231
+ bare: false
1232
+ });
1141
1233
  try {
1142
1234
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
1143
1235
  emit2(onProgress, {
@@ -1149,11 +1241,29 @@ async function vary(opts) {
1149
1241
  }
1150
1242
  await aiProcess.process.exited;
1151
1243
  const wtGit = simpleGit4(worktree.path);
1244
+ let hasChanges = false;
1152
1245
  try {
1153
1246
  await wtGit.add("-A");
1154
- await wtGit.commit(`stashes: vary ${stashId} from ${sourceStashId}`);
1155
- } catch {}
1247
+ const status = await wtGit.status();
1248
+ if (status.staged.length > 0) {
1249
+ await wtGit.commit(`stashes: vary ${stashId} from ${sourceStashId}`);
1250
+ hasChanges = true;
1251
+ logger.info("vary", `committed changes for ${stashId}`, { files: status.staged.length });
1252
+ } else {
1253
+ logger.warn("vary", `AI produced no file changes for ${stashId}`);
1254
+ }
1255
+ } catch (commitErr) {
1256
+ logger.warn("vary", `commit failed for ${stashId}`, {
1257
+ error: commitErr instanceof Error ? commitErr.message : String(commitErr)
1258
+ });
1259
+ }
1156
1260
  await worktreeManager.removeGeneration(stashId);
1261
+ if (!hasChanges) {
1262
+ const errorStash = { ...stash, status: "error", error: "AI generation produced no file changes" };
1263
+ persistence.saveStash(errorStash);
1264
+ emit2(onProgress, { type: "error", stashId, error: "AI generation produced no file changes" });
1265
+ return errorStash;
1266
+ }
1157
1267
  persistence.saveStash({ ...stash, status: "screenshotting" });
1158
1268
  emit2(onProgress, { type: "screenshotting", stashId });
1159
1269
  let screenshotPath = "";
@@ -1274,13 +1384,14 @@ async function show(projectPath, stashId) {
1274
1384
  return { stash: found, diff, files };
1275
1385
  }
1276
1386
  // ../mcp/src/tools/generate.ts
1387
+ var HONO_PORT = process.env.STASHES_PORT ?? "4000";
1277
1388
  var generateParams = {
1278
1389
  prompt: z.string().describe('What UI changes to generate (e.g. "make the hero section bolder")'),
1279
1390
  count: z.number().min(1).max(5).optional().describe("Number of stashes to generate (1-5, default 3)"),
1280
1391
  filePath: z.string().optional().describe('Optional: scope changes to a specific file (e.g. "src/components/Hero.tsx")'),
1281
1392
  exportName: z.string().optional().describe("Optional: specific component export name within the file")
1282
1393
  };
1283
- async function handleGenerate(args, projectPath) {
1394
+ async function handleGenerate(args, projectPath, server) {
1284
1395
  const { prompt, filePath, exportName } = args;
1285
1396
  const count = args.count ?? 3;
1286
1397
  initLogFile(projectPath);
@@ -1298,14 +1409,41 @@ async function handleGenerate(args, projectPath) {
1298
1409
  };
1299
1410
  persistence.saveProject(project);
1300
1411
  }
1412
+ let pendingEvents = [];
1413
+ let flushTimer = null;
1414
+ function flushPending() {
1415
+ if (pendingEvents.length === 0)
1416
+ return;
1417
+ const batch = pendingEvents.splice(0);
1418
+ flushTimer = null;
1419
+ fetch(`http://localhost:${HONO_PORT}/api/stash-activity`, {
1420
+ method: "POST",
1421
+ headers: { "Content-Type": "application/json" },
1422
+ body: JSON.stringify(batch.filter((e) => e.type === "activity"))
1423
+ }).catch(() => {});
1424
+ }
1301
1425
  const stashes = await generate({
1302
1426
  projectPath,
1303
1427
  projectId: project.id,
1304
1428
  prompt,
1305
1429
  count,
1306
1430
  component: filePath ? { filePath, exportName } : undefined,
1307
- onProgress: () => {}
1431
+ onProgress: (event) => {
1432
+ if (event.type === "activity") {
1433
+ pendingEvents.push(event);
1434
+ if (!flushTimer) {
1435
+ flushTimer = setTimeout(flushPending, 100);
1436
+ }
1437
+ }
1438
+ server.server.sendLoggingMessage({
1439
+ level: "info",
1440
+ data: event
1441
+ }).catch(() => {});
1442
+ }
1308
1443
  });
1444
+ if (flushTimer)
1445
+ clearTimeout(flushTimer);
1446
+ flushPending();
1309
1447
  return {
1310
1448
  content: [{
1311
1449
  type: "text",
@@ -1452,150 +1590,17 @@ async function handleRemove(args, projectPath) {
1452
1590
  // ../server/dist/index.js
1453
1591
  import { Hono as Hono2 } from "hono";
1454
1592
  import { cors } from "hono/cors";
1455
- import { join as join9, dirname as dirname2 } from "path";
1593
+ import { join as join11, dirname as dirname3 } from "path";
1456
1594
  import { fileURLToPath } from "url";
1457
- import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
1595
+ import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
1458
1596
 
1459
1597
  // ../server/dist/routes/api.js
1460
1598
  import { Hono } from "hono";
1461
- import { join as join7, basename } from "path";
1462
- import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
1463
- var app = new Hono;
1464
- app.get("/health", (c) => c.json({ status: "ok", service: "stashes" }));
1465
- app.get("/projects", (c) => {
1466
- const persistence = getPersistence();
1467
- const projects = persistence.listProjects();
1468
- const projectsWithCounts = projects.map((p) => ({
1469
- ...p,
1470
- stashCount: persistence.listStashes(p.id).length,
1471
- recentScreenshots: persistence.listStashes(p.id).filter((s) => s.screenshotUrl).slice(-4).map((s) => s.screenshotUrl)
1472
- }));
1473
- return c.json({ data: projectsWithCounts });
1474
- });
1475
- app.post("/projects", async (c) => {
1476
- const { name, description } = await c.req.json();
1477
- const project = {
1478
- id: `proj_${crypto.randomUUID().substring(0, 8)}`,
1479
- name,
1480
- description,
1481
- createdAt: new Date().toISOString(),
1482
- updatedAt: new Date().toISOString()
1483
- };
1484
- getPersistence().saveProject(project);
1485
- return c.json({ data: project }, 201);
1486
- });
1487
- app.get("/projects/:id", (c) => {
1488
- const persistence = getPersistence();
1489
- const project = persistence.getProject(c.req.param("id"));
1490
- if (!project)
1491
- return c.json({ error: "Project not found" }, 404);
1492
- const stashes = persistence.listStashes(project.id);
1493
- const chats = persistence.listChats(project.id);
1494
- return c.json({ data: { ...project, stashes, chats } });
1495
- });
1496
- app.delete("/projects/:id", (c) => {
1497
- const id = c.req.param("id");
1498
- getPersistence().deleteProject(id);
1499
- return c.json({ data: { deleted: id } });
1500
- });
1501
- app.get("/chats", (c) => {
1502
- const persistence = getPersistence();
1503
- const project = ensureProject(persistence);
1504
- const chats = persistence.listChats(project.id);
1505
- const stashes = persistence.listStashes(project.id);
1506
- return c.json({ data: { project, chats, stashes } });
1507
- });
1508
- app.post("/chats", async (c) => {
1509
- const persistence = getPersistence();
1510
- const project = ensureProject(persistence);
1511
- const { title, referencedStashIds } = await c.req.json();
1512
- const chatCount = persistence.listChats(project.id).length;
1513
- const chat = {
1514
- id: `chat_${crypto.randomUUID().substring(0, 8)}`,
1515
- projectId: project.id,
1516
- title: title?.trim() || `Chat ${chatCount + 1}`,
1517
- referencedStashIds: referencedStashIds ?? [],
1518
- createdAt: new Date().toISOString(),
1519
- updatedAt: new Date().toISOString()
1520
- };
1521
- persistence.saveChat(chat);
1522
- return c.json({ data: chat }, 201);
1523
- });
1524
- app.patch("/chats/:chatId", async (c) => {
1525
- const persistence = getPersistence();
1526
- const project = ensureProject(persistence);
1527
- const chatId = c.req.param("chatId");
1528
- const chat = persistence.getChat(project.id, chatId);
1529
- if (!chat)
1530
- return c.json({ error: "Chat not found" }, 404);
1531
- const body = await c.req.json();
1532
- const updated = {
1533
- ...chat,
1534
- ...body.referencedStashIds !== undefined ? { referencedStashIds: body.referencedStashIds } : {},
1535
- updatedAt: new Date().toISOString()
1536
- };
1537
- persistence.saveChat(updated);
1538
- return c.json({ data: updated });
1539
- });
1540
- app.get("/chats/:chatId", (c) => {
1541
- const persistence = getPersistence();
1542
- const project = ensureProject(persistence);
1543
- const chatId = c.req.param("chatId");
1544
- const chat = persistence.getChat(project.id, chatId);
1545
- if (!chat)
1546
- return c.json({ error: "Chat not found" }, 404);
1547
- const messages = persistence.getChatMessages(project.id, chatId);
1548
- const refIds = new Set(chat.referencedStashIds ?? []);
1549
- const stashes = persistence.listStashes(project.id).filter((s) => s.originChatId === chatId || refIds.has(s.id));
1550
- return c.json({ data: { ...chat, messages, stashes } });
1551
- });
1552
- app.delete("/chats/:chatId", (c) => {
1553
- const persistence = getPersistence();
1554
- const project = ensureProject(persistence);
1555
- const chatId = c.req.param("chatId");
1556
- persistence.deleteChat(project.id, chatId);
1557
- return c.json({ data: { deleted: chatId } });
1558
- });
1559
- app.get("/dev-server-status", async (c) => {
1560
- const port = serverState.userDevPort;
1561
- try {
1562
- const res = await fetch(`http://localhost:${port}`, {
1563
- method: "HEAD",
1564
- signal: AbortSignal.timeout(2000)
1565
- });
1566
- return c.json({ up: res.status < 500, port });
1567
- } catch {
1568
- return c.json({ up: false, port });
1569
- }
1570
- });
1571
- app.get("/screenshots/:filename", (c) => {
1572
- const filename = c.req.param("filename");
1573
- const filePath = join7(serverState.projectPath, ".stashes", "screenshots", filename);
1574
- if (!existsSync7(filePath))
1575
- return c.json({ error: "Not found" }, 404);
1576
- const content = readFileSync3(filePath);
1577
- return new Response(content, {
1578
- headers: { "content-type": "image/png", "cache-control": "no-cache" }
1579
- });
1580
- });
1581
- function ensureProject(persistence) {
1582
- const projects = persistence.listProjects();
1583
- if (projects.length > 0)
1584
- return projects[0];
1585
- const project = {
1586
- id: `proj_${crypto.randomUUID().substring(0, 8)}`,
1587
- name: basename(serverState.projectPath),
1588
- createdAt: new Date().toISOString(),
1589
- updatedAt: new Date().toISOString()
1590
- };
1591
- persistence.saveProject(project);
1592
- persistence.migrateOldChat(project.id);
1593
- return project;
1594
- }
1595
- var apiRoutes = app;
1599
+ import { join as join10, basename } from "path";
1600
+ import { existsSync as existsSync10, readFileSync as readFileSync5 } from "fs";
1596
1601
 
1597
1602
  // ../server/dist/services/stash-service.js
1598
- import { readFileSync as readFileSync4, existsSync as existsSync8 } from "fs";
1603
+ import { readFileSync as readFileSync3, existsSync as existsSync8 } from "fs";
1599
1604
  import { join as join8 } from "path";
1600
1605
 
1601
1606
  // ../server/dist/services/app-proxy.js
@@ -2079,6 +2084,7 @@ class StashService {
2079
2084
  worktreeManager;
2080
2085
  persistence;
2081
2086
  broadcast;
2087
+ activityStore;
2082
2088
  previewPool;
2083
2089
  selectedComponent = null;
2084
2090
  messageQueue = [];
@@ -2088,11 +2094,12 @@ class StashService {
2088
2094
  stashPollTimer = null;
2089
2095
  knownStashIds = new Set;
2090
2096
  pendingComponentResolve = null;
2091
- constructor(projectPath, worktreeManager, persistence, broadcast, stashPortStart, stashPortEnd) {
2097
+ constructor(projectPath, worktreeManager, persistence, broadcast, activityStore, stashPortStart, stashPortEnd) {
2092
2098
  this.projectPath = projectPath;
2093
2099
  this.worktreeManager = worktreeManager;
2094
2100
  this.persistence = persistence;
2095
2101
  this.broadcast = broadcast;
2102
+ this.activityStore = activityStore;
2096
2103
  this.previewPool = new PreviewPool(worktreeManager, broadcast, undefined, undefined, stashPortStart, stashPortEnd);
2097
2104
  }
2098
2105
  getActiveChatId() {
@@ -2126,7 +2133,14 @@ class StashService {
2126
2133
  "Reply with ONLY the file path relative to the project root."
2127
2134
  ].join(`
2128
2135
  `);
2129
- const aiProcess = startAiProcess("resolve-component", prompt, this.projectPath, undefined, "claude-haiku-4-5-20251001");
2136
+ const aiProcess = startAiProcess({
2137
+ id: "resolve-component",
2138
+ prompt,
2139
+ cwd: this.projectPath,
2140
+ model: "claude-haiku-4-5-20251001",
2141
+ bare: true,
2142
+ tools: ["Read", "Grep", "Glob", "Bash"]
2143
+ });
2130
2144
  let resolvedPath = "";
2131
2145
  try {
2132
2146
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
@@ -2189,7 +2203,7 @@ class StashService {
2189
2203
  if (filePath && filePath !== "auto-detect") {
2190
2204
  const sourceFile = join8(this.projectPath, filePath);
2191
2205
  if (existsSync8(sourceFile)) {
2192
- sourceCode = readFileSync4(sourceFile, "utf-8");
2206
+ sourceCode = readFileSync3(sourceFile, "utf-8");
2193
2207
  }
2194
2208
  }
2195
2209
  let stashContext = "";
@@ -2236,7 +2250,13 @@ ${sourceCode.substring(0, 3000)}
2236
2250
  ].filter(Boolean).join(`
2237
2251
  `);
2238
2252
  }
2239
- const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath, existingSessionId);
2253
+ const aiProcess = startAiProcess({
2254
+ id: "chat",
2255
+ prompt: chatPrompt,
2256
+ cwd: this.projectPath,
2257
+ resumeSessionId: existingSessionId,
2258
+ bare: false
2259
+ });
2240
2260
  let thinkingBuf = "";
2241
2261
  let textBuf = "";
2242
2262
  const now = new Date().toISOString();
@@ -2312,6 +2332,21 @@ ${sourceCode.substring(0, 3000)}
2312
2332
  }
2313
2333
  } else if (chunk.type === "tool_result") {
2314
2334
  this.stopStashPoll();
2335
+ let stashActivity;
2336
+ const toolNameForSnapshot = chunk.toolName ?? "";
2337
+ if (toolNameForSnapshot.includes("stashes_generate") || toolNameForSnapshot.includes("stashes_vary")) {
2338
+ const projectId2 = this.persistence.listProjects()[0]?.id ?? "";
2339
+ const allStashes = this.persistence.listStashes(projectId2);
2340
+ stashActivity = {};
2341
+ for (const s of allStashes) {
2342
+ if (this.activityStore.has(s.id)) {
2343
+ stashActivity[s.id] = this.activityStore.getSnapshot(s.id);
2344
+ this.activityStore.clear(s.id);
2345
+ }
2346
+ }
2347
+ if (Object.keys(stashActivity).length === 0)
2348
+ stashActivity = undefined;
2349
+ }
2315
2350
  let toolResult = chunk.content;
2316
2351
  let isError = false;
2317
2352
  try {
@@ -2326,6 +2361,7 @@ ${sourceCode.substring(0, 3000)}
2326
2361
  type: "tool_end",
2327
2362
  toolStatus: isError ? "error" : "completed",
2328
2363
  toolResult: toolResult.substring(0, 300),
2364
+ stashActivity,
2329
2365
  createdAt: now
2330
2366
  });
2331
2367
  this.broadcast({
@@ -2515,10 +2551,77 @@ ${refDescriptions.join(`
2515
2551
  }
2516
2552
  }
2517
2553
 
2554
+ // ../server/dist/services/activity-store.js
2555
+ import { appendFileSync as appendFileSync2, readFileSync as readFileSync4, existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync3 } from "fs";
2556
+ import { join as join9, dirname as dirname2 } from "path";
2557
+
2558
+ class ActivityStore {
2559
+ cache = new Map;
2560
+ projectPath;
2561
+ constructor(projectPath) {
2562
+ this.projectPath = projectPath;
2563
+ }
2564
+ jsonlPath(stashId) {
2565
+ return join9(this.projectPath, ".stashes", "activity", `${stashId}.jsonl`);
2566
+ }
2567
+ append(event) {
2568
+ const existing = this.cache.get(event.stashId) ?? [];
2569
+ existing.push(event);
2570
+ this.cache.set(event.stashId, existing);
2571
+ const filePath = this.jsonlPath(event.stashId);
2572
+ const dir = dirname2(filePath);
2573
+ if (!existsSync9(dir))
2574
+ mkdirSync6(dir, { recursive: true });
2575
+ appendFileSync2(filePath, JSON.stringify(event) + `
2576
+ `, "utf-8");
2577
+ }
2578
+ getEvents(stashId) {
2579
+ const cached = this.cache.get(stashId);
2580
+ if (cached && cached.length > 0)
2581
+ return cached;
2582
+ const filePath = this.jsonlPath(stashId);
2583
+ if (!existsSync9(filePath))
2584
+ return [];
2585
+ const lines = readFileSync4(filePath, "utf-8").trim().split(`
2586
+ `).filter(Boolean);
2587
+ const events = lines.map((line) => JSON.parse(line));
2588
+ this.cache.set(stashId, events);
2589
+ return events;
2590
+ }
2591
+ getSnapshot(stashId) {
2592
+ const actions = this.getEvents(stashId);
2593
+ const fileActions = actions.filter((a) => ["Read", "Write", "Edit", "Glob", "Grep", "Bash"].includes(a.action));
2594
+ const uniqueFiles = new Set(fileActions.filter((a) => a.file).map((a) => a.file));
2595
+ const timestamps = actions.map((a) => a.timestamp);
2596
+ const duration = timestamps.length > 1 ? Math.round((Math.max(...timestamps) - Math.min(...timestamps)) / 1000) : 0;
2597
+ return {
2598
+ actions: [...actions],
2599
+ stats: {
2600
+ filesChanged: uniqueFiles.size,
2601
+ duration,
2602
+ totalActions: actions.length
2603
+ }
2604
+ };
2605
+ }
2606
+ clear(stashId) {
2607
+ this.cache.delete(stashId);
2608
+ const filePath = this.jsonlPath(stashId);
2609
+ if (existsSync9(filePath)) {
2610
+ rmSync3(filePath);
2611
+ }
2612
+ }
2613
+ has(stashId) {
2614
+ if (this.cache.has(stashId) && (this.cache.get(stashId)?.length ?? 0) > 0)
2615
+ return true;
2616
+ return existsSync9(this.jsonlPath(stashId));
2617
+ }
2618
+ }
2619
+
2518
2620
  // ../server/dist/services/websocket.js
2519
2621
  var worktreeManager;
2520
2622
  var stashService;
2521
2623
  var persistence;
2624
+ var activityStore;
2522
2625
  var clients = new Set;
2523
2626
  function broadcast(event) {
2524
2627
  const data = JSON.stringify(event);
@@ -2529,10 +2632,14 @@ function broadcast(event) {
2529
2632
  function getPersistenceFromWs() {
2530
2633
  return persistence;
2531
2634
  }
2635
+ function getActivityStoreFromWs() {
2636
+ return activityStore;
2637
+ }
2532
2638
  function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPortStart, stashPortEnd) {
2533
2639
  worktreeManager = new WorktreeManager(projectPath);
2534
2640
  persistence = new PersistenceService(projectPath);
2535
- stashService = new StashService(projectPath, worktreeManager, persistence, broadcast, stashPortStart, stashPortEnd);
2641
+ activityStore = new ActivityStore(projectPath);
2642
+ stashService = new StashService(projectPath, worktreeManager, persistence, broadcast, activityStore, stashPortStart, stashPortEnd);
2536
2643
  return {
2537
2644
  open(ws) {
2538
2645
  clients.add(ws);
@@ -2549,6 +2656,15 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPor
2549
2656
  if (activeChatId) {
2550
2657
  ws.send(JSON.stringify({ type: "processing", chatId: activeChatId }));
2551
2658
  }
2659
+ const allStashes = persistence.listStashes(project.id);
2660
+ for (const stash of allStashes) {
2661
+ if (stash.status === "generating" && activityStore.has(stash.id)) {
2662
+ const events = activityStore.getEvents(stash.id);
2663
+ for (const event of events) {
2664
+ ws.send(JSON.stringify({ type: "stash:activity", stashId: stash.id, event }));
2665
+ }
2666
+ }
2667
+ }
2552
2668
  },
2553
2669
  async message(ws, message) {
2554
2670
  const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
@@ -2628,6 +2744,156 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPor
2628
2744
  };
2629
2745
  }
2630
2746
 
2747
+ // ../server/dist/routes/api.js
2748
+ var app = new Hono;
2749
+ app.get("/health", (c) => c.json({ status: "ok", service: "stashes" }));
2750
+ app.get("/projects", (c) => {
2751
+ const persistence2 = getPersistence();
2752
+ const projects = persistence2.listProjects();
2753
+ const projectsWithCounts = projects.map((p) => ({
2754
+ ...p,
2755
+ stashCount: persistence2.listStashes(p.id).length,
2756
+ recentScreenshots: persistence2.listStashes(p.id).filter((s) => s.screenshotUrl).slice(-4).map((s) => s.screenshotUrl)
2757
+ }));
2758
+ return c.json({ data: projectsWithCounts });
2759
+ });
2760
+ app.post("/projects", async (c) => {
2761
+ const { name, description } = await c.req.json();
2762
+ const project = {
2763
+ id: `proj_${crypto.randomUUID().substring(0, 8)}`,
2764
+ name,
2765
+ description,
2766
+ createdAt: new Date().toISOString(),
2767
+ updatedAt: new Date().toISOString()
2768
+ };
2769
+ getPersistence().saveProject(project);
2770
+ return c.json({ data: project }, 201);
2771
+ });
2772
+ app.get("/projects/:id", (c) => {
2773
+ const persistence2 = getPersistence();
2774
+ const project = persistence2.getProject(c.req.param("id"));
2775
+ if (!project)
2776
+ return c.json({ error: "Project not found" }, 404);
2777
+ const stashes = persistence2.listStashes(project.id);
2778
+ const chats = persistence2.listChats(project.id);
2779
+ return c.json({ data: { ...project, stashes, chats } });
2780
+ });
2781
+ app.delete("/projects/:id", (c) => {
2782
+ const id = c.req.param("id");
2783
+ getPersistence().deleteProject(id);
2784
+ return c.json({ data: { deleted: id } });
2785
+ });
2786
+ app.get("/chats", (c) => {
2787
+ const persistence2 = getPersistence();
2788
+ const project = ensureProject(persistence2);
2789
+ const chats = persistence2.listChats(project.id);
2790
+ const stashes = persistence2.listStashes(project.id);
2791
+ return c.json({ data: { project, chats, stashes } });
2792
+ });
2793
+ app.post("/chats", async (c) => {
2794
+ const persistence2 = getPersistence();
2795
+ const project = ensureProject(persistence2);
2796
+ const { title, referencedStashIds } = await c.req.json();
2797
+ const chatCount = persistence2.listChats(project.id).length;
2798
+ const chat = {
2799
+ id: `chat_${crypto.randomUUID().substring(0, 8)}`,
2800
+ projectId: project.id,
2801
+ title: title?.trim() || `Chat ${chatCount + 1}`,
2802
+ referencedStashIds: referencedStashIds ?? [],
2803
+ createdAt: new Date().toISOString(),
2804
+ updatedAt: new Date().toISOString()
2805
+ };
2806
+ persistence2.saveChat(chat);
2807
+ return c.json({ data: chat }, 201);
2808
+ });
2809
+ app.patch("/chats/:chatId", async (c) => {
2810
+ const persistence2 = getPersistence();
2811
+ const project = ensureProject(persistence2);
2812
+ const chatId = c.req.param("chatId");
2813
+ const chat = persistence2.getChat(project.id, chatId);
2814
+ if (!chat)
2815
+ return c.json({ error: "Chat not found" }, 404);
2816
+ const body = await c.req.json();
2817
+ const updated = {
2818
+ ...chat,
2819
+ ...body.referencedStashIds !== undefined ? { referencedStashIds: body.referencedStashIds } : {},
2820
+ updatedAt: new Date().toISOString()
2821
+ };
2822
+ persistence2.saveChat(updated);
2823
+ return c.json({ data: updated });
2824
+ });
2825
+ app.get("/chats/:chatId", (c) => {
2826
+ const persistence2 = getPersistence();
2827
+ const project = ensureProject(persistence2);
2828
+ const chatId = c.req.param("chatId");
2829
+ const chat = persistence2.getChat(project.id, chatId);
2830
+ if (!chat)
2831
+ return c.json({ error: "Chat not found" }, 404);
2832
+ const messages = persistence2.getChatMessages(project.id, chatId);
2833
+ const refIds = new Set(chat.referencedStashIds ?? []);
2834
+ const stashes = persistence2.listStashes(project.id).filter((s) => s.originChatId === chatId || refIds.has(s.id));
2835
+ return c.json({ data: { ...chat, messages, stashes } });
2836
+ });
2837
+ app.delete("/chats/:chatId", (c) => {
2838
+ const persistence2 = getPersistence();
2839
+ const project = ensureProject(persistence2);
2840
+ const chatId = c.req.param("chatId");
2841
+ persistence2.deleteChat(project.id, chatId);
2842
+ return c.json({ data: { deleted: chatId } });
2843
+ });
2844
+ app.get("/dev-server-status", async (c) => {
2845
+ const port = serverState.userDevPort;
2846
+ try {
2847
+ const res = await fetch(`http://localhost:${port}`, {
2848
+ method: "HEAD",
2849
+ signal: AbortSignal.timeout(2000)
2850
+ });
2851
+ return c.json({ up: res.status < 500, port });
2852
+ } catch {
2853
+ return c.json({ up: false, port });
2854
+ }
2855
+ });
2856
+ app.get("/screenshots/:filename", (c) => {
2857
+ const filename = c.req.param("filename");
2858
+ const filePath = join10(serverState.projectPath, ".stashes", "screenshots", filename);
2859
+ if (!existsSync10(filePath))
2860
+ return c.json({ error: "Not found" }, 404);
2861
+ const content = readFileSync5(filePath);
2862
+ return new Response(content, {
2863
+ headers: { "content-type": "image/png", "cache-control": "no-cache" }
2864
+ });
2865
+ });
2866
+ app.post("/stash-activity", async (c) => {
2867
+ const events = await c.req.json();
2868
+ const store = getActivityStoreFromWs();
2869
+ for (const event of events) {
2870
+ store.append(event);
2871
+ broadcast({ type: "stash:activity", stashId: event.stashId, event });
2872
+ }
2873
+ return c.json({ ok: true });
2874
+ });
2875
+ app.get("/stash-activity/:stashId", (c) => {
2876
+ const stashId = c.req.param("stashId");
2877
+ const store = getActivityStoreFromWs();
2878
+ const events = store.getEvents(stashId);
2879
+ return c.json({ data: events });
2880
+ });
2881
+ function ensureProject(persistence2) {
2882
+ const projects = persistence2.listProjects();
2883
+ if (projects.length > 0)
2884
+ return projects[0];
2885
+ const project = {
2886
+ id: `proj_${crypto.randomUUID().substring(0, 8)}`,
2887
+ name: basename(serverState.projectPath),
2888
+ createdAt: new Date().toISOString(),
2889
+ updatedAt: new Date().toISOString()
2890
+ };
2891
+ persistence2.saveProject(project);
2892
+ persistence2.migrateOldChat(project.id);
2893
+ return project;
2894
+ }
2895
+ var apiRoutes = app;
2896
+
2631
2897
  // ../server/dist/index.js
2632
2898
  var serverState = {
2633
2899
  projectPath: "",
@@ -2641,14 +2907,14 @@ app2.use("/*", cors());
2641
2907
  app2.route("/api", apiRoutes);
2642
2908
  app2.get("/*", async (c) => {
2643
2909
  const path = c.req.path;
2644
- const selfDir = dirname2(fileURLToPath(import.meta.url));
2645
- const bundledWebDir = join9(selfDir, "web");
2646
- const monorepoWebDir = join9(selfDir, "../../web/dist");
2647
- const webDistDir = existsSync9(join9(bundledWebDir, "index.html")) ? bundledWebDir : monorepoWebDir;
2910
+ const selfDir = dirname3(fileURLToPath(import.meta.url));
2911
+ const bundledWebDir = join11(selfDir, "web");
2912
+ const monorepoWebDir = join11(selfDir, "../../web/dist");
2913
+ const webDistDir = existsSync11(join11(bundledWebDir, "index.html")) ? bundledWebDir : monorepoWebDir;
2648
2914
  const requestPath = path === "/" ? "/index.html" : path;
2649
- const filePath = join9(webDistDir, requestPath);
2650
- if (existsSync9(filePath) && !filePath.includes("..")) {
2651
- const content = readFileSync5(filePath);
2915
+ const filePath = join11(webDistDir, requestPath);
2916
+ if (existsSync11(filePath) && !filePath.includes("..")) {
2917
+ const content = readFileSync6(filePath);
2652
2918
  const ext = filePath.split(".").pop() || "";
2653
2919
  const contentTypes = {
2654
2920
  html: "text/html; charset=utf-8",
@@ -2666,9 +2932,9 @@ app2.get("/*", async (c) => {
2666
2932
  headers: { "content-type": contentTypes[ext] || "application/octet-stream" }
2667
2933
  });
2668
2934
  }
2669
- const indexPath = join9(webDistDir, "index.html");
2670
- if (existsSync9(indexPath)) {
2671
- const html = readFileSync5(indexPath, "utf-8");
2935
+ const indexPath = join11(webDistDir, "index.html");
2936
+ if (existsSync11(indexPath)) {
2937
+ const html = readFileSync6(indexPath, "utf-8");
2672
2938
  return new Response(html, {
2673
2939
  headers: { "content-type": "text/html; charset=utf-8" }
2674
2940
  });
@@ -2731,11 +2997,11 @@ async function startServer(projectPath, userDevPort, requestedPort = STASHES_POR
2731
2997
  import open from "open";
2732
2998
 
2733
2999
  // ../server/dist/services/detector.js
2734
- import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
2735
- import { join as join10 } from "path";
3000
+ import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
3001
+ import { join as join12 } from "path";
2736
3002
  function detectFramework(projectPath) {
2737
- const packageJsonPath = join10(projectPath, "package.json");
2738
- if (!existsSync10(packageJsonPath)) {
3003
+ const packageJsonPath = join12(projectPath, "package.json");
3004
+ if (!existsSync12(packageJsonPath)) {
2739
3005
  return {
2740
3006
  framework: "unknown",
2741
3007
  devCommand: "npm run dev",
@@ -2743,7 +3009,7 @@ function detectFramework(projectPath) {
2743
3009
  configFile: null
2744
3010
  };
2745
3011
  }
2746
- const packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
3012
+ const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
2747
3013
  const deps = {
2748
3014
  ...packageJson.dependencies,
2749
3015
  ...packageJson.devDependencies
@@ -2797,7 +3063,7 @@ function getDevCommand(packageJson, fallback) {
2797
3063
  }
2798
3064
  function findConfig(projectPath, candidates) {
2799
3065
  for (const candidate of candidates) {
2800
- if (existsSync10(join10(projectPath, candidate))) {
3066
+ if (existsSync12(join12(projectPath, candidate))) {
2801
3067
  return candidate;
2802
3068
  }
2803
3069
  }
@@ -2825,7 +3091,7 @@ var server = new McpServer({
2825
3091
  name: "stashes",
2826
3092
  version: "0.1.0"
2827
3093
  });
2828
- 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));
3094
+ 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, server));
2829
3095
  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));
2830
3096
  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));
2831
3097
  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));