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/cli.js CHANGED
@@ -19,8 +19,8 @@ var __toESM = (mod, isNodeMode, target) => {
19
19
  var __require = import.meta.require;
20
20
 
21
21
  // src/index.ts
22
- import { readFileSync as readFileSync9 } from "fs";
23
- import { join as join13, dirname as dirname5 } from "path";
22
+ import { readFileSync as readFileSync10 } from "fs";
23
+ import { join as join15, dirname as dirname6 } from "path";
24
24
  import { fileURLToPath as fileURLToPath3 } from "url";
25
25
  import { Command } from "commander";
26
26
 
@@ -31,9 +31,9 @@ import open from "open";
31
31
  // ../server/dist/index.js
32
32
  import { Hono as Hono2 } from "hono";
33
33
  import { cors } from "hono/cors";
34
- import { join as join9, dirname as dirname2 } from "path";
34
+ import { join as join11, dirname as dirname3 } from "path";
35
35
  import { fileURLToPath } from "url";
36
- import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
36
+ import { existsSync as existsSync11, readFileSync as readFileSync6 } from "fs";
37
37
  // ../shared/dist/constants/index.js
38
38
  var STASHES_PORT = 4000;
39
39
  var DEFAULT_STASH_COUNT = 3;
@@ -56,156 +56,23 @@ var DEFAULT_DIRECTIVES = [
56
56
  ];
57
57
  // ../server/dist/routes/api.js
58
58
  import { Hono } from "hono";
59
- import { join, basename } from "path";
60
- import { existsSync, readFileSync } from "fs";
61
- var app = new Hono;
62
- app.get("/health", (c) => c.json({ status: "ok", service: "stashes" }));
63
- app.get("/projects", (c) => {
64
- const persistence = getPersistence();
65
- const projects = persistence.listProjects();
66
- const projectsWithCounts = projects.map((p) => ({
67
- ...p,
68
- stashCount: persistence.listStashes(p.id).length,
69
- recentScreenshots: persistence.listStashes(p.id).filter((s) => s.screenshotUrl).slice(-4).map((s) => s.screenshotUrl)
70
- }));
71
- return c.json({ data: projectsWithCounts });
72
- });
73
- app.post("/projects", async (c) => {
74
- const { name, description } = await c.req.json();
75
- const project = {
76
- id: `proj_${crypto.randomUUID().substring(0, 8)}`,
77
- name,
78
- description,
79
- createdAt: new Date().toISOString(),
80
- updatedAt: new Date().toISOString()
81
- };
82
- getPersistence().saveProject(project);
83
- return c.json({ data: project }, 201);
84
- });
85
- app.get("/projects/:id", (c) => {
86
- const persistence = getPersistence();
87
- const project = persistence.getProject(c.req.param("id"));
88
- if (!project)
89
- return c.json({ error: "Project not found" }, 404);
90
- const stashes = persistence.listStashes(project.id);
91
- const chats = persistence.listChats(project.id);
92
- return c.json({ data: { ...project, stashes, chats } });
93
- });
94
- app.delete("/projects/:id", (c) => {
95
- const id = c.req.param("id");
96
- getPersistence().deleteProject(id);
97
- return c.json({ data: { deleted: id } });
98
- });
99
- app.get("/chats", (c) => {
100
- const persistence = getPersistence();
101
- const project = ensureProject(persistence);
102
- const chats = persistence.listChats(project.id);
103
- const stashes = persistence.listStashes(project.id);
104
- return c.json({ data: { project, chats, stashes } });
105
- });
106
- app.post("/chats", async (c) => {
107
- const persistence = getPersistence();
108
- const project = ensureProject(persistence);
109
- const { title, referencedStashIds } = await c.req.json();
110
- const chatCount = persistence.listChats(project.id).length;
111
- const chat = {
112
- id: `chat_${crypto.randomUUID().substring(0, 8)}`,
113
- projectId: project.id,
114
- title: title?.trim() || `Chat ${chatCount + 1}`,
115
- referencedStashIds: referencedStashIds ?? [],
116
- createdAt: new Date().toISOString(),
117
- updatedAt: new Date().toISOString()
118
- };
119
- persistence.saveChat(chat);
120
- return c.json({ data: chat }, 201);
121
- });
122
- app.patch("/chats/:chatId", async (c) => {
123
- const persistence = getPersistence();
124
- const project = ensureProject(persistence);
125
- const chatId = c.req.param("chatId");
126
- const chat = persistence.getChat(project.id, chatId);
127
- if (!chat)
128
- return c.json({ error: "Chat not found" }, 404);
129
- const body = await c.req.json();
130
- const updated = {
131
- ...chat,
132
- ...body.referencedStashIds !== undefined ? { referencedStashIds: body.referencedStashIds } : {},
133
- updatedAt: new Date().toISOString()
134
- };
135
- persistence.saveChat(updated);
136
- return c.json({ data: updated });
137
- });
138
- app.get("/chats/:chatId", (c) => {
139
- const persistence = getPersistence();
140
- const project = ensureProject(persistence);
141
- const chatId = c.req.param("chatId");
142
- const chat = persistence.getChat(project.id, chatId);
143
- if (!chat)
144
- return c.json({ error: "Chat not found" }, 404);
145
- const messages = persistence.getChatMessages(project.id, chatId);
146
- const refIds = new Set(chat.referencedStashIds ?? []);
147
- const stashes = persistence.listStashes(project.id).filter((s) => s.originChatId === chatId || refIds.has(s.id));
148
- return c.json({ data: { ...chat, messages, stashes } });
149
- });
150
- app.delete("/chats/:chatId", (c) => {
151
- const persistence = getPersistence();
152
- const project = ensureProject(persistence);
153
- const chatId = c.req.param("chatId");
154
- persistence.deleteChat(project.id, chatId);
155
- return c.json({ data: { deleted: chatId } });
156
- });
157
- app.get("/dev-server-status", async (c) => {
158
- const port = serverState.userDevPort;
159
- try {
160
- const res = await fetch(`http://localhost:${port}`, {
161
- method: "HEAD",
162
- signal: AbortSignal.timeout(2000)
163
- });
164
- return c.json({ up: res.status < 500, port });
165
- } catch {
166
- return c.json({ up: false, port });
167
- }
168
- });
169
- app.get("/screenshots/:filename", (c) => {
170
- const filename = c.req.param("filename");
171
- const filePath = join(serverState.projectPath, ".stashes", "screenshots", filename);
172
- if (!existsSync(filePath))
173
- return c.json({ error: "Not found" }, 404);
174
- const content = readFileSync(filePath);
175
- return new Response(content, {
176
- headers: { "content-type": "image/png", "cache-control": "no-cache" }
177
- });
178
- });
179
- function ensureProject(persistence) {
180
- const projects = persistence.listProjects();
181
- if (projects.length > 0)
182
- return projects[0];
183
- const project = {
184
- id: `proj_${crypto.randomUUID().substring(0, 8)}`,
185
- name: basename(serverState.projectPath),
186
- createdAt: new Date().toISOString(),
187
- updatedAt: new Date().toISOString()
188
- };
189
- persistence.saveProject(project);
190
- persistence.migrateOldChat(project.id);
191
- return project;
192
- }
193
- var apiRoutes = app;
59
+ import { join as join10, basename } from "path";
60
+ import { existsSync as existsSync10, readFileSync as readFileSync5 } from "fs";
194
61
 
195
62
  // ../core/dist/generation.js
196
- import { readFileSync as readFileSync3, existsSync as existsSync7 } from "fs";
63
+ import { readFileSync as readFileSync2, existsSync as existsSync7 } from "fs";
197
64
  import { join as join7 } from "path";
198
65
  var {spawn: spawn3 } = globalThis.Bun;
199
66
  import simpleGit3 from "simple-git";
200
67
 
201
68
  // ../core/dist/worktree.js
202
69
  import simpleGit from "simple-git";
203
- import { join as join3 } from "path";
204
- import { existsSync as existsSync3, rmSync, symlinkSync } from "fs";
70
+ import { join as join2 } from "path";
71
+ import { existsSync as existsSync2, rmSync, symlinkSync } from "fs";
205
72
 
206
73
  // ../core/dist/logger.js
207
- import { appendFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
208
- import { join as join2 } from "path";
74
+ import { appendFileSync, mkdirSync, existsSync } from "fs";
75
+ import { join } from "path";
209
76
  var COLORS = {
210
77
  reset: "\x1B[0m",
211
78
  dim: "\x1B[2m",
@@ -229,12 +96,12 @@ var LEVEL_LABELS = {
229
96
  };
230
97
  var logFilePath = null;
231
98
  function initLogFile(projectPath) {
232
- const logDir = join2(projectPath, ".stashes", "logs");
233
- if (!existsSync2(logDir)) {
99
+ const logDir = join(projectPath, ".stashes", "logs");
100
+ if (!existsSync(logDir)) {
234
101
  mkdirSync(logDir, { recursive: true });
235
102
  }
236
103
  const date = new Date().toISOString().substring(0, 10);
237
- logFilePath = join2(logDir, `stashes-${date}.log`);
104
+ logFilePath = join(logDir, `stashes-${date}.log`);
238
105
  }
239
106
  function ts() {
240
107
  return new Date().toISOString().substring(11, 23);
@@ -274,8 +141,8 @@ class WorktreeManager {
274
141
  this.cleanupStaleWorktrees();
275
142
  }
276
143
  async cleanupStaleWorktrees() {
277
- const worktreesDir = join3(this.projectPath, ".stashes", "worktrees");
278
- if (!existsSync3(worktreesDir))
144
+ const worktreesDir = join2(this.projectPath, ".stashes", "worktrees");
145
+ if (!existsSync2(worktreesDir))
279
146
  return;
280
147
  try {
281
148
  const { readdirSync } = await import("fs");
@@ -285,7 +152,7 @@ class WorktreeManager {
285
152
  logger.info("worktree", `skipping active screenshot worktree: ${entry}`);
286
153
  continue;
287
154
  }
288
- const worktreePath = join3(worktreesDir, entry);
155
+ const worktreePath = join2(worktreesDir, entry);
289
156
  logger.info("worktree", `cleaning up stale worktree: ${entry}`);
290
157
  try {
291
158
  await this.git.raw(["worktree", "remove", "--force", worktreePath]);
@@ -303,8 +170,8 @@ class WorktreeManager {
303
170
  }
304
171
  async createForGeneration(stashId) {
305
172
  const branch = `stashes/${stashId}`;
306
- const worktreePath = join3(this.projectPath, ".stashes", "worktrees", stashId);
307
- if (existsSync3(worktreePath)) {
173
+ const worktreePath = join2(this.projectPath, ".stashes", "worktrees", stashId);
174
+ if (existsSync2(worktreePath)) {
308
175
  await this.removeGeneration(stashId);
309
176
  }
310
177
  logger.info("worktree", `creating generation: ${stashId}`, { branch, path: worktreePath });
@@ -317,8 +184,8 @@ class WorktreeManager {
317
184
  }
318
185
  async createForVary(stashId, sourceBranch) {
319
186
  const branch = `stashes/${stashId}`;
320
- const worktreePath = join3(this.projectPath, ".stashes", "worktrees", stashId);
321
- if (existsSync3(worktreePath)) {
187
+ const worktreePath = join2(this.projectPath, ".stashes", "worktrees", stashId);
188
+ if (existsSync2(worktreePath)) {
322
189
  await this.removeGeneration(stashId);
323
190
  }
324
191
  logger.info("worktree", `creating vary: ${stashId}`, { sourceBranch, branch });
@@ -331,20 +198,20 @@ class WorktreeManager {
331
198
  }
332
199
  async removeGeneration(stashId) {
333
200
  const info = this.worktrees.get(stashId);
334
- const worktreePath = info?.path ?? join3(this.projectPath, ".stashes", "worktrees", stashId);
201
+ const worktreePath = info?.path ?? join2(this.projectPath, ".stashes", "worktrees", stashId);
335
202
  logger.info("worktree", `removing generation worktree: ${stashId}`);
336
203
  try {
337
204
  await this.git.raw(["worktree", "remove", "--force", worktreePath]);
338
205
  } catch {
339
- if (existsSync3(worktreePath)) {
206
+ if (existsSync2(worktreePath)) {
340
207
  rmSync(worktreePath, { recursive: true, force: true });
341
208
  }
342
209
  }
343
210
  this.worktrees.delete(stashId);
344
211
  }
345
212
  async createPreview() {
346
- const previewPath = join3(this.projectPath, ".stashes", "preview");
347
- if (existsSync3(previewPath)) {
213
+ const previewPath = join2(this.projectPath, ".stashes", "preview");
214
+ if (existsSync2(previewPath)) {
348
215
  try {
349
216
  await this.git.raw(["worktree", "remove", "--force", previewPath]);
350
217
  } catch {
@@ -370,11 +237,11 @@ class WorktreeManager {
370
237
  const msg = err instanceof Error ? err.message : String(err);
371
238
  if (msg.includes("is already used by worktree")) {
372
239
  logger.warn("worktree", `stale worktree blocking ${branch}, cleaning up`);
373
- const staleDir = join3(this.projectPath, ".stashes", "worktrees", stashId);
240
+ const staleDir = join2(this.projectPath, ".stashes", "worktrees", stashId);
374
241
  try {
375
242
  await this.git.raw(["worktree", "remove", "--force", staleDir]);
376
243
  } catch {
377
- if (existsSync3(staleDir)) {
244
+ if (existsSync2(staleDir)) {
378
245
  rmSync(staleDir, { recursive: true, force: true });
379
246
  }
380
247
  }
@@ -393,9 +260,9 @@ class WorktreeManager {
393
260
  return PREVIEW_PORT;
394
261
  }
395
262
  async createPreviewForPool(stashId) {
396
- const previewPath = join3(this.projectPath, ".stashes", "previews", stashId);
263
+ const previewPath = join2(this.projectPath, ".stashes", "previews", stashId);
397
264
  const branch = `stashes/${stashId}`;
398
- if (existsSync3(previewPath)) {
265
+ if (existsSync2(previewPath)) {
399
266
  try {
400
267
  await this.git.raw(["worktree", "remove", "--force", previewPath]);
401
268
  } catch {
@@ -410,10 +277,10 @@ class WorktreeManager {
410
277
  const msg = err instanceof Error ? err.message : String(err);
411
278
  if (msg.includes("is already used by worktree")) {
412
279
  logger.warn("worktree", `branch ${branch} locked by stale worktree, cleaning up`);
413
- const staleDir = join3(this.projectPath, ".stashes", "worktrees", stashId);
414
- const legacyPreview = join3(this.projectPath, ".stashes", "preview");
280
+ const staleDir = join2(this.projectPath, ".stashes", "worktrees", stashId);
281
+ const legacyPreview = join2(this.projectPath, ".stashes", "preview");
415
282
  for (const dir of [staleDir, legacyPreview]) {
416
- if (existsSync3(dir)) {
283
+ if (existsSync2(dir)) {
417
284
  try {
418
285
  await this.git.raw(["worktree", "remove", "--force", dir]);
419
286
  } catch {
@@ -432,12 +299,12 @@ class WorktreeManager {
432
299
  return previewPath;
433
300
  }
434
301
  async removePreviewForPool(stashId) {
435
- const previewPath = join3(this.projectPath, ".stashes", "previews", stashId);
302
+ const previewPath = join2(this.projectPath, ".stashes", "previews", stashId);
436
303
  logger.info("worktree", `removing pool preview: ${stashId}`);
437
304
  try {
438
305
  await this.git.raw(["worktree", "remove", "--force", previewPath]);
439
306
  } catch {
440
- if (existsSync3(previewPath)) {
307
+ if (existsSync2(previewPath)) {
441
308
  rmSync(previewPath, { recursive: true, force: true });
442
309
  }
443
310
  }
@@ -454,7 +321,7 @@ class WorktreeManager {
454
321
  try {
455
322
  await this.git.raw(["worktree", "remove", "--force", this.previewPath]);
456
323
  } catch {
457
- if (existsSync3(this.previewPath)) {
324
+ if (existsSync2(this.previewPath)) {
458
325
  rmSync(this.previewPath, { recursive: true, force: true });
459
326
  }
460
327
  }
@@ -474,16 +341,16 @@ class WorktreeManager {
474
341
  }
475
342
  }
476
343
  } catch {}
477
- const worktreesDir = join3(this.projectPath, ".stashes", "worktrees");
478
- if (existsSync3(worktreesDir)) {
344
+ const worktreesDir = join2(this.projectPath, ".stashes", "worktrees");
345
+ if (existsSync2(worktreesDir)) {
479
346
  rmSync(worktreesDir, { recursive: true, force: true });
480
347
  }
481
- const previewDir = join3(this.projectPath, ".stashes", "preview");
482
- if (existsSync3(previewDir)) {
348
+ const previewDir = join2(this.projectPath, ".stashes", "preview");
349
+ if (existsSync2(previewDir)) {
483
350
  rmSync(previewDir, { recursive: true, force: true });
484
351
  }
485
- const previewsDir = join3(this.projectPath, ".stashes", "previews");
486
- if (existsSync3(previewsDir)) {
352
+ const previewsDir = join2(this.projectPath, ".stashes", "previews");
353
+ if (existsSync2(previewsDir)) {
487
354
  rmSync(previewsDir, { recursive: true, force: true });
488
355
  }
489
356
  logger.info("worktree", `cleanup complete`);
@@ -491,9 +358,9 @@ class WorktreeManager {
491
358
  symlinkDeps(worktreePath) {
492
359
  const symlinks = ["node_modules", ".env", ".env.local"];
493
360
  for (const name of symlinks) {
494
- const source = join3(this.projectPath, name);
495
- const target = join3(worktreePath, name);
496
- if (existsSync3(source) && !existsSync3(target)) {
361
+ const source = join2(this.projectPath, name);
362
+ const target = join2(worktreePath, name);
363
+ if (existsSync2(source) && !existsSync2(target)) {
497
364
  try {
498
365
  symlinkSync(source, target);
499
366
  } catch {}
@@ -503,19 +370,19 @@ class WorktreeManager {
503
370
  }
504
371
 
505
372
  // ../core/dist/persistence.js
506
- import { readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync4, rmSync as rmSync2, readdirSync } from "fs";
507
- import { join as join4, dirname } from "path";
373
+ import { readFileSync, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync3, rmSync as rmSync2, readdirSync } from "fs";
374
+ import { join as join3, dirname } from "path";
508
375
  var STASHES_DIR = ".stashes";
509
376
  function ensureDir(dirPath) {
510
- if (!existsSync4(dirPath)) {
377
+ if (!existsSync3(dirPath)) {
511
378
  mkdirSync2(dirPath, { recursive: true });
512
379
  }
513
380
  }
514
381
  function readJson(filePath, fallback) {
515
- if (!existsSync4(filePath))
382
+ if (!existsSync3(filePath))
516
383
  return fallback;
517
384
  try {
518
- return JSON.parse(readFileSync2(filePath, "utf-8"));
385
+ return JSON.parse(readFileSync(filePath, "utf-8"));
519
386
  } catch {
520
387
  logger.warn("persistence", `failed to read ${filePath}, using fallback`);
521
388
  return fallback;
@@ -529,12 +396,12 @@ function writeJson(filePath, data) {
529
396
  class PersistenceService {
530
397
  basePath;
531
398
  constructor(projectPath) {
532
- this.basePath = join4(projectPath, STASHES_DIR);
399
+ this.basePath = join3(projectPath, STASHES_DIR);
533
400
  ensureDir(this.basePath);
534
401
  this.ensureGitignore(projectPath);
535
402
  }
536
403
  listProjects() {
537
- return readJson(join4(this.basePath, "projects.json"), []);
404
+ return readJson(join3(this.basePath, "projects.json"), []);
538
405
  }
539
406
  getProject(projectId) {
540
407
  return this.listProjects().find((p) => p.id === projectId);
@@ -547,18 +414,18 @@ class PersistenceService {
547
414
  } else {
548
415
  projects.push(project);
549
416
  }
550
- writeJson(join4(this.basePath, "projects.json"), projects);
417
+ writeJson(join3(this.basePath, "projects.json"), projects);
551
418
  }
552
419
  deleteProject(projectId) {
553
420
  const projects = this.listProjects().filter((p) => p.id !== projectId);
554
- writeJson(join4(this.basePath, "projects.json"), projects);
555
- const projectDir = join4(this.basePath, "projects", projectId);
556
- if (existsSync4(projectDir)) {
421
+ writeJson(join3(this.basePath, "projects.json"), projects);
422
+ const projectDir = join3(this.basePath, "projects", projectId);
423
+ if (existsSync3(projectDir)) {
557
424
  rmSync2(projectDir, { recursive: true, force: true });
558
425
  }
559
426
  }
560
427
  listStashes(projectId) {
561
- const filePath = join4(this.basePath, "projects", projectId, "stashes.json");
428
+ const filePath = join3(this.basePath, "projects", projectId, "stashes.json");
562
429
  return readJson(filePath, []);
563
430
  }
564
431
  getStash(projectId, stashId) {
@@ -577,67 +444,67 @@ class PersistenceService {
577
444
  } else {
578
445
  stashes.push(stash);
579
446
  }
580
- const filePath = join4(this.basePath, "projects", stash.projectId, "stashes.json");
447
+ const filePath = join3(this.basePath, "projects", stash.projectId, "stashes.json");
581
448
  writeJson(filePath, stashes);
582
449
  }
583
450
  deleteStash(projectId, stashId) {
584
451
  const stashes = this.listStashes(projectId).filter((s) => s.id !== stashId);
585
- const filePath = join4(this.basePath, "projects", projectId, "stashes.json");
452
+ const filePath = join3(this.basePath, "projects", projectId, "stashes.json");
586
453
  writeJson(filePath, stashes);
587
454
  }
588
455
  getProjectSettings(projectId) {
589
- const filePath = join4(this.basePath, "projects", projectId, "settings.json");
456
+ const filePath = join3(this.basePath, "projects", projectId, "settings.json");
590
457
  return readJson(filePath, {});
591
458
  }
592
459
  saveProjectSettings(projectId, settings) {
593
- const filePath = join4(this.basePath, "projects", projectId, "settings.json");
460
+ const filePath = join3(this.basePath, "projects", projectId, "settings.json");
594
461
  writeJson(filePath, settings);
595
462
  }
596
463
  listChats(projectId) {
597
- const dir = join4(this.basePath, "projects", projectId, "chats");
598
- if (!existsSync4(dir))
464
+ const dir = join3(this.basePath, "projects", projectId, "chats");
465
+ if (!existsSync3(dir))
599
466
  return [];
600
467
  const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
601
468
  return files.map((f) => {
602
- const data = readJson(join4(dir, f), null);
469
+ const data = readJson(join3(dir, f), null);
603
470
  return data?.chat;
604
471
  }).filter(Boolean).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
605
472
  }
606
473
  getChat(projectId, chatId) {
607
- const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
474
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
608
475
  const data = readJson(filePath, null);
609
476
  return data?.chat;
610
477
  }
611
478
  saveChat(chat) {
612
- const filePath = join4(this.basePath, "projects", chat.projectId, "chats", `${chat.id}.json`);
479
+ const filePath = join3(this.basePath, "projects", chat.projectId, "chats", `${chat.id}.json`);
613
480
  const existing = readJson(filePath, { chat, messages: [] });
614
481
  writeJson(filePath, { ...existing, chat });
615
482
  }
616
483
  deleteChat(projectId, chatId) {
617
- const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
618
- if (existsSync4(filePath)) {
484
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
485
+ if (existsSync3(filePath)) {
619
486
  rmSync2(filePath);
620
487
  }
621
488
  }
622
489
  getChatMessages(projectId, chatId) {
623
- const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
490
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
624
491
  const data = readJson(filePath, { messages: [] });
625
492
  return data.messages;
626
493
  }
627
494
  saveChatMessage(projectId, chatId, message) {
628
- const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
495
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
629
496
  const data = readJson(filePath, { chat: this.getChat(projectId, chatId), messages: [] });
630
497
  writeJson(filePath, { ...data, messages: [...data.messages, message] });
631
498
  }
632
499
  updateChatMessage(projectId, chatId, messageId, patch) {
633
- const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
500
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
634
501
  const data = readJson(filePath, { chat: this.getChat(projectId, chatId), messages: [] });
635
502
  const messages = data.messages.map((m) => m.id === messageId ? { ...m, ...patch } : m);
636
503
  writeJson(filePath, { ...data, messages });
637
504
  }
638
505
  migrateOldChat(projectId) {
639
- const oldPath = join4(this.basePath, "projects", projectId, "chat.json");
640
- if (!existsSync4(oldPath))
506
+ const oldPath = join3(this.basePath, "projects", projectId, "chat.json");
507
+ if (!existsSync3(oldPath))
641
508
  return null;
642
509
  const messages = readJson(oldPath, []);
643
510
  if (messages.length === 0) {
@@ -652,16 +519,16 @@ class PersistenceService {
652
519
  createdAt: messages[0].createdAt,
653
520
  updatedAt: messages[messages.length - 1].createdAt
654
521
  };
655
- const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
522
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
656
523
  writeJson(filePath, { chat, messages });
657
524
  rmSync2(oldPath);
658
525
  logger.info("persistence", `migrated old chat.json \u2192 ${chatId}`);
659
526
  return chatId;
660
527
  }
661
528
  ensureGitignore(projectPath) {
662
- const gitignorePath = join4(projectPath, ".gitignore");
663
- if (existsSync4(gitignorePath)) {
664
- const content = readFileSync2(gitignorePath, "utf-8");
529
+ const gitignorePath = join3(projectPath, ".gitignore");
530
+ if (existsSync3(gitignorePath)) {
531
+ const content = readFileSync(gitignorePath, "utf-8");
665
532
  if (!content.includes(".stashes/")) {
666
533
  writeFileSync(gitignorePath, content.trimEnd() + `
667
534
  .stashes/
@@ -677,37 +544,68 @@ class PersistenceService {
677
544
 
678
545
  // ../core/dist/ai-process.js
679
546
  var {spawn } = globalThis.Bun;
547
+ import { writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
548
+ import { join as join4 } from "path";
549
+ import { tmpdir } from "os";
680
550
  var CLAUDE_BIN = "/opt/homebrew/bin/claude";
551
+ var DEFAULT_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
552
+ function getPlaywrightMcpConfigPath() {
553
+ const configDir = join4(tmpdir(), "stashes-mcp");
554
+ const configPath = join4(configDir, "playwright.json");
555
+ if (!existsSync4(configPath)) {
556
+ mkdirSync3(configDir, { recursive: true });
557
+ writeFileSync2(configPath, JSON.stringify({
558
+ mcpServers: {
559
+ playwright: { command: "npx", args: ["@playwright/mcp@latest"] }
560
+ }
561
+ }), "utf-8");
562
+ }
563
+ return configPath;
564
+ }
681
565
  var processes = new Map;
682
- function startAiProcess(id, prompt, cwd, resumeSessionId, model) {
683
- killAiProcess(id);
684
- logger.info("claude", `spawning process: ${id}`, {
685
- cwd,
686
- promptLength: prompt.length,
687
- promptPreview: prompt.substring(0, 100),
688
- resumeSessionId,
689
- model
566
+ function startAiProcess(idOrOpts, prompt, cwd, resumeSessionId, model) {
567
+ const opts = typeof idOrOpts === "string" ? { id: idOrOpts, prompt, cwd, resumeSessionId, model } : idOrOpts;
568
+ const bare = opts.bare ?? true;
569
+ const tools = opts.tools ?? DEFAULT_TOOLS;
570
+ killAiProcess(opts.id);
571
+ logger.info("claude", `spawning process: ${opts.id}`, {
572
+ cwd: opts.cwd,
573
+ promptLength: opts.prompt.length,
574
+ promptPreview: opts.prompt.substring(0, 100),
575
+ resumeSessionId: opts.resumeSessionId,
576
+ model: opts.model,
577
+ bare,
578
+ tools: bare ? tools.join(",") : "all"
690
579
  });
691
- const cmd = [CLAUDE_BIN, "-p", prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
692
- if (resumeSessionId) {
693
- cmd.push("--resume", resumeSessionId);
580
+ const cmd = [CLAUDE_BIN, "-p", opts.prompt, "--output-format=stream-json", "--verbose", "--dangerously-skip-permissions"];
581
+ if (bare) {
582
+ cmd.push("--bare");
583
+ if (tools.length > 0) {
584
+ cmd.push("--tools", tools.join(","));
585
+ }
586
+ if (opts.mcpConfigPath) {
587
+ cmd.push("--mcp-config", opts.mcpConfigPath);
588
+ }
694
589
  }
695
- if (model) {
696
- cmd.push("--model", model);
590
+ if (opts.resumeSessionId) {
591
+ cmd.push("--resume", opts.resumeSessionId);
592
+ }
593
+ if (opts.model) {
594
+ cmd.push("--model", opts.model);
697
595
  }
698
596
  const proc = spawn({
699
597
  cmd,
700
598
  stdin: "ignore",
701
599
  stdout: "pipe",
702
600
  stderr: "pipe",
703
- cwd,
601
+ cwd: opts.cwd,
704
602
  env: { ...process.env }
705
603
  });
706
604
  proc.exited.then((code) => {
707
- logger.info("claude", `process exited: ${id}`, { exitCode: code });
605
+ logger.info("claude", `process exited: ${opts.id}`, { exitCode: code });
708
606
  });
709
- const aiProcess = { process: proc, id };
710
- processes.set(id, aiProcess);
607
+ const aiProcess = { process: proc, id: opts.id };
608
+ processes.set(opts.id, aiProcess);
711
609
  return aiProcess;
712
610
  }
713
611
  function killAiProcess(id) {
@@ -722,8 +620,8 @@ function killAiProcess(id) {
722
620
  }
723
621
  return false;
724
622
  }
725
- var toolNameMap = new Map;
726
623
  async function* parseClaudeStream(proc) {
624
+ const toolNameMap = new Map;
727
625
  const stdout = proc.stdout;
728
626
  if (!stdout || typeof stdout === "number") {
729
627
  throw new Error("Process stdout is not a readable stream");
@@ -820,18 +718,18 @@ async function* parseClaudeStream(proc) {
820
718
 
821
719
  // ../core/dist/smart-screenshot.js
822
720
  import { join as join6 } from "path";
823
- import { mkdirSync as mkdirSync4, existsSync as existsSync6 } from "fs";
721
+ import { mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
824
722
  import simpleGit2 from "simple-git";
825
723
 
826
724
  // ../core/dist/screenshot.js
827
725
  var {spawn: spawn2 } = globalThis.Bun;
828
726
  import { join as join5 } from "path";
829
- import { mkdirSync as mkdirSync3, existsSync as existsSync5 } from "fs";
727
+ import { mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
830
728
  var SCREENSHOTS_DIR = ".stashes/screenshots";
831
729
  async function captureScreenshot(port, projectPath, stashId) {
832
730
  const screenshotsDir = join5(projectPath, SCREENSHOTS_DIR);
833
731
  if (!existsSync5(screenshotsDir)) {
834
- mkdirSync3(screenshotsDir, { recursive: true });
732
+ mkdirSync4(screenshotsDir, { recursive: true });
835
733
  }
836
734
  const filename = `${stashId}.png`;
837
735
  const outputPath = join5(screenshotsDir, filename);
@@ -891,14 +789,8 @@ ${truncatedDiff}`;
891
789
  }
892
790
  function buildScreenshotPrompt(port, diff, screenshotDir, stashId) {
893
791
  return [
894
- "You are a screenshot assistant. Your ONLY job is to take screenshots of a running web app using Playwright MCP tools.",
895
- "",
896
- "IMPORTANT RULES:",
897
- "- ONLY use these Playwright MCP tools: mcp__playwright__browser_navigate, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_click, mcp__playwright__browser_snapshot",
898
- "- Do NOT use any other tools (no Bash, Read, Grep, Agent, ToolSearch, etc.)",
899
- "- Do NOT start any sessions or call useai tools",
900
- "- Do NOT read or analyze code files \u2014 the diff below tells you everything",
901
- "- Be fast \u2014 you have a strict time limit",
792
+ "You are a screenshot assistant. Take screenshots of a running web app using Playwright MCP tools.",
793
+ "Be fast \u2014 you have a strict time limit.",
902
794
  "",
903
795
  `## The app is running at: http://localhost:${port}`,
904
796
  "",
@@ -965,7 +857,7 @@ async function captureSmartScreenshots(opts) {
965
857
  const { projectPath, stashId, stashBranch, parentBranch, worktreePath, port, model = "sonnet", timeout = DEFAULT_TIMEOUT } = opts;
966
858
  const screenshotDir = join6(projectPath, SCREENSHOTS_DIR2);
967
859
  if (!existsSync6(screenshotDir)) {
968
- mkdirSync4(screenshotDir, { recursive: true });
860
+ mkdirSync5(screenshotDir, { recursive: true });
969
861
  }
970
862
  const diff = await getStashDiff(worktreePath, parentBranch);
971
863
  if (!diff) {
@@ -975,7 +867,15 @@ async function captureSmartScreenshots(opts) {
975
867
  const processId = `screenshot-ai-${stashId}`;
976
868
  const prompt = buildScreenshotPrompt(port, diff, screenshotDir, stashId);
977
869
  const modelFlag = model === "sonnet" ? "sonnet" : "haiku";
978
- const aiProcess = startAiProcess(processId, prompt, worktreePath, undefined, modelFlag);
870
+ const aiProcess = startAiProcess({
871
+ id: processId,
872
+ prompt,
873
+ cwd: worktreePath,
874
+ model: modelFlag,
875
+ bare: true,
876
+ tools: [],
877
+ mcpConfigPath: getPlaywrightMcpConfigPath()
878
+ });
979
879
  let textOutput = "";
980
880
  let timedOut = false;
981
881
  const timeoutId = setTimeout(() => {
@@ -1103,7 +1003,7 @@ async function generate(opts) {
1103
1003
  if (component?.filePath) {
1104
1004
  const sourceFile = join7(projectPath, component.filePath);
1105
1005
  if (existsSync7(sourceFile)) {
1106
- sourceCode = readFileSync3(sourceFile, "utf-8");
1006
+ sourceCode = readFileSync2(sourceFile, "utf-8");
1107
1007
  }
1108
1008
  }
1109
1009
  const completedStashes = [];
@@ -1138,7 +1038,12 @@ async function generate(opts) {
1138
1038
  } else {
1139
1039
  stashPrompt = buildFreeformStashPrompt(prompt, directive);
1140
1040
  }
1141
- const aiProcess = startAiProcess(stashId, stashPrompt, worktree.path);
1041
+ const aiProcess = startAiProcess({
1042
+ id: stashId,
1043
+ prompt: stashPrompt,
1044
+ cwd: worktree.path,
1045
+ bare: false
1046
+ });
1142
1047
  try {
1143
1048
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
1144
1049
  emit(onProgress, {
@@ -1147,14 +1052,63 @@ async function generate(opts) {
1147
1052
  content: chunk.content,
1148
1053
  streamType: chunk.type
1149
1054
  });
1055
+ if (chunk.type === "tool_use" && chunk.toolName) {
1056
+ const knownTools = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
1057
+ if (knownTools.includes(chunk.toolName)) {
1058
+ const filePath = chunk.toolInput?.file_path ?? chunk.toolInput?.path ?? chunk.toolInput?.command ?? undefined;
1059
+ const lines = chunk.toolInput?.content ? chunk.toolInput.content.split(`
1060
+ `).length : undefined;
1061
+ emit(onProgress, {
1062
+ type: "activity",
1063
+ stashId,
1064
+ action: chunk.toolName,
1065
+ file: filePath,
1066
+ lines,
1067
+ timestamp: Date.now()
1068
+ });
1069
+ }
1070
+ } else if (chunk.type === "thinking") {
1071
+ emit(onProgress, {
1072
+ type: "activity",
1073
+ stashId,
1074
+ action: "thinking",
1075
+ content: chunk.content.substring(0, 200),
1076
+ timestamp: Date.now()
1077
+ });
1078
+ } else if (chunk.type === "text") {
1079
+ emit(onProgress, {
1080
+ type: "activity",
1081
+ stashId,
1082
+ action: "text",
1083
+ content: chunk.content.substring(0, 200),
1084
+ timestamp: Date.now()
1085
+ });
1086
+ }
1150
1087
  }
1151
1088
  await aiProcess.process.exited;
1152
1089
  const wtGit = simpleGit3(worktree.path);
1090
+ let hasChanges = false;
1153
1091
  try {
1154
1092
  await wtGit.add("-A");
1155
- await wtGit.commit(`stashes: stash ${stashId}`);
1156
- } catch {}
1093
+ const status = await wtGit.status();
1094
+ if (status.staged.length > 0) {
1095
+ await wtGit.commit(`stashes: stash ${stashId}`);
1096
+ hasChanges = true;
1097
+ logger.info("generation", `committed changes for ${stashId}`, { files: status.staged.length });
1098
+ } else {
1099
+ logger.warn("generation", `AI produced no file changes for ${stashId} \u2014 skipping`);
1100
+ }
1101
+ } catch (commitErr) {
1102
+ logger.warn("generation", `commit failed for ${stashId}`, {
1103
+ error: commitErr instanceof Error ? commitErr.message : String(commitErr)
1104
+ });
1105
+ }
1157
1106
  await worktreeManager.removeGeneration(stashId);
1107
+ if (!hasChanges) {
1108
+ persistence.saveStash({ ...stash, status: "error", error: "AI generation produced no file changes" });
1109
+ emit(onProgress, { type: "error", stashId, error: "AI generation produced no file changes" });
1110
+ return;
1111
+ }
1158
1112
  const generatedStash = { ...stash, status: "screenshotting" };
1159
1113
  completedStashes.push(generatedStash);
1160
1114
  persistence.saveStash(generatedStash);
@@ -1285,7 +1239,12 @@ async function vary(opts) {
1285
1239
  persistence.saveStash(stash);
1286
1240
  emit2(onProgress, { type: "generating", stashId, number: stashNumber });
1287
1241
  const varyPrompt = `The user wants to vary the current UI. Apply this change: ${prompt}`;
1288
- const aiProcess = startAiProcess(stashId, varyPrompt, worktree.path);
1242
+ const aiProcess = startAiProcess({
1243
+ id: stashId,
1244
+ prompt: varyPrompt,
1245
+ cwd: worktree.path,
1246
+ bare: false
1247
+ });
1289
1248
  try {
1290
1249
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
1291
1250
  emit2(onProgress, {
@@ -1297,11 +1256,29 @@ async function vary(opts) {
1297
1256
  }
1298
1257
  await aiProcess.process.exited;
1299
1258
  const wtGit = simpleGit4(worktree.path);
1259
+ let hasChanges = false;
1300
1260
  try {
1301
1261
  await wtGit.add("-A");
1302
- await wtGit.commit(`stashes: vary ${stashId} from ${sourceStashId}`);
1303
- } catch {}
1262
+ const status = await wtGit.status();
1263
+ if (status.staged.length > 0) {
1264
+ await wtGit.commit(`stashes: vary ${stashId} from ${sourceStashId}`);
1265
+ hasChanges = true;
1266
+ logger.info("vary", `committed changes for ${stashId}`, { files: status.staged.length });
1267
+ } else {
1268
+ logger.warn("vary", `AI produced no file changes for ${stashId}`);
1269
+ }
1270
+ } catch (commitErr) {
1271
+ logger.warn("vary", `commit failed for ${stashId}`, {
1272
+ error: commitErr instanceof Error ? commitErr.message : String(commitErr)
1273
+ });
1274
+ }
1304
1275
  await worktreeManager.removeGeneration(stashId);
1276
+ if (!hasChanges) {
1277
+ const errorStash = { ...stash, status: "error", error: "AI generation produced no file changes" };
1278
+ persistence.saveStash(errorStash);
1279
+ emit2(onProgress, { type: "error", stashId, error: "AI generation produced no file changes" });
1280
+ return errorStash;
1281
+ }
1305
1282
  persistence.saveStash({ ...stash, status: "screenshotting" });
1306
1283
  emit2(onProgress, { type: "screenshotting", stashId });
1307
1284
  let screenshotPath = "";
@@ -1399,7 +1376,7 @@ async function cleanup(projectPath) {
1399
1376
  logger.info("manage", "cleanup complete");
1400
1377
  }
1401
1378
  // ../server/dist/services/stash-service.js
1402
- import { readFileSync as readFileSync4, existsSync as existsSync8 } from "fs";
1379
+ import { readFileSync as readFileSync3, existsSync as existsSync8 } from "fs";
1403
1380
  import { join as join8 } from "path";
1404
1381
 
1405
1382
  // ../server/dist/services/app-proxy.js
@@ -1883,6 +1860,7 @@ class StashService {
1883
1860
  worktreeManager;
1884
1861
  persistence;
1885
1862
  broadcast;
1863
+ activityStore;
1886
1864
  previewPool;
1887
1865
  selectedComponent = null;
1888
1866
  messageQueue = [];
@@ -1892,11 +1870,12 @@ class StashService {
1892
1870
  stashPollTimer = null;
1893
1871
  knownStashIds = new Set;
1894
1872
  pendingComponentResolve = null;
1895
- constructor(projectPath, worktreeManager, persistence, broadcast, stashPortStart, stashPortEnd) {
1873
+ constructor(projectPath, worktreeManager, persistence, broadcast, activityStore, stashPortStart, stashPortEnd) {
1896
1874
  this.projectPath = projectPath;
1897
1875
  this.worktreeManager = worktreeManager;
1898
1876
  this.persistence = persistence;
1899
1877
  this.broadcast = broadcast;
1878
+ this.activityStore = activityStore;
1900
1879
  this.previewPool = new PreviewPool(worktreeManager, broadcast, undefined, undefined, stashPortStart, stashPortEnd);
1901
1880
  }
1902
1881
  getActiveChatId() {
@@ -1930,7 +1909,14 @@ class StashService {
1930
1909
  "Reply with ONLY the file path relative to the project root."
1931
1910
  ].join(`
1932
1911
  `);
1933
- const aiProcess = startAiProcess("resolve-component", prompt, this.projectPath, undefined, "claude-haiku-4-5-20251001");
1912
+ const aiProcess = startAiProcess({
1913
+ id: "resolve-component",
1914
+ prompt,
1915
+ cwd: this.projectPath,
1916
+ model: "claude-haiku-4-5-20251001",
1917
+ bare: true,
1918
+ tools: ["Read", "Grep", "Glob", "Bash"]
1919
+ });
1934
1920
  let resolvedPath = "";
1935
1921
  try {
1936
1922
  for await (const chunk of parseClaudeStream(aiProcess.process)) {
@@ -1993,7 +1979,7 @@ class StashService {
1993
1979
  if (filePath && filePath !== "auto-detect") {
1994
1980
  const sourceFile = join8(this.projectPath, filePath);
1995
1981
  if (existsSync8(sourceFile)) {
1996
- sourceCode = readFileSync4(sourceFile, "utf-8");
1982
+ sourceCode = readFileSync3(sourceFile, "utf-8");
1997
1983
  }
1998
1984
  }
1999
1985
  let stashContext = "";
@@ -2040,7 +2026,13 @@ ${sourceCode.substring(0, 3000)}
2040
2026
  ].filter(Boolean).join(`
2041
2027
  `);
2042
2028
  }
2043
- const aiProcess = startAiProcess("chat", chatPrompt, this.projectPath, existingSessionId);
2029
+ const aiProcess = startAiProcess({
2030
+ id: "chat",
2031
+ prompt: chatPrompt,
2032
+ cwd: this.projectPath,
2033
+ resumeSessionId: existingSessionId,
2034
+ bare: false
2035
+ });
2044
2036
  let thinkingBuf = "";
2045
2037
  let textBuf = "";
2046
2038
  const now = new Date().toISOString();
@@ -2116,6 +2108,21 @@ ${sourceCode.substring(0, 3000)}
2116
2108
  }
2117
2109
  } else if (chunk.type === "tool_result") {
2118
2110
  this.stopStashPoll();
2111
+ let stashActivity;
2112
+ const toolNameForSnapshot = chunk.toolName ?? "";
2113
+ if (toolNameForSnapshot.includes("stashes_generate") || toolNameForSnapshot.includes("stashes_vary")) {
2114
+ const projectId2 = this.persistence.listProjects()[0]?.id ?? "";
2115
+ const allStashes = this.persistence.listStashes(projectId2);
2116
+ stashActivity = {};
2117
+ for (const s of allStashes) {
2118
+ if (this.activityStore.has(s.id)) {
2119
+ stashActivity[s.id] = this.activityStore.getSnapshot(s.id);
2120
+ this.activityStore.clear(s.id);
2121
+ }
2122
+ }
2123
+ if (Object.keys(stashActivity).length === 0)
2124
+ stashActivity = undefined;
2125
+ }
2119
2126
  let toolResult = chunk.content;
2120
2127
  let isError = false;
2121
2128
  try {
@@ -2130,6 +2137,7 @@ ${sourceCode.substring(0, 3000)}
2130
2137
  type: "tool_end",
2131
2138
  toolStatus: isError ? "error" : "completed",
2132
2139
  toolResult: toolResult.substring(0, 300),
2140
+ stashActivity,
2133
2141
  createdAt: now
2134
2142
  });
2135
2143
  this.broadcast({
@@ -2319,10 +2327,77 @@ ${refDescriptions.join(`
2319
2327
  }
2320
2328
  }
2321
2329
 
2330
+ // ../server/dist/services/activity-store.js
2331
+ import { appendFileSync as appendFileSync2, readFileSync as readFileSync4, existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync3 } from "fs";
2332
+ import { join as join9, dirname as dirname2 } from "path";
2333
+
2334
+ class ActivityStore {
2335
+ cache = new Map;
2336
+ projectPath;
2337
+ constructor(projectPath) {
2338
+ this.projectPath = projectPath;
2339
+ }
2340
+ jsonlPath(stashId) {
2341
+ return join9(this.projectPath, ".stashes", "activity", `${stashId}.jsonl`);
2342
+ }
2343
+ append(event) {
2344
+ const existing = this.cache.get(event.stashId) ?? [];
2345
+ existing.push(event);
2346
+ this.cache.set(event.stashId, existing);
2347
+ const filePath = this.jsonlPath(event.stashId);
2348
+ const dir = dirname2(filePath);
2349
+ if (!existsSync9(dir))
2350
+ mkdirSync6(dir, { recursive: true });
2351
+ appendFileSync2(filePath, JSON.stringify(event) + `
2352
+ `, "utf-8");
2353
+ }
2354
+ getEvents(stashId) {
2355
+ const cached = this.cache.get(stashId);
2356
+ if (cached && cached.length > 0)
2357
+ return cached;
2358
+ const filePath = this.jsonlPath(stashId);
2359
+ if (!existsSync9(filePath))
2360
+ return [];
2361
+ const lines = readFileSync4(filePath, "utf-8").trim().split(`
2362
+ `).filter(Boolean);
2363
+ const events = lines.map((line) => JSON.parse(line));
2364
+ this.cache.set(stashId, events);
2365
+ return events;
2366
+ }
2367
+ getSnapshot(stashId) {
2368
+ const actions = this.getEvents(stashId);
2369
+ const fileActions = actions.filter((a) => ["Read", "Write", "Edit", "Glob", "Grep", "Bash"].includes(a.action));
2370
+ const uniqueFiles = new Set(fileActions.filter((a) => a.file).map((a) => a.file));
2371
+ const timestamps = actions.map((a) => a.timestamp);
2372
+ const duration = timestamps.length > 1 ? Math.round((Math.max(...timestamps) - Math.min(...timestamps)) / 1000) : 0;
2373
+ return {
2374
+ actions: [...actions],
2375
+ stats: {
2376
+ filesChanged: uniqueFiles.size,
2377
+ duration,
2378
+ totalActions: actions.length
2379
+ }
2380
+ };
2381
+ }
2382
+ clear(stashId) {
2383
+ this.cache.delete(stashId);
2384
+ const filePath = this.jsonlPath(stashId);
2385
+ if (existsSync9(filePath)) {
2386
+ rmSync3(filePath);
2387
+ }
2388
+ }
2389
+ has(stashId) {
2390
+ if (this.cache.has(stashId) && (this.cache.get(stashId)?.length ?? 0) > 0)
2391
+ return true;
2392
+ return existsSync9(this.jsonlPath(stashId));
2393
+ }
2394
+ }
2395
+
2322
2396
  // ../server/dist/services/websocket.js
2323
2397
  var worktreeManager;
2324
2398
  var stashService;
2325
2399
  var persistence;
2400
+ var activityStore;
2326
2401
  var clients = new Set;
2327
2402
  function broadcast(event) {
2328
2403
  const data = JSON.stringify(event);
@@ -2333,10 +2408,14 @@ function broadcast(event) {
2333
2408
  function getPersistenceFromWs() {
2334
2409
  return persistence;
2335
2410
  }
2411
+ function getActivityStoreFromWs() {
2412
+ return activityStore;
2413
+ }
2336
2414
  function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPortStart, stashPortEnd) {
2337
2415
  worktreeManager = new WorktreeManager(projectPath);
2338
2416
  persistence = new PersistenceService(projectPath);
2339
- stashService = new StashService(projectPath, worktreeManager, persistence, broadcast, stashPortStart, stashPortEnd);
2417
+ activityStore = new ActivityStore(projectPath);
2418
+ stashService = new StashService(projectPath, worktreeManager, persistence, broadcast, activityStore, stashPortStart, stashPortEnd);
2340
2419
  return {
2341
2420
  open(ws) {
2342
2421
  clients.add(ws);
@@ -2353,6 +2432,15 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPor
2353
2432
  if (activeChatId) {
2354
2433
  ws.send(JSON.stringify({ type: "processing", chatId: activeChatId }));
2355
2434
  }
2435
+ const allStashes = persistence.listStashes(project.id);
2436
+ for (const stash of allStashes) {
2437
+ if (stash.status === "generating" && activityStore.has(stash.id)) {
2438
+ const events = activityStore.getEvents(stash.id);
2439
+ for (const event of events) {
2440
+ ws.send(JSON.stringify({ type: "stash:activity", stashId: stash.id, event }));
2441
+ }
2442
+ }
2443
+ }
2356
2444
  },
2357
2445
  async message(ws, message) {
2358
2446
  const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
@@ -2432,6 +2520,156 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort, stashPor
2432
2520
  };
2433
2521
  }
2434
2522
 
2523
+ // ../server/dist/routes/api.js
2524
+ var app = new Hono;
2525
+ app.get("/health", (c) => c.json({ status: "ok", service: "stashes" }));
2526
+ app.get("/projects", (c) => {
2527
+ const persistence2 = getPersistence();
2528
+ const projects = persistence2.listProjects();
2529
+ const projectsWithCounts = projects.map((p) => ({
2530
+ ...p,
2531
+ stashCount: persistence2.listStashes(p.id).length,
2532
+ recentScreenshots: persistence2.listStashes(p.id).filter((s) => s.screenshotUrl).slice(-4).map((s) => s.screenshotUrl)
2533
+ }));
2534
+ return c.json({ data: projectsWithCounts });
2535
+ });
2536
+ app.post("/projects", async (c) => {
2537
+ const { name, description } = await c.req.json();
2538
+ const project = {
2539
+ id: `proj_${crypto.randomUUID().substring(0, 8)}`,
2540
+ name,
2541
+ description,
2542
+ createdAt: new Date().toISOString(),
2543
+ updatedAt: new Date().toISOString()
2544
+ };
2545
+ getPersistence().saveProject(project);
2546
+ return c.json({ data: project }, 201);
2547
+ });
2548
+ app.get("/projects/:id", (c) => {
2549
+ const persistence2 = getPersistence();
2550
+ const project = persistence2.getProject(c.req.param("id"));
2551
+ if (!project)
2552
+ return c.json({ error: "Project not found" }, 404);
2553
+ const stashes = persistence2.listStashes(project.id);
2554
+ const chats = persistence2.listChats(project.id);
2555
+ return c.json({ data: { ...project, stashes, chats } });
2556
+ });
2557
+ app.delete("/projects/:id", (c) => {
2558
+ const id = c.req.param("id");
2559
+ getPersistence().deleteProject(id);
2560
+ return c.json({ data: { deleted: id } });
2561
+ });
2562
+ app.get("/chats", (c) => {
2563
+ const persistence2 = getPersistence();
2564
+ const project = ensureProject(persistence2);
2565
+ const chats = persistence2.listChats(project.id);
2566
+ const stashes = persistence2.listStashes(project.id);
2567
+ return c.json({ data: { project, chats, stashes } });
2568
+ });
2569
+ app.post("/chats", async (c) => {
2570
+ const persistence2 = getPersistence();
2571
+ const project = ensureProject(persistence2);
2572
+ const { title, referencedStashIds } = await c.req.json();
2573
+ const chatCount = persistence2.listChats(project.id).length;
2574
+ const chat = {
2575
+ id: `chat_${crypto.randomUUID().substring(0, 8)}`,
2576
+ projectId: project.id,
2577
+ title: title?.trim() || `Chat ${chatCount + 1}`,
2578
+ referencedStashIds: referencedStashIds ?? [],
2579
+ createdAt: new Date().toISOString(),
2580
+ updatedAt: new Date().toISOString()
2581
+ };
2582
+ persistence2.saveChat(chat);
2583
+ return c.json({ data: chat }, 201);
2584
+ });
2585
+ app.patch("/chats/:chatId", async (c) => {
2586
+ const persistence2 = getPersistence();
2587
+ const project = ensureProject(persistence2);
2588
+ const chatId = c.req.param("chatId");
2589
+ const chat = persistence2.getChat(project.id, chatId);
2590
+ if (!chat)
2591
+ return c.json({ error: "Chat not found" }, 404);
2592
+ const body = await c.req.json();
2593
+ const updated = {
2594
+ ...chat,
2595
+ ...body.referencedStashIds !== undefined ? { referencedStashIds: body.referencedStashIds } : {},
2596
+ updatedAt: new Date().toISOString()
2597
+ };
2598
+ persistence2.saveChat(updated);
2599
+ return c.json({ data: updated });
2600
+ });
2601
+ app.get("/chats/:chatId", (c) => {
2602
+ const persistence2 = getPersistence();
2603
+ const project = ensureProject(persistence2);
2604
+ const chatId = c.req.param("chatId");
2605
+ const chat = persistence2.getChat(project.id, chatId);
2606
+ if (!chat)
2607
+ return c.json({ error: "Chat not found" }, 404);
2608
+ const messages = persistence2.getChatMessages(project.id, chatId);
2609
+ const refIds = new Set(chat.referencedStashIds ?? []);
2610
+ const stashes = persistence2.listStashes(project.id).filter((s) => s.originChatId === chatId || refIds.has(s.id));
2611
+ return c.json({ data: { ...chat, messages, stashes } });
2612
+ });
2613
+ app.delete("/chats/:chatId", (c) => {
2614
+ const persistence2 = getPersistence();
2615
+ const project = ensureProject(persistence2);
2616
+ const chatId = c.req.param("chatId");
2617
+ persistence2.deleteChat(project.id, chatId);
2618
+ return c.json({ data: { deleted: chatId } });
2619
+ });
2620
+ app.get("/dev-server-status", async (c) => {
2621
+ const port = serverState.userDevPort;
2622
+ try {
2623
+ const res = await fetch(`http://localhost:${port}`, {
2624
+ method: "HEAD",
2625
+ signal: AbortSignal.timeout(2000)
2626
+ });
2627
+ return c.json({ up: res.status < 500, port });
2628
+ } catch {
2629
+ return c.json({ up: false, port });
2630
+ }
2631
+ });
2632
+ app.get("/screenshots/:filename", (c) => {
2633
+ const filename = c.req.param("filename");
2634
+ const filePath = join10(serverState.projectPath, ".stashes", "screenshots", filename);
2635
+ if (!existsSync10(filePath))
2636
+ return c.json({ error: "Not found" }, 404);
2637
+ const content = readFileSync5(filePath);
2638
+ return new Response(content, {
2639
+ headers: { "content-type": "image/png", "cache-control": "no-cache" }
2640
+ });
2641
+ });
2642
+ app.post("/stash-activity", async (c) => {
2643
+ const events = await c.req.json();
2644
+ const store = getActivityStoreFromWs();
2645
+ for (const event of events) {
2646
+ store.append(event);
2647
+ broadcast({ type: "stash:activity", stashId: event.stashId, event });
2648
+ }
2649
+ return c.json({ ok: true });
2650
+ });
2651
+ app.get("/stash-activity/:stashId", (c) => {
2652
+ const stashId = c.req.param("stashId");
2653
+ const store = getActivityStoreFromWs();
2654
+ const events = store.getEvents(stashId);
2655
+ return c.json({ data: events });
2656
+ });
2657
+ function ensureProject(persistence2) {
2658
+ const projects = persistence2.listProjects();
2659
+ if (projects.length > 0)
2660
+ return projects[0];
2661
+ const project = {
2662
+ id: `proj_${crypto.randomUUID().substring(0, 8)}`,
2663
+ name: basename(serverState.projectPath),
2664
+ createdAt: new Date().toISOString(),
2665
+ updatedAt: new Date().toISOString()
2666
+ };
2667
+ persistence2.saveProject(project);
2668
+ persistence2.migrateOldChat(project.id);
2669
+ return project;
2670
+ }
2671
+ var apiRoutes = app;
2672
+
2435
2673
  // ../server/dist/index.js
2436
2674
  var serverState = {
2437
2675
  projectPath: "",
@@ -2445,14 +2683,14 @@ app2.use("/*", cors());
2445
2683
  app2.route("/api", apiRoutes);
2446
2684
  app2.get("/*", async (c) => {
2447
2685
  const path = c.req.path;
2448
- const selfDir = dirname2(fileURLToPath(import.meta.url));
2449
- const bundledWebDir = join9(selfDir, "web");
2450
- const monorepoWebDir = join9(selfDir, "../../web/dist");
2451
- const webDistDir = existsSync9(join9(bundledWebDir, "index.html")) ? bundledWebDir : monorepoWebDir;
2686
+ const selfDir = dirname3(fileURLToPath(import.meta.url));
2687
+ const bundledWebDir = join11(selfDir, "web");
2688
+ const monorepoWebDir = join11(selfDir, "../../web/dist");
2689
+ const webDistDir = existsSync11(join11(bundledWebDir, "index.html")) ? bundledWebDir : monorepoWebDir;
2452
2690
  const requestPath = path === "/" ? "/index.html" : path;
2453
- const filePath = join9(webDistDir, requestPath);
2454
- if (existsSync9(filePath) && !filePath.includes("..")) {
2455
- const content = readFileSync5(filePath);
2691
+ const filePath = join11(webDistDir, requestPath);
2692
+ if (existsSync11(filePath) && !filePath.includes("..")) {
2693
+ const content = readFileSync6(filePath);
2456
2694
  const ext = filePath.split(".").pop() || "";
2457
2695
  const contentTypes = {
2458
2696
  html: "text/html; charset=utf-8",
@@ -2470,9 +2708,9 @@ app2.get("/*", async (c) => {
2470
2708
  headers: { "content-type": contentTypes[ext] || "application/octet-stream" }
2471
2709
  });
2472
2710
  }
2473
- const indexPath = join9(webDistDir, "index.html");
2474
- if (existsSync9(indexPath)) {
2475
- const html = readFileSync5(indexPath, "utf-8");
2711
+ const indexPath = join11(webDistDir, "index.html");
2712
+ if (existsSync11(indexPath)) {
2713
+ const html = readFileSync6(indexPath, "utf-8");
2476
2714
  return new Response(html, {
2477
2715
  headers: { "content-type": "text/html; charset=utf-8" }
2478
2716
  });
@@ -2532,11 +2770,11 @@ async function startServer(projectPath, userDevPort, requestedPort = STASHES_POR
2532
2770
  }
2533
2771
 
2534
2772
  // ../server/dist/services/detector.js
2535
- import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
2536
- import { join as join10 } from "path";
2773
+ import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
2774
+ import { join as join12 } from "path";
2537
2775
  function detectFramework(projectPath) {
2538
- const packageJsonPath = join10(projectPath, "package.json");
2539
- if (!existsSync10(packageJsonPath)) {
2776
+ const packageJsonPath = join12(projectPath, "package.json");
2777
+ if (!existsSync12(packageJsonPath)) {
2540
2778
  return {
2541
2779
  framework: "unknown",
2542
2780
  devCommand: "npm run dev",
@@ -2544,7 +2782,7 @@ function detectFramework(projectPath) {
2544
2782
  configFile: null
2545
2783
  };
2546
2784
  }
2547
- const packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
2785
+ const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
2548
2786
  const deps = {
2549
2787
  ...packageJson.dependencies,
2550
2788
  ...packageJson.devDependencies
@@ -2598,7 +2836,7 @@ function getDevCommand(packageJson, fallback) {
2598
2836
  }
2599
2837
  function findConfig(projectPath, candidates) {
2600
2838
  for (const candidate of candidates) {
2601
- if (existsSync10(join10(projectPath, candidate))) {
2839
+ if (existsSync12(join12(projectPath, candidate))) {
2602
2840
  return candidate;
2603
2841
  }
2604
2842
  }
@@ -2790,8 +3028,8 @@ Cleaning up all stashes and worktrees...`);
2790
3028
  }
2791
3029
 
2792
3030
  // src/commands/setup.ts
2793
- import { existsSync as existsSync11, readFileSync as readFileSync7, writeFileSync as writeFileSync2, mkdirSync as mkdirSync5 } from "fs";
2794
- import { dirname as dirname3, join as join11 } from "path";
3031
+ import { existsSync as existsSync13, readFileSync as readFileSync8, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
3032
+ import { dirname as dirname4, join as join13 } from "path";
2795
3033
  import { homedir } from "os";
2796
3034
  import * as p from "@clack/prompts";
2797
3035
  import pc from "picocolors";
@@ -2810,63 +3048,63 @@ var MCP_ENTRY_ZED = {
2810
3048
  };
2811
3049
  function buildToolDefinitions() {
2812
3050
  const home = homedir();
2813
- const appSupport = join11(home, "Library", "Application Support");
3051
+ const appSupport = join13(home, "Library", "Application Support");
2814
3052
  return [
2815
3053
  {
2816
3054
  id: "claude-code",
2817
3055
  name: "Claude Code",
2818
- configPath: join11(home, ".claude.json"),
3056
+ configPath: join13(home, ".claude.json"),
2819
3057
  serversKey: "mcpServers",
2820
3058
  format: "standard",
2821
- detect: () => existsSync11(join11(home, ".claude.json")) || existsSync11(join11(home, ".claude"))
3059
+ detect: () => existsSync13(join13(home, ".claude.json")) || existsSync13(join13(home, ".claude"))
2822
3060
  },
2823
3061
  {
2824
3062
  id: "claude-desktop",
2825
3063
  name: "Claude Desktop",
2826
- configPath: join11(appSupport, "Claude", "claude_desktop_config.json"),
3064
+ configPath: join13(appSupport, "Claude", "claude_desktop_config.json"),
2827
3065
  serversKey: "mcpServers",
2828
3066
  format: "standard",
2829
- detect: () => existsSync11(join11(appSupport, "Claude")) || existsSync11("/Applications/Claude.app")
3067
+ detect: () => existsSync13(join13(appSupport, "Claude")) || existsSync13("/Applications/Claude.app")
2830
3068
  },
2831
3069
  {
2832
3070
  id: "vscode",
2833
3071
  name: "VS Code",
2834
- configPath: join11(appSupport, "Code", "User", "mcp.json"),
3072
+ configPath: join13(appSupport, "Code", "User", "mcp.json"),
2835
3073
  serversKey: "servers",
2836
3074
  format: "standard",
2837
- detect: () => existsSync11(join11(appSupport, "Code", "User"))
3075
+ detect: () => existsSync13(join13(appSupport, "Code", "User"))
2838
3076
  },
2839
3077
  {
2840
3078
  id: "cursor",
2841
3079
  name: "Cursor",
2842
- configPath: join11(home, ".cursor", "mcp.json"),
3080
+ configPath: join13(home, ".cursor", "mcp.json"),
2843
3081
  serversKey: "mcpServers",
2844
3082
  format: "standard",
2845
- detect: () => existsSync11(join11(home, ".cursor"))
3083
+ detect: () => existsSync13(join13(home, ".cursor"))
2846
3084
  },
2847
3085
  {
2848
3086
  id: "windsurf",
2849
3087
  name: "Windsurf",
2850
- configPath: join11(home, ".codeium", "windsurf", "mcp_config.json"),
3088
+ configPath: join13(home, ".codeium", "windsurf", "mcp_config.json"),
2851
3089
  serversKey: "mcpServers",
2852
3090
  format: "standard",
2853
- detect: () => existsSync11(join11(home, ".codeium", "windsurf"))
3091
+ detect: () => existsSync13(join13(home, ".codeium", "windsurf"))
2854
3092
  },
2855
3093
  {
2856
3094
  id: "zed",
2857
3095
  name: "Zed",
2858
- configPath: join11(appSupport, "Zed", "settings.json"),
3096
+ configPath: join13(appSupport, "Zed", "settings.json"),
2859
3097
  serversKey: "context_servers",
2860
3098
  format: "zed",
2861
- detect: () => existsSync11(join11(appSupport, "Zed"))
3099
+ detect: () => existsSync13(join13(appSupport, "Zed"))
2862
3100
  }
2863
3101
  ];
2864
3102
  }
2865
3103
  function readJsonFile(path) {
2866
- if (!existsSync11(path))
3104
+ if (!existsSync13(path))
2867
3105
  return {};
2868
3106
  try {
2869
- const raw = readFileSync7(path, "utf-8").trim();
3107
+ const raw = readFileSync8(path, "utf-8").trim();
2870
3108
  if (!raw)
2871
3109
  return {};
2872
3110
  return JSON.parse(raw);
@@ -2875,8 +3113,8 @@ function readJsonFile(path) {
2875
3113
  }
2876
3114
  }
2877
3115
  function writeJsonFile(path, data) {
2878
- mkdirSync5(dirname3(path), { recursive: true });
2879
- writeFileSync2(path, JSON.stringify(data, null, 2) + `
3116
+ mkdirSync7(dirname4(path), { recursive: true });
3117
+ writeFileSync3(path, JSON.stringify(data, null, 2) + `
2880
3118
  `);
2881
3119
  }
2882
3120
  function isConfigured(tool) {
@@ -3014,16 +3252,16 @@ async function setupCommand(options) {
3014
3252
 
3015
3253
  // src/commands/update.ts
3016
3254
  import { execFileSync, execSync } from "child_process";
3017
- import { writeFileSync as writeFileSync3, unlinkSync, chmodSync, readFileSync as readFileSync8 } from "fs";
3018
- import { tmpdir } from "os";
3019
- import { join as join12, dirname as dirname4 } from "path";
3255
+ import { writeFileSync as writeFileSync4, unlinkSync, chmodSync, readFileSync as readFileSync9 } from "fs";
3256
+ import { tmpdir as tmpdir2 } from "os";
3257
+ import { join as join14, dirname as dirname5 } from "path";
3020
3258
  import { fileURLToPath as fileURLToPath2 } from "url";
3021
3259
  import * as p2 from "@clack/prompts";
3022
3260
  import pc2 from "picocolors";
3023
3261
  function getCurrentVersion() {
3024
- const selfDir = dirname4(fileURLToPath2(import.meta.url));
3025
- const pkgPath = join12(selfDir, "..", "package.json");
3026
- return JSON.parse(readFileSync8(pkgPath, "utf-8")).version;
3262
+ const selfDir = dirname5(fileURLToPath2(import.meta.url));
3263
+ const pkgPath = join14(selfDir, "..", "package.json");
3264
+ return JSON.parse(readFileSync9(pkgPath, "utf-8")).version;
3027
3265
  }
3028
3266
  function fetchLatestVersion() {
3029
3267
  try {
@@ -3099,8 +3337,8 @@ async function updateCommand() {
3099
3337
  }
3100
3338
  s.stop(`Removed from ${configuredTools.length} tool${configuredTools.length === 1 ? "" : "s"}`);
3101
3339
  }
3102
- const scriptPath = join12(tmpdir(), `stashes-update-${Date.now()}.sh`);
3103
- writeFileSync3(scriptPath, buildUpdateScript(), "utf-8");
3340
+ const scriptPath = join14(tmpdir2(), `stashes-update-${Date.now()}.sh`);
3341
+ writeFileSync4(scriptPath, buildUpdateScript(), "utf-8");
3104
3342
  chmodSync(scriptPath, 493);
3105
3343
  try {
3106
3344
  execFileSync("bash", [scriptPath], { stdio: "inherit" });
@@ -3117,9 +3355,9 @@ Update failed. Try manually:`);
3117
3355
  }
3118
3356
 
3119
3357
  // src/index.ts
3120
- var selfDir = dirname5(fileURLToPath3(import.meta.url));
3121
- var pkgPath = join13(selfDir, "..", "package.json");
3122
- var version = JSON.parse(readFileSync9(pkgPath, "utf-8")).version;
3358
+ var selfDir = dirname6(fileURLToPath3(import.meta.url));
3359
+ var pkgPath = join15(selfDir, "..", "package.json");
3360
+ var version = JSON.parse(readFileSync10(pkgPath, "utf-8")).version;
3123
3361
  var program = new Command;
3124
3362
  program.name("stashes").description("Generate AI-powered UI design explorations in your project").version(version, "-v, --version");
3125
3363
  program.command("browse", { isDefault: true }).description("Start the Stashes server and open the web UI").argument("[path]", "Project directory", ".").option("-p, --port <port>", "Stashes server port", "4000").option("-d, --dev-port <port>", "Your app dev server port (auto-detected)").option("-n, --stashes <count>", "Default stash count", "3").option("--no-open", "Do not open the browser").action(startCommand);