stashes 0.1.12 → 0.1.14

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
@@ -52,7 +52,7 @@ var DEFAULT_DIRECTIVES = [
52
52
  ];
53
53
  // ../server/dist/routes/api.js
54
54
  import { Hono } from "hono";
55
- import { join } from "path";
55
+ import { join, basename } from "path";
56
56
  import { existsSync, readFileSync } from "fs";
57
57
  var app = new Hono;
58
58
  app.get("/health", (c) => c.json({ status: "ok", service: "stashes" }));
@@ -84,14 +84,54 @@ app.get("/projects/:id", (c) => {
84
84
  if (!project)
85
85
  return c.json({ error: "Project not found" }, 404);
86
86
  const stashes = persistence.listStashes(project.id);
87
- const chat = persistence.getChatHistory(project.id);
88
- return c.json({ data: { ...project, stashes, chat } });
87
+ const chats = persistence.listChats(project.id);
88
+ return c.json({ data: { ...project, stashes, chats } });
89
89
  });
90
90
  app.delete("/projects/:id", (c) => {
91
91
  const id = c.req.param("id");
92
92
  getPersistence().deleteProject(id);
93
93
  return c.json({ data: { deleted: id } });
94
94
  });
95
+ app.get("/chats", (c) => {
96
+ const persistence = getPersistence();
97
+ const project = ensureProject(persistence);
98
+ const chats = persistence.listChats(project.id);
99
+ const stashes = persistence.listStashes(project.id);
100
+ return c.json({ data: { project, chats, stashes } });
101
+ });
102
+ app.post("/chats", async (c) => {
103
+ const persistence = getPersistence();
104
+ const project = ensureProject(persistence);
105
+ const { title } = await c.req.json();
106
+ const chatCount = persistence.listChats(project.id).length;
107
+ const chat = {
108
+ id: `chat_${crypto.randomUUID().substring(0, 8)}`,
109
+ projectId: project.id,
110
+ title: title?.trim() || `Chat ${chatCount + 1}`,
111
+ createdAt: new Date().toISOString(),
112
+ updatedAt: new Date().toISOString()
113
+ };
114
+ persistence.saveChat(chat);
115
+ return c.json({ data: chat }, 201);
116
+ });
117
+ app.get("/chats/:chatId", (c) => {
118
+ const persistence = getPersistence();
119
+ const project = ensureProject(persistence);
120
+ const chatId = c.req.param("chatId");
121
+ const chat = persistence.getChat(project.id, chatId);
122
+ if (!chat)
123
+ return c.json({ error: "Chat not found" }, 404);
124
+ const messages = persistence.getChatMessages(project.id, chatId);
125
+ const stashes = persistence.listStashes(project.id).filter((s) => s.originChatId === chatId);
126
+ return c.json({ data: { ...chat, messages, stashes } });
127
+ });
128
+ app.delete("/chats/:chatId", (c) => {
129
+ const persistence = getPersistence();
130
+ const project = ensureProject(persistence);
131
+ const chatId = c.req.param("chatId");
132
+ persistence.deleteChat(project.id, chatId);
133
+ return c.json({ data: { deleted: chatId } });
134
+ });
95
135
  app.get("/screenshots/:filename", (c) => {
96
136
  const filename = c.req.param("filename");
97
137
  const filePath = join(serverState.projectPath, ".stashes", "screenshots", filename);
@@ -102,6 +142,20 @@ app.get("/screenshots/:filename", (c) => {
102
142
  headers: { "content-type": "image/png", "cache-control": "no-cache" }
103
143
  });
104
144
  });
145
+ function ensureProject(persistence) {
146
+ const projects = persistence.listProjects();
147
+ if (projects.length > 0)
148
+ return projects[0];
149
+ const project = {
150
+ id: `proj_${crypto.randomUUID().substring(0, 8)}`,
151
+ name: basename(serverState.projectPath),
152
+ createdAt: new Date().toISOString(),
153
+ updatedAt: new Date().toISOString()
154
+ };
155
+ persistence.saveProject(project);
156
+ persistence.migrateOldChat(project.id);
157
+ return project;
158
+ }
105
159
  var apiRoutes = app;
106
160
 
107
161
  // ../core/dist/generation.js
@@ -411,7 +465,7 @@ class WorktreeManager {
411
465
  }
412
466
 
413
467
  // ../core/dist/persistence.js
414
- import { readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync4, rmSync as rmSync2 } from "fs";
468
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync4, rmSync as rmSync2, readdirSync } from "fs";
415
469
  import { join as join4, dirname } from "path";
416
470
  var STASHES_DIR = ".stashes";
