stashes 0.1.7 → 0.1.8

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
@@ -37,6 +37,7 @@ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
37
37
  // ../shared/dist/constants/index.js
38
38
  var STASHES_PORT = 4000;
39
39
  var DEFAULT_STASH_COUNT = 3;
40
+ var APP_PROXY_PORT = STASHES_PORT + 1;
40
41
  var STASH_PORT_START = 4010;
41
42
  var DEFAULT_DIRECTIVES = [
42
43
  "Minimal and clean \u2014 reduce visual noise, emphasize whitespace, limit to 2-3 colors, typography-driven hierarchy",
@@ -410,6 +411,9 @@ class PersistenceService {
410
411
  const filePath = join4(this.basePath, "projects", projectId, "stashes.json");
411
412
  return readJson(filePath, []);
412
413
  }
414
+ getStash(projectId, stashId) {
415
+ return this.listStashes(projectId).find((s) => s.id === stashId);
416
+ }
413
417
  saveStash(stash) {
414
418
  const stashes = [...this.listStashes(stash.projectId)];
415
419
  const index = stashes.findIndex((s) => s.id === stash.id);
@@ -973,7 +977,7 @@ class StashService {
973
977
  });
974
978
  }
975
979
  }
976
- async chat(projectId, message) {
980
+ async chat(projectId, message, referenceStashIds) {
977
981
  const component = this.selectedComponent;
978
982
  let sourceCode = "";
979
983
  const filePath = component?.filePath || "";
@@ -983,8 +987,18 @@ class StashService {
983
987
  sourceCode = readFileSync4(sourceFile, "utf-8");
984
988
  }
985
989
  }
990
+ let stashContext = "";
991
+ if (referenceStashIds?.length) {
992
+ const refs = referenceStashIds.map((id) => this.persistence.getStash(projectId, id)).filter(Boolean).map((s) => `- "${s.prompt}"${s.componentPath ? ` (${s.componentPath})` : ""}`);
993
+ if (refs.length) {
994
+ stashContext = `
995
+ Referenced stashes:
996
+ ${refs.join(`
997
+ `)}`;
998
+ }
999
+ }
986
1000
  const chatPrompt = [
987
- "The user is asking about a UI component in their project. Answer concisely.",
1001
+ "The user is asking about their UI project. Answer concisely.",
988
1002
  "Do NOT modify any files.",
989
1003
  "",
990
1004
  component ? `Component: ${component.name}` : "",
@@ -994,6 +1008,7 @@ Source:
994
1008
  \`\`\`
995
1009
  ${sourceCode.substring(0, 3000)}
996
1010
  \`\`\`` : "",
1011
+ stashContext,
997
1012
  "",
998
1013
  `User question: ${message}`
999
1014
  ].filter(Boolean).join(`
@@ -1046,18 +1061,23 @@ ${sourceCode.substring(0, 3000)}
1046
1061
  break;
1047
1062
  }
1048
1063
  }
1049
- async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT) {
1050
- if (!this.selectedComponent) {
1051
- throw new Error("No component selected");
1064
+ async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT, referenceStashIds) {
1065
+ let enrichedPrompt = prompt;
1066
+ if (referenceStashIds?.length) {
1067
+ const refDescriptions = referenceStashIds.map((id) => this.persistence.getStash(projectId, id)).filter(Boolean).map((s) => `- "${s.prompt}"${s.componentPath ? ` (${s.componentPath})` : ""}`);
1068
+ if (refDescriptions.length) {
1069
+ enrichedPrompt = `${prompt}
1070
+
1071
+ ## Reference Stashes (use as inspiration)
1072
+ ${refDescriptions.join(`
1073
+ `)}`;
1074
+ }
1052
1075
  }
1053
1076
  await generate({
1054
1077
  projectPath: this.projectPath,
1055
1078
  projectId,
1056
- prompt,
1057
- component: {
1058
- filePath: this.selectedComponent.filePath,
1059
- exportName: this.selectedComponent.name
1060
- },
1079
+ prompt: enrichedPrompt,
1080
+ component: this.selectedComponent ? { filePath: this.selectedComponent.filePath, exportName: this.selectedComponent.name } : undefined,
1061
1081
  count: stashCount,
1062
1082
  onProgress: (event) => this.progressToBroadcast(event)
1063
1083
  });
@@ -1174,7 +1194,7 @@ function broadcast(event) {
1174
1194
  function getPersistenceFromWs() {
1175
1195
  return persistence;
1176
1196
  }
1177
- function createWebSocketHandler(projectPath, userDevPort) {
1197
+ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
1178
1198
  worktreeManager = new WorktreeManager(projectPath);
1179
1199
  persistence = new PersistenceService(projectPath);
1180
1200
  stashService = new StashService(projectPath, worktreeManager, persistence, broadcast);
@@ -1182,7 +1202,7 @@ function createWebSocketHandler(projectPath, userDevPort) {
1182
1202
  open(ws) {
1183
1203
  clients.add(ws);
1184
1204
  logger.info("ws", "client connected", { total: clients.size });
1185
- ws.send(JSON.stringify({ type: "server_ready", port: userDevPort }));
1205
+ ws.send(JSON.stringify({ type: "server_ready", port: userDevPort, appProxyPort }));
1186
1206
  },
1187
1207
  async message(ws, message) {
1188
1208
  const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
@@ -1209,7 +1229,7 @@ function createWebSocketHandler(projectPath, userDevPort) {
1209
1229
  type: "text",
1210
1230
  createdAt: new Date().toISOString()
1211
1231
  });
1212
- await stashService.chat(event.projectId, event.message);
1232
+ await stashService.chat(event.projectId, event.message, event.referenceStashIds);
1213
1233
  break;
1214
1234
  case "generate":
1215
1235
  persistence.saveChatMessage(event.projectId, {
@@ -1219,7 +1239,7 @@ function createWebSocketHandler(projectPath, userDevPort) {
1219
1239
  type: "text",
1220
1240
  createdAt: new Date().toISOString()
1221
1241
  });
1222
- await stashService.generate(event.projectId, event.prompt, event.stashCount);
1242
+ await stashService.generate(event.projectId, event.prompt, event.stashCount, event.referenceStashIds);
1223
1243
  break;
1224
1244
  case "vary":
1225
1245
  await stashService.vary(event.sourceStashId, event.prompt);
@@ -1247,6 +1267,111 @@ function createWebSocketHandler(projectPath, userDevPort) {
1247
1267
  };
1248
1268
  }
1249
1269
 
1270
+ // ../server/dist/services/app-proxy.js
1271
+ function startAppProxy(userDevPort, proxyPort, injectOverlay) {
1272
+ const server = Bun.serve({
1273
+ port: proxyPort,
1274
+ async fetch(req, server2) {
1275
+ if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
1276
+ const url2 = new URL(req.url);
1277
+ const success = server2.upgrade(req, {
1278
+ data: {
1279
+ path: url2.pathname + url2.search,
1280
+ upstream: null,
1281
+ ready: false,
1282
+ buffer: []
1283
+ }
1284
+ });
1285
+ return success ? undefined : new Response("WebSocket upgrade failed", { status: 400 });
1286
+ }
1287
+ const url = new URL(req.url);
1288
+ const targetUrl = `http://localhost:${userDevPort}${url.pathname}${url.search}`;
1289
+ try {
1290
+ const headers = new Headers;
1291
+ for (const [key, value] of req.headers.entries()) {
1292
+ if (!["host", "accept-encoding"].includes(key.toLowerCase())) {
1293
+ headers.set(key, value);
1294
+ }
1295
+ }
1296
+ headers.set("host", `localhost:${userDevPort}`);
1297
+ const response = await fetch(targetUrl, {
1298
+ method: req.method,
1299
+ headers,
1300
+ body: req.method !== "GET" && req.method !== "HEAD" ? await req.arrayBuffer() : undefined,
1301
+ redirect: "manual"
1302
+ });
1303
+ const contentType = response.headers.get("content-type") || "";
1304
+ if (contentType.includes("text/html")) {
1305
+ const html = await response.text();
1306
+ const injectedHtml = injectOverlay(html);
1307
+ const respHeaders2 = new Headers(response.headers);
1308
+ respHeaders2.delete("content-encoding");
1309
+ respHeaders2.delete("content-length");
1310
+ return new Response(injectedHtml, {
1311
+ status: response.status,
1312
+ headers: respHeaders2
1313
+ });
1314
+ }
1315
+ const respHeaders = new Headers(response.headers);
1316
+ respHeaders.delete("content-encoding");
1317
+ return new Response(response.body, {
1318
+ status: response.status,
1319
+ headers: respHeaders
1320
+ });
1321
+ } catch (err) {
1322
+ return new Response(JSON.stringify({ error: "Proxy failed", detail: String(err) }), {
1323
+ status: 502,
1324
+ headers: { "content-type": "application/json" }
1325
+ });
1326
+ }
1327
+ },
1328
+ websocket: {
1329
+ open(ws) {
1330
+ const { data } = ws;
1331
+ const upstream = new WebSocket(`ws://localhost:${userDevPort}${data.path}`);
1332
+ upstream.addEventListener("open", () => {
1333
+ data.upstream = upstream;
1334
+ data.ready = true;
1335
+ for (const msg of data.buffer) {
1336
+ upstream.send(msg);
1337
+ }
1338
+ data.buffer = [];
1339
+ });
1340
+ upstream.addEventListener("message", (event) => {
1341
+ try {
1342
+ ws.sendText(typeof event.data === "string" ? event.data : String(event.data));
1343
+ } catch {}
1344
+ });
1345
+ upstream.addEventListener("close", () => {
1346
+ try {
1347
+ ws.close();
1348
+ } catch {}
1349
+ });
1350
+ upstream.addEventListener("error", () => {
1351
+ try {
1352
+ ws.close();
1353
+ } catch {}
1354
+ });
1355
+ },
1356
+ message(ws, msg) {
1357
+ const { data } = ws;
1358
+ if (data.ready && data.upstream) {
1359
+ data.upstream.send(typeof msg === "string" ? msg : new Uint8Array(msg));
1360
+ } else {
1361
+ data.buffer.push(typeof msg === "string" ? msg : new Uint8Array(msg).buffer);
1362
+ }
1363
+ },
1364
+ close(ws) {
1365
+ try {
1366
+ ws.data.upstream?.close();
1367
+ } catch {}
1368
+ }
1369
+ }
1370
+ });
1371
+ logger.info("proxy", `App proxy at http://localhost:${proxyPort} \u2192 http://localhost:${userDevPort}`);
1372
+ return server;
1373
+ }
1374
+
1250
1375
  // ../server/dist/index.js