417
471
  function ensureDir(dirPath) {
@@ -488,14 +542,64 @@ class PersistenceService {
488
542
  const filePath = join4(this.basePath, "projects", projectId, "stashes.json");
489
543
  writeJson(filePath, stashes);
490
544
  }
491
- getChatHistory(projectId) {
492
- const filePath = join4(this.basePath, "projects", projectId, "chat.json");
493
- return readJson(filePath, []);
494
- }
495
- saveChatMessage(projectId, message) {
496
- const messages = [...this.getChatHistory(projectId), message];
497
- const filePath = join4(this.basePath, "projects", projectId, "chat.json");
498
- writeJson(filePath, messages);
545
+ listChats(projectId) {
546
+ const dir = join4(this.basePath, "projects", projectId, "chats");
547
+ if (!existsSync4(dir))
548
+ return [];
549
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
550
+ return files.map((f) => {
551
+ const data = readJson(join4(dir, f), null);
552
+ return data?.chat;
553
+ }).filter(Boolean).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
554
+ }
555
+ getChat(projectId, chatId) {
556
+ const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
557
+ const data = readJson(filePath, null);
558
+ return data?.chat;
559
+ }
560
+ saveChat(chat) {
561
+ const filePath = join4(this.basePath, "projects", chat.projectId, "chats", `${chat.id}.json`);
562
+ const existing = readJson(filePath, { chat, messages: [] });
563
+ writeJson(filePath, { ...existing, chat });
564
+ }
565
+ deleteChat(projectId, chatId) {
566
+ const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
567
+ if (existsSync4(filePath)) {
568
+ rmSync2(filePath);
569
+ }
570
+ }
571
+ getChatMessages(projectId, chatId) {
572
+ const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
573
+ const data = readJson(filePath, { messages: [] });
574
+ return data.messages;
575
+ }
576
+ saveChatMessage(projectId, chatId, message) {
577
+ const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
578
+ const data = readJson(filePath, { chat: this.getChat(projectId, chatId), messages: [] });
579
+ writeJson(filePath, { ...data, messages: [...data.messages, message] });
580
+ }
581
+ migrateOldChat(projectId) {
582
+ const oldPath = join4(this.basePath, "projects", projectId, "chat.json");
583
+ if (!existsSync4(oldPath))
584
+ return null;
585
+ const messages = readJson(oldPath, []);
586
+ if (messages.length === 0) {
587
+ rmSync2(oldPath);
588
+ return null;
589
+ }
590
+ const chatId = `chat_${crypto.randomUUID().substring(0, 8)}`;
591
+ const chat = {
592
+ id: chatId,
593
+ projectId,
594
+ title: "Initial conversation",
595
+ createdAt: messages[0].createdAt,
596
+ updatedAt: messages[messages.length - 1].createdAt
597
+ };
598
+ const filePath = join4(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
599
+ writeJson(filePath, { chat, messages });
600
+ rmSync2(oldPath);
601
+ logger.info("persistence", `migrated old chat.json \u2192 ${chatId}`);
602
+ return chatId;
499
603
  }
500
604
  ensureGitignore(projectPath) {
501
605
  const gitignorePath = join4(projectPath, ".gitignore");
@@ -721,7 +825,7 @@ async function allocatePort() {
721
825
  throw new Error("No available ports in range 4010-4030");
722
826
  }
723
827
  async function generate(opts) {
724
- const { projectPath, projectId, prompt, component, count = DEFAULT_STASH_COUNT, directives = DEFAULT_DIRECTIVES, onProgress } = opts;
828
+ const { projectPath, projectId, chatId, prompt, component, count = DEFAULT_STASH_COUNT, directives = DEFAULT_DIRECTIVES, onProgress } = opts;
725
829
  const worktreeManager = new WorktreeManager(projectPath);
726
830
  const persistence = new PersistenceService(projectPath);
727
831
  const selectedDirectives = directives.slice(0, count);
@@ -743,6 +847,7 @@ async function generate(opts) {
743
847
  id: stashId,
744
848
  number: stashNumber,
745
849
  projectId,
850
+ originChatId: chatId,
746
851
  prompt,
747
852
  componentPath: component?.filePath,
748
853
  branch: worktree.branch,
@@ -844,7 +949,7 @@ async function allocatePort2() {
844
949
  throw new Error("No available ports in range 4010-4030");
845
950
  }
846
951
  async function vary(opts) {
847
- const { projectPath, sourceStashId, prompt, onProgress } = opts;
952
+ const { projectPath, sourceStashId, chatId, prompt, onProgress } = opts;
848
953
  const persistence = new PersistenceService(projectPath);
849
954
  const worktreeManager = new WorktreeManager(projectPath);
850
955
  let sourceStash;
@@ -867,6 +972,7 @@ async function vary(opts) {
867
972
  id: stashId,
868
973
  number: stashNumber,
869
974
  projectId,
975
+ originChatId: chatId,
870
976
  prompt,
871
977
  componentPath: sourceStash.componentPath,
872
978
  branch: worktree.branch,
@@ -1224,7 +1330,7 @@ class StashService {
1224
1330
  });
1225
1331
  }
1226
1332
  }
1227
- async message(projectId, message, referenceStashIds, componentContext) {
1333
+ async message(projectId, chatId, message, referenceStashIds, componentContext) {
1228
1334
  const component = componentContext ? { name: componentContext.name, filePath: this.selectedComponent?.filePath || "" } : this.selectedComponent;
1229
1335
  let sourceCode = "";
1230
1336
  const filePath = component?.filePath || "";
@@ -1322,7 +1428,7 @@ ${sourceCode.substring(0, 3000)}
1322
1428
  }
1323
1429
  await aiProcess.process.exited;
1324
1430
  if (fullResponse) {
1325
- this.persistence.saveChatMessage(projectId, {
1431
+ this.persistence.saveChatMessage(projectId, chatId, {
1326
1432
  id: crypto.randomUUID(),
1327
1433
  role: "assistant",
1328
1434
  content: fullResponse,
@@ -1438,7 +1544,14 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
1438
1544
  open(ws) {
1439
1545
  clients.add(ws);
1440
1546
  logger.info("ws", "client connected", { total: clients.size });
1441
- ws.send(JSON.stringify({ type: "server_ready", port: userDevPort, appProxyPort }));
1547
+ const project = ensureProject(persistence);
1548
+ ws.send(JSON.stringify({
1549
+ type: "server_ready",
1550
+ port: userDevPort,
1551
+ appProxyPort,
1552
+ projectId: project.id,
1553
+ projectName: project.name
1554
+ }));
1442
1555
  },
1443
1556
  async message(ws, message) {
1444
1557
  const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
@@ -1458,7 +1571,7 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
1458
1571
  stashService.setSelectedComponent(event.component);
1459
1572
  break;
1460
1573
  case "message":
1461
- persistence.saveChatMessage(event.projectId, {
1574
+ persistence.saveChatMessage(event.projectId, event.chatId, {
1462
1575
  id: crypto.randomUUID(),
1463
1576
  role: "user",
1464
1577
  content: event.message,
@@ -1467,7 +1580,7 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
1467
1580
  referenceStashIds: event.referenceStashIds,
1468
1581
  componentContext: event.componentContext
1469
1582
  });
1470
- await stashService.message(event.projectId, event.message, event.referenceStashIds, event.componentContext);
1583
+ await stashService.message(event.projectId, event.chatId, event.message, event.referenceStashIds, event.componentContext);
1471
1584
  break;
1472
1585
  case "interact":
1473
1586
  await stashService.switchPreview(event.stashId, event.sortedStashIds);
package/dist/mcp.js CHANGED
@@ -347,7 +347,7 @@ class WorktreeManager {
347
347
  }
348
348
 
349
349
  // ../core/dist/persistence.js
350
- import { readFileSync, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync3, rmSync as rmSync2 } from "fs";
350
+ import { readFileSync, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync3, rmSync as rmSync2, readdirSync } from "fs";
351
351
  import { join as join3, dirname } from "path";
352
352
  var STASHES_DIR = ".stashes";
353
353
  function ensureDir(dirPath) {
@@ -424,14 +424,64 @@ class PersistenceService {
424
424
  const filePath = join3(this.basePath, "projects", projectId, "stashes.json");
425
425
  writeJson(filePath, stashes);
426
426
  }
427
- getChatHistory(projectId) {
428
- const filePath = join3(this.basePath, "projects", projectId, "chat.json");
429
- return readJson(filePath, []);
427
+ listChats(projectId) {
428
+ const dir = join3(this.basePath, "projects", projectId, "chats");
429
+ if (!existsSync3(dir))
430
+ return [];
431
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
432
+ return files.map((f) => {
433
+ const data = readJson(join3(dir, f), null);
434
+ return data?.chat;
435
+ }).filter(Boolean).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
436
+ }
437
+ getChat(projectId, chatId) {
438
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
439
+ const data = readJson(filePath, null);
440
+ return data?.chat;
441
+ }
442
+ saveChat(chat) {
443
+ const filePath = join3(this.basePath, "projects", chat.projectId, "chats", `${chat.id}.json`);
444
+ const existing = readJson(filePath, { chat, messages: [] });
445
+ writeJson(filePath, { ...existing, chat });
446
+ }
447
+ deleteChat(projectId, chatId) {
448
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
449
+ if (existsSync3(filePath)) {
450
+ rmSync2(filePath);
451
+ }
430
452
  }
431
- saveChatMessage(projectId, message) {
432
- const messages = [...this.getChatHistory(projectId), message];
433
- const filePath = join3(this.basePath, "projects", projectId, "chat.json");
434
- writeJson(filePath, messages);
453
+ getChatMessages(projectId, chatId) {
454
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
455
+ const data = readJson(filePath, { messages: [] });
456
+ return data.messages;
457
+ }
458
+ saveChatMessage(projectId, chatId, message) {
459
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
460
+ const data = readJson(filePath, { chat: this.getChat(projectId, chatId), messages: [] });
461
+ writeJson(filePath, { ...data, messages: [...data.messages, message] });
462
+ }
463
+ migrateOldChat(projectId) {
464
+ const oldPath = join3(this.basePath, "projects", projectId, "chat.json");
465
+ if (!existsSync3(oldPath))
466
+ return null;
467
+ const messages = readJson(oldPath, []);
468
+ if (messages.length === 0) {
469
+ rmSync2(oldPath);
470
+ return null;
471
+ }
472
+ const chatId = `chat_${crypto.randomUUID().substring(0, 8)}`;
473
+ const chat = {
474
+ id: chatId,
475
+ projectId,
476
+ title: "Initial conversation",
477
+ createdAt: messages[0].createdAt,
478
+ updatedAt: messages[messages.length - 1].createdAt
479
+ };
480
+ const filePath = join3(this.basePath, "projects", projectId, "chats", `${chatId}.json`);
481
+ writeJson(filePath, { chat, messages });
482
+ rmSync2(oldPath);
483
+ logger.info("persistence", `migrated old chat.json \u2192 ${chatId}`);
484
+ return chatId;
435
485
  }
436
486
  ensureGitignore(projectPath) {
437
487
  const gitignorePath = join3(projectPath, ".gitignore");
@@ -657,7 +707,7 @@ async function allocatePort() {
657
707
  throw new Error("No available ports in range 4010-4030");
658
708
  }
659
709
  async function generate(opts) {
660
- const { projectPath, projectId, prompt, component, count = DEFAULT_STASH_COUNT, directives = DEFAULT_DIRECTIVES, onProgress } = opts;
710
+ const { projectPath, projectId, chatId, prompt, component, count = DEFAULT_STASH_COUNT, directives = DEFAULT_DIRECTIVES, onProgress } = opts;
661
711
  const worktreeManager = new WorktreeManager(projectPath);
662
712
  const persistence = new PersistenceService(projectPath);
663
713
  const selectedDirectives = directives.slice(0, count);
@@ -679,6 +729,7 @@ async function generate(opts) {
679
729
  id: stashId,
680
730
  number: stashNumber,
681
731
  projectId,
732
+ originChatId: chatId,
682
733
  prompt,
683
734
  componentPath: component?.filePath,
684
735
  branch: worktree.branch,
@@ -780,7 +831,7 @@ async function allocatePort2() {
780
831
  throw new Error("No available ports in range 4010-4030");
781
832
  }
782
833
  async function vary(opts) {
783
- const { projectPath, sourceStashId, prompt, onProgress } = opts;
834
+ const { projectPath, sourceStashId, chatId, prompt, onProgress } = opts;
784
835
  const persistence = new PersistenceService(projectPath);
785
836
  const worktreeManager = new WorktreeManager(projectPath);
786
837
  let sourceStash;
@@ -803,6 +854,7 @@ async function vary(opts) {
803
854
  id: stashId,
804
855
  number: stashNumber,
805
856
  projectId,
857
+ originChatId: chatId,
806
858
  prompt,
807
859
  componentPath: sourceStash.componentPath,
808
860
  branch: worktree.branch,
@@ -1059,7 +1111,7 @@ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
1059
1111
 
1060
1112
  // ../server/dist/routes/api.js
1061
1113
  import { Hono } from "hono";
1062
- import { join as join6 } from "path";
1114
+ import { join as join6, basename } from "path";
1063
1115
  import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
1064
1116
  var app = new Hono;
1065
1117
  app.get("/health", (c) => c.json({ status: "ok", service: "stashes" }));
@@ -1091,14 +1143,54 @@ app.get("/projects/:id", (c) => {
1091
1143
  if (!project)
1092
1144
  return c.json({ error: "Project not found" }, 404);
1093
1145
  const stashes = persistence.listStashes(project.id);
1094
- const chat = persistence.getChatHistory(project.id);
1095
- return c.json({ data: { ...project, stashes, chat } });
1146
+ const chats = persistence.listChats(project.id);
1147
+ return c.json({ data: { ...project, stashes, chats } });
1096
1148
  });
1097
1149
  app.delete("/projects/:id", (c) => {
1098
1150
  const id = c.req.param("id");
1099
1151
  getPersistence().deleteProject(id);
1100
1152
  return c.json({ data: { deleted: id } });
1101
1153
  });
1154
+ app.get("/chats", (c) => {
1155
+ const persistence = getPersistence();
1156
+ const project = ensureProject(persistence);
1157
+ const chats = persistence.listChats(project.id);
1158
+ const stashes = persistence.listStashes(project.id);
1159
+ return c.json({ data: { project, chats, stashes } });
1160
+ });
1161
+ app.post("/chats", async (c) => {
1162
+ const persistence = getPersistence();
1163
+ const project = ensureProject(persistence);
1164
+ const { title } = await c.req.json();
1165
+ const chatCount = persistence.listChats(project.id).length;
1166
+ const chat = {
1167
+ id: `chat_${crypto.randomUUID().substring(0, 8)}`,
1168
+ projectId: project.id,
1169
+ title: title?.trim() || `Chat ${chatCount + 1}`,
1170
+ createdAt: new Date().toISOString(),
1171
+ updatedAt: new Date().toISOString()
1172
+ };
1173
+ persistence.saveChat(chat);
1174
+ return c.json({ data: chat }, 201);
1175
+ });
1176
+ app.get("/chats/:chatId", (c) => {
1177
+ const persistence = getPersistence();
1178
+ const project = ensureProject(persistence);
1179
+ const chatId = c.req.param("chatId");
1180
+ const chat = persistence.getChat(project.id, chatId);
1181
+ if (!chat)
1182
+ return c.json({ error: "Chat not found" }, 404);
1183
+ const messages = persistence.getChatMessages(project.id, chatId);
1184
+ const stashes = persistence.listStashes(project.id).filter((s) => s.originChatId === chatId);
1185
+ return c.json({ data: { ...chat, messages, stashes } });
1186
+ });
1187
+ app.delete("/chats/:chatId", (c) => {
1188
+ const persistence = getPersistence();
1189
+ const project = ensureProject(persistence);
1190
+ const chatId = c.req.param("chatId");
1191
+ persistence.deleteChat(project.id, chatId);
1192
+ return c.json({ data: { deleted: chatId } });
1193
+ });
1102
1194
  app.get("/screenshots/:filename", (c) => {
1103
1195
  const filename = c.req.param("filename");
1104
1196
  const filePath = join6(serverState.projectPath, ".stashes", "screenshots", filename);
@@ -1109,6 +1201,20 @@ app.get("/screenshots/:filename", (c) => {
1109
1201
  headers: { "content-type": "image/png", "cache-control": "no-cache" }
1110
1202
  });
1111
1203
  });
1204
+ function ensureProject(persistence) {
1205
+ const projects = persistence.listProjects();
1206
+ if (projects.length > 0)
1207
+ return projects[0];
1208
+ const project = {
1209
+ id: `proj_${crypto.randomUUID().substring(0, 8)}`,
1210
+ name: basename(serverState.projectPath),
1211
+ createdAt: new Date().toISOString(),
1212
+ updatedAt: new Date().toISOString()
1213
+ };
1214
+ persistence.saveProject(project);
1215
+ persistence.migrateOldChat(project.id);
1216
+ return project;
1217
+ }
1112
1218
  var apiRoutes = app;
1113
1219
 
1114
1220
  // ../server/dist/services/stash-service.js
@@ -1354,7 +1460,7 @@ class StashService {
1354
1460
  });
1355
1461
  }
1356
1462
  }
1357
- async message(projectId, message, referenceStashIds, componentContext) {
1463
+ async message(projectId, chatId, message, referenceStashIds, componentContext) {
1358
1464
  const component = componentContext ? { name: componentContext.name, filePath: this.selectedComponent?.filePath || "" } : this.selectedComponent;
1359
1465
  let sourceCode = "";
1360
1466
  const filePath = component?.filePath || "";
@@ -1452,7 +1558,7 @@ ${sourceCode.substring(0, 3000)}
1452
1558
  }
1453
1559
  await aiProcess.process.exited;
1454
1560
  if (fullResponse) {
1455
- this.persistence.saveChatMessage(projectId, {
1561
+ this.persistence.saveChatMessage(projectId, chatId, {
1456
1562
  id: crypto.randomUUID(),
1457
1563
  role: "assistant",
1458
1564
  content: fullResponse,
@@ -1568,7 +1674,14 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
1568
1674
  open(ws) {
1569
1675
  clients.add(ws);
1570
1676
  logger.info("ws", "client connected", { total: clients.size });
1571
- ws.send(JSON.stringify({ type: "server_ready", port: userDevPort, appProxyPort }));
1677
+ const project = ensureProject(persistence);
1678
+ ws.send(JSON.stringify({
1679
+ type: "server_ready",
1680
+ port: userDevPort,
1681
+ appProxyPort,
1682
+ projectId: project.id,
1683
+ projectName: project.name
1684
+ }));
1572
1685
  },
1573
1686
  async message(ws, message) {
1574
1687
  const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
@@ -1588,7 +1701,7 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
1588
1701
  stashService.setSelectedComponent(event.component);
1589
1702
  break;
1590
1703
  case "message":
1591
- persistence.saveChatMessage(event.projectId, {
1704
+ persistence.saveChatMessage(event.projectId, event.chatId, {
1592
1705
  id: crypto.randomUUID(),
1593
1706
  role: "user",
1594
1707
  content: event.message,
@@ -1597,7 +1710,7 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
1597
1710
  referenceStashIds: event.referenceStashIds,
1598
1711
  componentContext: event.componentContext
1599
1712
  });
1600
- await stashService.message(event.projectId, event.message, event.referenceStashIds, event.componentContext);
1713
+ await stashService.message(event.projectId, event.chatId, event.message, event.referenceStashIds, event.componentContext);
1601
1714
  break;
1602
1715
  case "interact":
1603
1716
  await stashService.switchPreview(event.stashId, event.sortedStashIds);
@@ -0,0 +1 @@
1
+ /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-100:oklch(93.6% .032 17.717);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-700:oklch(55.5% .163 48.998);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-900:oklch(37.8% .077 168.94);--color-emerald-950:oklch(26.2% .051 172.552);--color-purple-400:oklch(71.4% .203 305.504);--color-neutral-900:oklch(20.5% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--animate-bounce:bounce 1s infinite;--blur-sm:8px;--aspect-video:16 / 9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.collapse{visibility:collapse}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.top-1\/2{top:50%}.top-2\.5{top:calc(var(--spacing) * 2.5)}.-right-1{right:calc(var(--spacing) * -1)}.right-0{right:calc(var(--spacing) * 0)}.right-2\.5{right:calc(var(--spacing) * 2.5)}.bottom-2\.5{bottom:calc(var(--spacing) * 2.5)}.bottom-4{bottom:calc(var(--spacing) * 4)}.bottom-full{bottom:100%}.-left-1{left:calc(var(--spacing) * -1)}.left-0{left:calc(var(--spacing) * 0)}.left-1\/2{left:50%}.left-2\.5{left:calc(var(--spacing) * 2.5)}.z-10{z-index:10}.z-20{z-index:20}.mx-auto{margin-inline:auto}.my-1{margin-block:calc(var(--spacing) * 1)}.my-1\.5{margin-block:calc(var(--spacing) * 1.5)}.my-2{margin-block:calc(var(--spacing) * 2)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-10{margin-bottom:calc(var(--spacing) * 10)}.-ml-1{margin-left:calc(var(--spacing) * -1)}.ml-0\.5{margin-left:calc(var(--spacing) * .5)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-1\.5{margin-left:calc(var(--spacing) * 1.5)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-7{margin-left:calc(var(--spacing) * 7)}.ml-auto{margin-left:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.aspect-video{aspect-ratio:var(--aspect-video)}.h-1{height:calc(var(--spacing) * 1)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-full{height:100%}.h-screen{height:100vh}.min-h-0{min-height:calc(var(--spacing) * 0)}.min-h-screen{min-height:100vh}.w-1{width:calc(var(--spacing) * 1)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-10{width:calc(var(--spacing) * 10)}.w-32{width:calc(var(--spacing) * 32)}.w-full{width:100%}.w-px{width:1px}.w-screen{width:100vw}.max-w-5xl{max-width:var(--container-5xl)}.max-w-\[280px\]{max-width:280px}.max-w-\[680px\]{max-width:680px}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-\[130px\]{min-width:130px}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-0\.5{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-1{--tw-translate-y:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.animate-bounce{animation:var(--animate-bounce)}.animate-pulse{animation:var(--animate-pulse)}.cursor-col-resize{cursor:col-resize}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-none{--tw-border-style:none;border-style:none}.border-\[var\(--stashes-border\)\]{border-color:var(--stashes-border)}.border-\[var\(--stashes-primary\)\]{border-color:var(--stashes-primary)}.border-emerald-900\/40{border-color:#004e3b66}@supports (color:color-mix(in lab,red,red)){.border-emerald-900\/40{border-color:color-mix(in oklab,var(--color-emerald-900) 40%,transparent)}}.border-red-900\/40{border-color:#82181a66}@supports (color:color-mix(in lab,red,red)){.border-red-900\/40{border-color:color-mix(in oklab,var(--color-red-900) 40%,transparent)}}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.border-white\/10{border-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}.border-white\/60{border-color:#fff9}@supports (color:color-mix(in lab,red,red)){.border-white\/60{border-color:color-mix(in oklab,var(--color-white) 60%,transparent)}}.bg-\[var\(--stashes-bg\)\]{background-color:var(--stashes-bg)}.bg-\[var\(--stashes-border\)\]{background-color:var(--stashes-border)}.bg-\[var\(--stashes-code-bg\)\]{background-color:var(--stashes-code-bg)}.bg-\[var\(--stashes-primary\)\]{background-color:var(--stashes-primary)}.bg-\[var\(--stashes-primary-dim\)\]{background-color:var(--stashes-primary-dim)}.bg-\[var\(--stashes-surface\)\]{background-color:var(--stashes-surface)}.bg-\[var\(--stashes-text-muted\)\]{background-color:var(--stashes-text-muted)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.bg-black\/70{background-color:#000000b3}@supports (color:color-mix(in lab,red,red)){.bg-black\/70{background-color:color-mix(in oklab,var(--color-black) 70%,transparent)}}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-emerald-500\/10{background-color:#00bb7f1a}@supports (color:color-mix(in lab,red,red)){.bg-emerald-500\/10{background-color:color-mix(in oklab,var(--color-emerald-500) 10%,transparent)}}.bg-emerald-500\/15{background-color:#00bb7f26}@supports (color:color-mix(in lab,red,red)){.bg-emerald-500\/15{background-color:color-mix(in oklab,var(--color-emerald-500) 15%,transparent)}}.bg-emerald-900\/40{background-color:#004e3b66}@supports (color:color-mix(in lab,red,red)){.bg-emerald-900\/40{background-color:color-mix(in oklab,var(--color-emerald-900) 40%,transparent)}}.bg-emerald-950\/30{background-color:#002c224d}@supports (color:color-mix(in lab,red,red)){.bg-emerald-950\/30{background-color:color-mix(in oklab,var(--color-emerald-950) 30%,transparent)}}.bg-neutral-900\/90{background-color:#171717e6}@supports (color:color-mix(in lab,red,red)){.bg-neutral-900\/90{background-color:color-mix(in oklab,var(--color-neutral-900) 90%,transparent)}}.bg-red-100{background-color:var(--color-red-100)}.bg-red-500{background-color:var(--color-red-500)}.bg-red-900\/40{background-color:#82181a66}@supports (color:color-mix(in lab,red,red)){.bg-red-900\/40{background-color:color-mix(in oklab,var(--color-red-900) 40%,transparent)}}.bg-red-950\/30{background-color:#4608094d}@supports (color:color-mix(in lab,red,red)){.bg-red-950\/30{background-color:color-mix(in oklab,var(--color-red-950) 30%,transparent)}}.bg-transparent{background-color:#0000}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-\[var\(--stashes-primary\)\]{--tw-gradient-from:var(--stashes-primary);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-400{--tw-gradient-to:var(--color-purple-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.object-cover{object-fit:cover}.object-top{object-position:top}.p-1{padding:calc(var(--spacing) * 1)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-3{padding:calc(var(--spacing) * 3)}.p-3\.5{padding:calc(var(--spacing) * 3.5)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-4{padding-left:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.85em\]{font-size:.85em}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[15px\]{font-size:15px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-all{word-break:break-all}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[var\(--stashes-primary\)\]{color:var(--stashes-primary)}.text-\[var\(--stashes-text\)\]{color:var(--stashes-text)}.text-\[var\(--stashes-text-muted\)\]{color:var(--stashes-text-muted)}.text-amber-400{color:var(--color-amber-400)}.text-amber-400\/70{color:#fcbb00b3}@supports (color:color-mix(in lab,red,red)){.text-amber-400\/70{color:color-mix(in oklab,var(--color-amber-400) 70%,transparent)}}.text-amber-700{color:var(--color-amber-700)}.text-emerald-300{color:var(--color-emerald-300)}.text-emerald-300\/80{color:#5ee9b5cc}@supports (color:color-mix(in lab,red,red)){.text-emerald-300\/80{color:color-mix(in oklab,var(--color-emerald-300) 80%,transparent)}}.text-emerald-400{color:var(--color-emerald-400)}.text-emerald-700{color:var(--color-emerald-700)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-red-700{color:var(--color-red-700)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-100{opacity:1}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-0{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-\[var\(--stashes-ring\)\]{--tw-ring-color:var(--stashes-ring)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}@media(hover:hover){.group-hover\:translate-y-0:is(:where(.group):hover *){--tw-translate-y:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:scale-\[1\.02\]:is(:where(.group):hover *){scale:1.02}.group-hover\:border-\[var\(--stashes-primary\)\]:is(:where(.group):hover *){border-color:var(--stashes-primary)}.group-hover\:text-\[var\(--stashes-primary\)\]:is(:where(.group):hover *){color:var(--stashes-primary)}.group-hover\:text-\[var\(--stashes-text\)\]:is(:where(.group):hover *){color:var(--stashes-text)}.group-hover\:opacity-0:is(:where(.group):hover *){opacity:0}.group-hover\:opacity-40:is(:where(.group):hover *){opacity:.4}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.placeholder\:text-\[var\(--stashes-text-muted\)\]::placeholder{color:var(--stashes-text-muted)}.first\:mt-0:first-child{margin-top:calc(var(--spacing) * 0)}.last\:mb-0:last-child{margin-bottom:calc(var(--spacing) * 0)}.focus-within\:border-\[var\(--stashes-primary\)\]:focus-within{border-color:var(--stashes-primary)}.focus-within\:ring-1:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-within\:ring-\[var\(--stashes-ring\)\]:focus-within{--tw-ring-color:var(--stashes-ring)}@media(hover:hover){.hover\:-translate-y-0\.5:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:border-\[var\(--stashes-border-hover\)\]:hover{border-color:var(--stashes-border-hover)}.hover\:border-\[var\(--stashes-primary\)\]:hover{border-color:var(--stashes-primary)}.hover\:border-white:hover{border-color:var(--color-white)}.hover\:bg-\[var\(--stashes-bg\)\]:hover{background-color:var(--stashes-bg)}.hover\:bg-\[var\(--stashes-primary\)\]:hover{background-color:var(--stashes-primary)}.hover\:bg-\[var\(--stashes-primary-dim\)\]:hover{background-color:var(--stashes-primary-dim)}.hover\:bg-\[var\(--stashes-primary-hover\)\]:hover{background-color:var(--stashes-primary-hover)}.hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/10:hover{background-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}.hover\:text-\[var\(--stashes-text\)\]:hover{color:var(--stashes-text)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}}.active\:bg-\[var\(--stashes-primary\)\]:active{background-color:var(--stashes-primary)}@media(min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:p-10{padding:calc(var(--spacing) * 10)}}@media(min-width:64rem){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:p-12{padding:calc(var(--spacing) * 12)}}@media(min-width:80rem){.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(prefers-color-scheme:dark){.dark\:bg-amber-500\/15{background-color:#f99c0026}@supports (color:color-mix(in lab,red,red)){.dark\:bg-amber-500\/15{background-color:color-mix(in oklab,var(--color-amber-500) 15%,transparent)}}.dark\:bg-emerald-500\/15{background-color:#00bb7f26}@supports (color:color-mix(in lab,red,red)){.dark\:bg-emerald-500\/15{background-color:color-mix(in oklab,var(--color-emerald-500) 15%,transparent)}}.dark\:bg-red-500\/15{background-color:#fb2c3626}@supports (color:color-mix(in lab,red,red)){.dark\:bg-red-500\/15{background-color:color-mix(in oklab,var(--color-red-500) 15%,transparent)}}.dark\:text-amber-400{color:var(--color-amber-400)}.dark\:text-emerald-400{color:var(--color-emerald-400)}.dark\:text-red-400{color:var(--color-red-400)}}}:root{--stashes-primary:#16a34a;--stashes-primary-hover:#15803d;--stashes-primary-dim:#16a34a0f;--stashes-bg:#f8f8f7;--stashes-surface:#fff;--stashes-surface-hover:#fafaf9;--stashes-border:#e8e8e5;--stashes-border-hover:#d4d4d0;--stashes-text:#1c1c1c;--stashes-text-muted:#8b8b8b;--stashes-code-bg:#0000000a;--stashes-shadow-sm:0 1px 2px #0000000a;--stashes-shadow:0 2px 8px #0000000f, 0 1px 2px #0000000a;--stashes-shadow-lg:0 8px 24px #0000001a, 0 2px 6px #0000000a;--stashes-ring:#16a34a4d}@media(prefers-color-scheme:dark){:root{--stashes-primary:#22c55e;--stashes-primary-hover:#4ade80;--stashes-primary-dim:#22c55e14;--stashes-bg:#0c0c0c;--stashes-surface:#161616;--stashes-surface-hover:#1e1e1e;--stashes-border:#2a2a2a;--stashes-border-hover:#3a3a3a;--stashes-text:#e8e8e8;--stashes-text-muted:#737373;--stashes-code-bg:#ffffff0d;--stashes-shadow-sm:0 1px 2px #0003;--stashes-shadow:0 2px 8px #0000004d, 0 1px 2px #0003;--stashes-shadow-lg:0 8px 24px #0006, 0 2px 6px #0003;--stashes-ring:#22c55e4d}}body{background:var(--stashes-bg);color:var(--stashes-text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-feature-settings:"cv01","cv02","cv03","cv04";margin:0;font-family:Inter,system-ui,-apple-system,sans-serif;font-size:14px;line-height:1.5}*{scrollbar-width:thin;scrollbar-color:var(--stashes-border) transparent}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--stashes-border);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--stashes-border-hover)}:focus-visible{outline:2px solid var(--stashes-ring);outline-offset:2px}::selection{background:var(--stashes-primary-dim);color:var(--stashes-text)}.stash-shadow{box-shadow:var(--stashes-shadow-sm);transition:box-shadow .2s,transform .2s}.stash-shadow:hover{box-shadow:var(--stashes-shadow-lg)}.stash-shadow-md{box-shadow:var(--stashes-shadow);transition:box-shadow .2s,transform .2s}.stash-shadow-md:hover{box-shadow:var(--stashes-shadow-lg)}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideUp{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}@keyframes shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}