1251
1376
  var serverState = {
1252
1377
  projectPath: "",
@@ -1258,56 +1383,6 @@ function getPersistence() {
1258
1383
  var app2 = new Hono2;
1259
1384
  app2.use("/*", cors());
1260
1385
  app2.route("/api", apiRoutes);
1261
- async function proxyToUserApp(c, targetPath, injectOverlay = false) {
1262
- const userDevPort = serverState.userDevPort;
1263
- const url = new URL(c.req.url);
1264
- const targetUrl = `http://localhost:${userDevPort}${targetPath}${url.search}`;
1265
- try {
1266
- const headers = new Headers;
1267
- for (const [key, value] of Object.entries(c.req.header())) {
1268
- if (!["host", "accept-encoding"].includes(key.toLowerCase()) && value) {
1269
- headers.set(key, value);
1270
- }
1271
- }
1272
- headers.set("host", `localhost:${userDevPort}`);
1273
- const response = await fetch(targetUrl, {
1274
- method: c.req.method,
1275
- headers,
1276
- body: c.req.method !== "GET" && c.req.method !== "HEAD" ? await c.req.arrayBuffer() : undefined,
1277
- redirect: "manual"
1278
- });
1279
- const contentType = response.headers.get("content-type") || "";
1280
- if (injectOverlay && contentType.includes("text/html")) {
1281
- const html = await response.text();
1282
- const injectedHtml = injectOverlayScript(html);
1283
- const respHeaders2 = new Headers(response.headers);
1284
- respHeaders2.delete("content-encoding");
1285
- respHeaders2.delete("content-length");
1286
- return new Response(injectedHtml, {
1287
- status: response.status,
1288
- headers: respHeaders2
1289
- });
1290
- }
1291
- const respHeaders = new Headers(response.headers);
1292
- respHeaders.delete("content-encoding");
1293
- return new Response(response.body, {
1294
- status: response.status,
1295
- headers: respHeaders
1296
- });
1297
- } catch (err) {
1298
- return new Response(JSON.stringify({ error: "Proxy failed", detail: String(err) }), {
1299
- status: 502,
1300
- headers: { "content-type": "application/json" }
1301
- });
1302
- }
1303
- }
1304
- app2.all("/app/*", (c) => {
1305
- const path = c.req.path.replace(/^\/app/, "") || "/";
1306
- return proxyToUserApp(c, path, true);
1307
- });
1308
- app2.all("/_next/*", (c) => proxyToUserApp(c, c.req.path));
1309
- app2.all("/__nextjs*", (c) => proxyToUserApp(c, c.req.path));
1310
- app2.all("/__next*", (c) => proxyToUserApp(c, c.req.path));
1311
1386
  app2.get("/*", async (c) => {
1312
1387
  const path = c.req.path;
1313
1388
  const selfDir = dirname2(fileURLToPath(import.meta.url));
@@ -1350,7 +1425,9 @@ app2.get("/*", async (c) => {
1350
1425
  function startServer(projectPath, userDevPort, port = STASHES_PORT) {
1351
1426
  serverState = { projectPath, userDevPort };
1352
1427
  initLogFile(projectPath);
1353
- const wsHandler = createWebSocketHandler(projectPath, userDevPort);
1428
+ const appProxyPort = port + 1;
1429
+ startAppProxy(userDevPort, appProxyPort, injectOverlayScript);
1430
+ const wsHandler = createWebSocketHandler(projectPath, userDevPort, appProxyPort);
1354
1431
  const server = Bun.serve({
1355
1432
  port,
1356
1433
  fetch(req, server2) {
@@ -1515,22 +1592,25 @@ function injectOverlayScript(html) {
1515
1592
  }
1516
1593
  });
1517
1594
 
1518
- document.addEventListener('click', function(e) {
1519
- if (pickerEnabled) return;
1520
- var link = e.target;
1521
- while (link && link.tagName !== 'A') link = link.parentElement;
1522
- if (link && link.href) {
1523
- var url;
1524
- try { url = new URL(link.href); } catch(_) { return; }
1525
- if (url.origin === window.location.origin) {
1526
- var path = url.pathname;
1527
- if (!path.startsWith('/app/')) {
1528
- e.preventDefault();
1529
- window.location.href = '/app' + path + url.search + url.hash;
1530
- }
1531
- }
1532
- }
1533
- }, false);
1595
+ // Report current URL to parent for status bar display
1596
+ function reportUrl() {
1597
+ window.parent.postMessage({
1598
+ type: 'stashes:url_change',
1599
+ url: window.location.pathname + window.location.search + window.location.hash
1600
+ }, '*');
1601
+ }
1602
+ reportUrl();
1603
+ var origPush = history.pushState;
1604
+ history.pushState = function() {
1605
+ origPush.apply(this, arguments);
1606
+ setTimeout(reportUrl, 0);
1607
+ };
1608
+ var origReplace = history.replaceState;
1609
+ history.replaceState = function() {
1610
+ origReplace.apply(this, arguments);
1611
+ setTimeout(reportUrl, 0);
1612
+ };
1613
+ window.addEventListener('popstate', reportUrl);
1534
1614
 
1535
1615
  document.addEventListener('mousemove', onMouseMove, { passive: true });
1536
1616
  document.addEventListener('click', onClick, true);
package/dist/mcp.js CHANGED
@@ -33,6 +33,7 @@ import simpleGit2 from "simple-git";
33
33
  // ../shared/dist/constants/index.js
34
34
  var STASHES_PORT = 4000;
35
35
  var DEFAULT_STASH_COUNT = 3;
36
+ var APP_PROXY_PORT = STASHES_PORT + 1;
36
37
  var STASH_PORT_START = 4010;
37
38
  var DEFAULT_DIRECTIVES = [
38
39
  "Minimal and clean \u2014 reduce visual noise, emphasize whitespace, limit to 2-3 colors, typography-driven hierarchy",
@@ -346,6 +347,9 @@ class PersistenceService {
346
347
  const filePath = join3(this.basePath, "projects", projectId, "stashes.json");
347
348
  return readJson(filePath, []);
348
349
  }
350
+ getStash(projectId, stashId) {
351
+ return this.listStashes(projectId).find((s) => s.id === stashId);
352
+ }
349
353
  saveStash(stash) {
350
354
  const stashes = [...this.listStashes(stash.projectId)];
351
355
  const index = stashes.findIndex((s) => s.id === stash.id);
@@ -1103,7 +1107,7 @@ class StashService {
1103
1107
  });
1104
1108
  }
1105
1109
  }
1106
- async chat(projectId, message) {
1110
+ async chat(projectId, message, referenceStashIds) {
1107
1111
  const component = this.selectedComponent;
1108
1112
  let sourceCode = "";
1109
1113
  const filePath = component?.filePath || "";
@@ -1113,8 +1117,18 @@ class StashService {
1113
1117
  sourceCode = readFileSync4(sourceFile, "utf-8");
1114
1118
  }
1115
1119
  }
1120
+ let stashContext = "";
1121
+ if (referenceStashIds?.length) {
1122
+ const refs = referenceStashIds.map((id) => this.persistence.getStash(projectId, id)).filter(Boolean).map((s) => `- "${s.prompt}"${s.componentPath ? ` (${s.componentPath})` : ""}`);
1123
+ if (refs.length) {
1124
+ stashContext = `
1125
+ Referenced stashes:
1126
+ ${refs.join(`
1127
+ `)}`;
1128
+ }
1129
+ }
1116
1130
  const chatPrompt = [
1117
- "The user is asking about a UI component in their project. Answer concisely.",
1131
+ "The user is asking about their UI project. Answer concisely.",
1118
1132
  "Do NOT modify any files.",
1119
1133
  "",
1120
1134
  component ? `Component: ${component.name}` : "",
@@ -1124,6 +1138,7 @@ Source:
1124
1138
  \`\`\`
1125
1139
  ${sourceCode.substring(0, 3000)}
1126
1140
  \`\`\`` : "",
1141
+ stashContext,
1127
1142
  "",
1128
1143
  `User question: ${message}`
1129
1144
  ].filter(Boolean).join(`
@@ -1176,18 +1191,23 @@ ${sourceCode.substring(0, 3000)}
1176
1191
  break;
1177
1192
  }
1178
1193
  }
1179
- async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT) {
1180
- if (!this.selectedComponent) {
1181
- throw new Error("No component selected");
1194
+ async generate(projectId, prompt, stashCount = DEFAULT_STASH_COUNT, referenceStashIds) {
1195
+ let enrichedPrompt = prompt;
1196
+ if (referenceStashIds?.length) {
1197
+ const refDescriptions = referenceStashIds.map((id) => this.persistence.getStash(projectId, id)).filter(Boolean).map((s) => `- "${s.prompt}"${s.componentPath ? ` (${s.componentPath})` : ""}`);
1198
+ if (refDescriptions.length) {
1199
+ enrichedPrompt = `${prompt}
1200
+
1201
+ ## Reference Stashes (use as inspiration)
1202
+ ${refDescriptions.join(`
1203
+ `)}`;
1204
+ }
1182
1205
  }
1183
1206
  await generate({
1184
1207
  projectPath: this.projectPath,
1185
1208
  projectId,
1186
- prompt,
1187
- component: {
1188
- filePath: this.selectedComponent.filePath,
1189
- exportName: this.selectedComponent.name
1190
- },
1209
+ prompt: enrichedPrompt,
1210
+ component: this.selectedComponent ? { filePath: this.selectedComponent.filePath, exportName: this.selectedComponent.name } : undefined,
1191
1211
  count: stashCount,
1192
1212
  onProgress: (event) => this.progressToBroadcast(event)
1193
1213
  });
@@ -1304,7 +1324,7 @@ function broadcast(event) {
1304
1324
  function getPersistenceFromWs() {
1305
1325
  return persistence;
1306
1326
  }
1307
- function createWebSocketHandler(projectPath, userDevPort) {
1327
+ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
1308
1328
  worktreeManager = new WorktreeManager(projectPath);
1309
1329
  persistence = new PersistenceService(projectPath);
1310
1330
  stashService = new StashService(projectPath, worktreeManager, persistence, broadcast);
@@ -1312,7 +1332,7 @@ function createWebSocketHandler(projectPath, userDevPort) {
1312
1332
  open(ws) {
1313
1333
  clients.add(ws);
1314
1334
  logger.info("ws", "client connected", { total: clients.size });
1315
- ws.send(JSON.stringify({ type: "server_ready", port: userDevPort }));
1335
+ ws.send(JSON.stringify({ type: "server_ready", port: userDevPort, appProxyPort }));
1316
1336
  },
1317
1337
  async message(ws, message) {
1318
1338
  const raw = typeof message === "string" ? message : new TextDecoder().decode(message);
@@ -1339,7 +1359,7 @@ function createWebSocketHandler(projectPath, userDevPort) {
1339
1359
  type: "text",
1340
1360
  createdAt: new Date().toISOString()
1341
1361
  });
1342
- await stashService.chat(event.projectId, event.message);
1362
+ await stashService.chat(event.projectId, event.message, event.referenceStashIds);
1343
1363
  break;
1344
1364
  case "generate":
1345
1365
  persistence.saveChatMessage(event.projectId, {
@@ -1349,7 +1369,7 @@ function createWebSocketHandler(projectPath, userDevPort) {
1349
1369
  type: "text",
1350
1370
  createdAt: new Date().toISOString()
1351
1371
  });
1352
- await stashService.generate(event.projectId, event.prompt, event.stashCount);
1372
+ await stashService.generate(event.projectId, event.prompt, event.stashCount, event.referenceStashIds);
1353
1373
  break;
1354
1374
  case "vary":
1355
1375
  await stashService.vary(event.sourceStashId, event.prompt);
@@ -1377,6 +1397,111 @@ function createWebSocketHandler(projectPath, userDevPort) {
1377
1397
  };
1378
1398
  }
1379
1399
 
1400
+ // ../server/dist/services/app-proxy.js
1401
+ function startAppProxy(userDevPort, proxyPort, injectOverlay) {
1402
+ const server = Bun.serve({
1403
+ port: proxyPort,
1404
+ async fetch(req, server2) {
1405
+ if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
1406
+ const url2 = new URL(req.url);
1407
+ const success = server2.upgrade(req, {
1408
+ data: {
1409
+ path: url2.pathname + url2.search,
1410
+ upstream: null,
1411
+ ready: false,
1412
+ buffer: []
1413
+ }
1414
+ });
1415
+ return success ? undefined : new Response("WebSocket upgrade failed", { status: 400 });
1416
+ }
1417
+ const url = new URL(req.url);
1418
+ const targetUrl = `http://localhost:${userDevPort}${url.pathname}${url.search}`;
1419
+ try {
1420
+ const headers = new Headers;
1421
+ for (const [key, value] of req.headers.entries()) {
1422
+ if (!["host", "accept-encoding"].includes(key.toLowerCase())) {
1423
+ headers.set(key, value);
1424
+ }
1425
+ }
1426
+ headers.set("host", `localhost:${userDevPort}`);
1427
+ const response = await fetch(targetUrl, {
1428
+ method: req.method,
1429
+ headers,
1430
+ body: req.method !== "GET" && req.method !== "HEAD" ? await req.arrayBuffer() : undefined,
1431
+ redirect: "manual"
1432
+ });
1433
+ const contentType = response.headers.get("content-type") || "";
1434
+ if (contentType.includes("text/html")) {
1435
+ const html = await response.text();
1436
+ const injectedHtml = injectOverlay(html);
1437
+ const respHeaders2 = new Headers(response.headers);
1438
+ respHeaders2.delete("content-encoding");
1439
+ respHeaders2.delete("content-length");
1440
+ return new Response(injectedHtml, {
1441
+ status: response.status,
1442
+ headers: respHeaders2
1443
+ });
1444
+ }
1445
+ const respHeaders = new Headers(response.headers);
1446
+ respHeaders.delete("content-encoding");
1447
+ return new Response(response.body, {
1448
+ status: response.status,
1449
+ headers: respHeaders
1450
+ });
1451
+ } catch (err) {
1452
+ return new Response(JSON.stringify({ error: "Proxy failed", detail: String(err) }), {
1453
+ status: 502,
1454
+ headers: { "content-type": "application/json" }
1455
+ });
1456
+ }
1457
+ },
1458
+ websocket: {
1459
+ open(ws) {
1460
+ const { data } = ws;
1461
+ const upstream = new WebSocket(`ws://localhost:${userDevPort}${data.path}`);
1462
+ upstream.addEventListener("open", () => {
1463
+ data.upstream = upstream;
1464
+ data.ready = true;
1465
+ for (const msg of data.buffer) {
1466
+ upstream.send(msg);
1467
+ }
1468
+ data.buffer = [];
1469
+ });
1470
+ upstream.addEventListener("message", (event) => {
1471
+ try {
1472
+ ws.sendText(typeof event.data === "string" ? event.data : String(event.data));
1473
+ } catch {}
1474
+ });
1475
+ upstream.addEventListener("close", () => {
1476
+ try {
1477
+ ws.close();
1478
+ } catch {}
1479
+ });
1480
+ upstream.addEventListener("error", () => {
1481
+ try {
1482
+ ws.close();
1483
+ } catch {}
1484
+ });
1485
+ },
1486
+ message(ws, msg) {
1487
+ const { data } = ws;
1488
+ if (data.ready && data.upstream) {
1489
+ data.upstream.send(typeof msg === "string" ? msg : new Uint8Array(msg));
1490
+ } else {
1491
+ data.buffer.push(typeof msg === "string" ? msg : new Uint8Array(msg).buffer);
1492
+ }
1493
+ },
1494
+ close(ws) {
1495
+ try {
1496
+ ws.data.upstream?.close();
1497
+ } catch {}
1498
+ }
1499
+ }
1500
+ });
1501
+ logger.info("proxy", `App proxy at http://localhost:${proxyPort} \u2192 http://localhost:${userDevPort}`);
1502
+ return server;
1503
+ }
1504
+
1380
1505
  // ../server/dist/index.js
1381
1506
  var serverState = {
1382
1507
  projectPath: "",
@@ -1388,56 +1513,6 @@ function getPersistence() {
1388
1513
  var app2 = new Hono2;
1389
1514
  app2.use("/*", cors());
1390
1515
  app2.route("/api", apiRoutes);
1391
- async function proxyToUserApp(c, targetPath, injectOverlay = false) {
1392
- const userDevPort = serverState.userDevPort;
1393
- const url = new URL(c.req.url);
1394
- const targetUrl = `http://localhost:${userDevPort}${targetPath}${url.search}`;
1395
- try {
1396
- const headers = new Headers;
1397
- for (const [key, value] of Object.entries(c.req.header())) {
1398
- if (!["host", "accept-encoding"].includes(key.toLowerCase()) && value) {
1399
- headers.set(key, value);
1400
- }
1401
- }
1402
- headers.set("host", `localhost:${userDevPort}`);
1403
- const response = await fetch(targetUrl, {
1404
- method: c.req.method,
1405
- headers,
1406
- body: c.req.method !== "GET" && c.req.method !== "HEAD" ? await c.req.arrayBuffer() : undefined,
1407
- redirect: "manual"
1408
- });
1409
- const contentType = response.headers.get("content-type") || "";
1410
- if (injectOverlay && contentType.includes("text/html")) {
1411
- const html = await response.text();
1412
- const injectedHtml = injectOverlayScript(html);
1413
- const respHeaders2 = new Headers(response.headers);
1414
- respHeaders2.delete("content-encoding");
1415
- respHeaders2.delete("content-length");
1416
- return new Response(injectedHtml, {
1417
- status: response.status,
1418
- headers: respHeaders2
1419
- });
1420
- }
1421
- const respHeaders = new Headers(response.headers);
1422
- respHeaders.delete("content-encoding");
1423
- return new Response(response.body, {
1424
- status: response.status,
1425
- headers: respHeaders
1426
- });
1427
- } catch (err) {
1428
- return new Response(JSON.stringify({ error: "Proxy failed", detail: String(err) }), {
1429
- status: 502,
1430
- headers: { "content-type": "application/json" }
1431
- });
1432
- }
1433
- }
1434
- app2.all("/app/*", (c) => {
1435
- const path = c.req.path.replace(/^\/app/, "") || "/";
1436
- return proxyToUserApp(c, path, true);
1437
- });
1438
- app2.all("/_next/*", (c) => proxyToUserApp(c, c.req.path));
1439
- app2.all("/__nextjs*", (c) => proxyToUserApp(c, c.req.path));
1440
- app2.all("/__next*", (c) => proxyToUserApp(c, c.req.path));
1441
1516
  app2.get("/*", async (c) => {
1442
1517
  const path = c.req.path;
1443
1518
  const selfDir = dirname2(fileURLToPath(import.meta.url));
@@ -1480,7 +1555,9 @@ app2.get("/*", async (c) => {
1480
1555
  function startServer(projectPath, userDevPort, port = STASHES_PORT) {
1481
1556
  serverState = { projectPath, userDevPort };
1482
1557
  initLogFile(projectPath);
1483
- const wsHandler = createWebSocketHandler(projectPath, userDevPort);
1558
+ const appProxyPort = port + 1;
1559
+ startAppProxy(userDevPort, appProxyPort, injectOverlayScript);
1560
+ const wsHandler = createWebSocketHandler(projectPath, userDevPort, appProxyPort);
1484
1561
  const server = Bun.serve({
1485
1562
  port,
1486
1563
  fetch(req, server2) {
@@ -1645,22 +1722,25 @@ function injectOverlayScript(html) {
1645
1722
  }
1646
1723
  });
1647
1724
 
1648
- document.addEventListener('click', function(e) {
1649
- if (pickerEnabled) return;
1650
- var link = e.target;
1651
- while (link && link.tagName !== 'A') link = link.parentElement;
1652
- if (link && link.href) {
1653
- var url;
1654
- try { url = new URL(link.href); } catch(_) { return; }
1655
- if (url.origin === window.location.origin) {
1656
- var path = url.pathname;
1657
- if (!path.startsWith('/app/')) {
1658
- e.preventDefault();
1659
- window.location.href = '/app' + path + url.search + url.hash;
1660
- }
1661
- }
1662
- }
1663
- }, false);
1725
+ // Report current URL to parent for status bar display
1726
+ function reportUrl() {
1727
+ window.parent.postMessage({
1728
+ type: 'stashes:url_change',
1729
+ url: window.location.pathname + window.location.search + window.location.hash
1730
+ }, '*');
1731
+ }
1732
+ reportUrl();
1733
+ var origPush = history.pushState;
1734
+ history.pushState = function() {
1735
+ origPush.apply(this, arguments);
1736
+ setTimeout(reportUrl, 0);
1737
+ };
1738
+ var origReplace = history.replaceState;
1739
+ history.replaceState = function() {
1740
+ origReplace.apply(this, arguments);
1741
+ setTimeout(reportUrl, 0);
1742
+ };
1743
+ window.addEventListener('popstate', reportUrl);
1664
1744
 
1665
1745
  document.addEventListener('mousemove', onMouseMove, { passive: true });
1666
1746
  document.addEventListener('click', onClick, true);