stashes 0.1.38 → 0.1.40
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 +463 -426
- package/dist/mcp.js +343 -306
- package/dist/web/assets/index-DtFzLchV.css +1 -0
- package/dist/web/assets/index-Dtvtzi3e.js +96 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-D6x476ew.js +0 -96
- package/dist/web/assets/index-DQGPJamz.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -102,18 +102,35 @@ app.get("/chats", (c) => {
|
|
|
102
102
|
app.post("/chats", async (c) => {
|
|
103
103
|
const persistence = getPersistence();
|
|
104
104
|
const project = ensureProject(persistence);
|
|
105
|
-
const { title } = await c.req.json();
|
|
105
|
+
const { title, referencedStashIds } = await c.req.json();
|
|
106
106
|
const chatCount = persistence.listChats(project.id).length;
|
|
107
107
|
const chat = {
|
|
108
108
|
id: `chat_${crypto.randomUUID().substring(0, 8)}`,
|
|
109
109
|
projectId: project.id,
|
|
110
110
|
title: title?.trim() || `Chat ${chatCount + 1}`,
|
|
111
|
+
referencedStashIds: referencedStashIds ?? [],
|
|
111
112
|
createdAt: new Date().toISOString(),
|
|
112
113
|
updatedAt: new Date().toISOString()
|
|
113
114
|
};
|
|
114
115
|
persistence.saveChat(chat);
|
|
115
116
|
return c.json({ data: chat }, 201);
|
|
116
117
|
});
|
|
118
|
+
app.patch("/chats/:chatId", async (c) => {
|
|
119
|
+
const persistence = getPersistence();
|
|
120
|
+
const project = ensureProject(persistence);
|
|
121
|
+
const chatId = c.req.param("chatId");
|
|
122
|
+
const chat = persistence.getChat(project.id, chatId);
|
|
123
|
+
if (!chat)
|
|
124
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
125
|
+
const body = await c.req.json();
|
|
126
|
+
const updated = {
|
|
127
|
+
...chat,
|
|
128
|
+
...body.referencedStashIds !== undefined ? { referencedStashIds: body.referencedStashIds } : {},
|
|
129
|
+
updatedAt: new Date().toISOString()
|
|
130
|
+
};
|
|
131
|
+
persistence.saveChat(updated);
|
|
132
|
+
return c.json({ data: updated });
|
|
133
|
+
});
|
|
117
134
|
app.get("/chats/:chatId", (c) => {
|
|
118
135
|
const persistence = getPersistence();
|
|
119
136
|
const project = ensureProject(persistence);
|
|
@@ -122,7 +139,8 @@ app.get("/chats/:chatId", (c) => {
|
|
|
122
139
|
if (!chat)
|
|
123
140
|
return c.json({ error: "Chat not found" }, 404);
|
|
124
141
|
const messages = persistence.getChatMessages(project.id, chatId);
|
|
125
|
-
const
|
|
142
|
+
const refIds = new Set(chat.referencedStashIds ?? []);
|
|
143
|
+
const stashes = persistence.listStashes(project.id).filter((s) => s.originChatId === chatId || refIds.has(s.id));
|
|
126
144
|
return c.json({ data: { ...chat, messages, stashes } });
|
|
127
145
|
});
|
|
128
146
|
app.delete("/chats/:chatId", (c) => {
|
|
@@ -1362,141 +1380,428 @@ async function cleanup(projectPath) {
|
|
|
1362
1380
|
import { readFileSync as readFileSync4, existsSync as existsSync8 } from "fs";
|
|
1363
1381
|
import { join as join8 } from "path";
|
|
1364
1382
|
|
|
1365
|
-
// ../server/dist/services/
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
this.usedPorts.add(port);
|
|
1410
|
-
logger.info("pool", `cold start: ${stashId} on port ${port}`, { poolSize: this.entries.size });
|
|
1411
|
-
await this.waitForPort(port, 60000);
|
|
1412
|
-
return port;
|
|
1413
|
-
}
|
|
1414
|
-
heartbeat(stashId) {
|
|
1415
|
-
const entry = this.entries.get(stashId);
|
|
1416
|
-
if (entry) {
|
|
1417
|
-
entry.lastHeartbeat = Date.now();
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
isWarm(stashId) {
|
|
1421
|
-
return this.entries.has(stashId);
|
|
1422
|
-
}
|
|
1423
|
-
getPort(stashId) {
|
|
1424
|
-
return this.entries.get(stashId)?.port ?? null;
|
|
1425
|
-
}
|
|
1426
|
-
async stop(stashId) {
|
|
1427
|
-
const entry = this.entries.get(stashId);
|
|
1428
|
-
if (!entry)
|
|
1429
|
-
return;
|
|
1430
|
-
logger.info("pool", `stopping: ${stashId} on port ${entry.port}`);
|
|
1431
|
-
this.killEntry(entry);
|
|
1432
|
-
this.entries.delete(stashId);
|
|
1433
|
-
this.usedPorts.delete(entry.port);
|
|
1434
|
-
try {
|
|
1435
|
-
await this.worktreeManager.removePreviewForPool(stashId);
|
|
1436
|
-
} catch (err) {
|
|
1437
|
-
logger.warn("pool", `worktree removal failed for ${stashId}`, {
|
|
1438
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1383
|
+
// ../server/dist/services/app-proxy.js
|
|
1384
|
+
import { spawn as spawn5 } from "child_process";
|
|
1385
|
+
function startAppProxy(userDevPort, proxyPort, injectOverlay) {
|
|
1386
|
+
const overlayScript = injectOverlay("", userDevPort, proxyPort);
|
|
1387
|
+
const overlayEscaped = JSON.stringify(overlayScript);
|
|
1388
|
+
const proxyScript = `
|
|
1389
|
+
const http = require('http');
|
|
1390
|
+
const net = require('net');
|
|
1391
|
+
const zlib = require('zlib');
|
|
1392
|
+
const UPSTREAM = ${userDevPort};
|
|
1393
|
+
const OVERLAY = ${overlayEscaped};
|
|
1394
|
+
|
|
1395
|
+
const server = http.createServer((clientReq, clientRes) => {
|
|
1396
|
+
const opts = {
|
|
1397
|
+
hostname: 'localhost',
|
|
1398
|
+
port: UPSTREAM,
|
|
1399
|
+
path: clientReq.url,
|
|
1400
|
+
method: clientReq.method,
|
|
1401
|
+
headers: clientReq.headers,
|
|
1402
|
+
};
|
|
1403
|
+
const proxyReq = http.request(opts, (proxyRes) => {
|
|
1404
|
+
const ct = proxyRes.headers['content-type'] || '';
|
|
1405
|
+
if (ct.includes('text/html')) {
|
|
1406
|
+
// Buffer HTML to inject overlay
|
|
1407
|
+
const chunks = [];
|
|
1408
|
+
proxyRes.on('data', c => chunks.push(c));
|
|
1409
|
+
proxyRes.on('end', () => {
|
|
1410
|
+
let html = Buffer.concat(chunks);
|
|
1411
|
+
const enc = proxyRes.headers['content-encoding'];
|
|
1412
|
+
// Decompress if needed
|
|
1413
|
+
if (enc === 'gzip') {
|
|
1414
|
+
try { html = zlib.gunzipSync(html); } catch {}
|
|
1415
|
+
} else if (enc === 'br') {
|
|
1416
|
+
try { html = zlib.brotliDecompressSync(html); } catch {}
|
|
1417
|
+
} else if (enc === 'deflate') {
|
|
1418
|
+
try { html = zlib.inflateSync(html); } catch {}
|
|
1419
|
+
}
|
|
1420
|
+
const hdrs = { ...proxyRes.headers };
|
|
1421
|
+
delete hdrs['content-length'];
|
|
1422
|
+
delete hdrs['content-encoding'];
|
|
1423
|
+
delete hdrs['transfer-encoding'];
|
|
1424
|
+
clientRes.writeHead(proxyRes.statusCode, hdrs);
|
|
1425
|
+
clientRes.write(html);
|
|
1426
|
+
clientRes.end(OVERLAY);
|
|
1439
1427
|
});
|
|
1428
|
+
} else {
|
|
1429
|
+
// Non-HTML: stream through unchanged
|
|
1430
|
+
clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
1431
|
+
proxyRes.pipe(clientRes);
|
|
1440
1432
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
neighbors.push(sortedStashIds[nextIndex]);
|
|
1454
|
-
}
|
|
1455
|
-
for (const stashId of neighbors) {
|
|
1456
|
-
if (this.entries.size >= this.maxSize)
|
|
1457
|
-
break;
|
|
1458
|
-
logger.info("pool", `prefetching neighbor: ${stashId}`);
|
|
1459
|
-
this.getOrStart(stashId).then((port) => {
|
|
1460
|
-
this.broadcast({ type: "stash:port", stashId, port });
|
|
1461
|
-
}).catch((err) => {
|
|
1462
|
-
logger.warn("pool", `prefetch failed for ${stashId}`, {
|
|
1463
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1464
|
-
});
|
|
1465
|
-
});
|
|
1433
|
+
proxyRes.on('error', () => clientRes.end());
|
|
1434
|
+
});
|
|
1435
|
+
proxyReq.on('error', () => { try { clientRes.writeHead(502); clientRes.end(); } catch {} });
|
|
1436
|
+
clientReq.pipe(proxyReq);
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
// WebSocket upgrades: raw TCP pipe
|
|
1440
|
+
server.on('upgrade', (req, socket, head) => {
|
|
1441
|
+
const upstream = net.createConnection(UPSTREAM, 'localhost', () => {
|
|
1442
|
+
const lines = [req.method + ' ' + req.url + ' HTTP/1.1'];
|
|
1443
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
1444
|
+
lines.push(k + ': ' + (Array.isArray(v) ? v.join(', ') : v));
|
|
1466
1445
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1446
|
+
upstream.write(lines.join('\\r\\n') + '\\r\\n\\r\\n');
|
|
1447
|
+
if (head.length) upstream.write(head);
|
|
1448
|
+
socket.pipe(upstream);
|
|
1449
|
+
upstream.pipe(socket);
|
|
1450
|
+
});
|
|
1451
|
+
upstream.on('error', () => socket.destroy());
|
|
1452
|
+
socket.on('error', () => upstream.destroy());
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
server.listen(${proxyPort}, () => {
|
|
1456
|
+
if (process.send) process.send('ready');
|
|
1457
|
+
});
|
|
1458
|
+
`;
|
|
1459
|
+
const child = spawn5("node", ["-e", proxyScript], {
|
|
1460
|
+
stdio: ["ignore", "inherit", "inherit", "ipc"]
|
|
1461
|
+
});
|
|
1462
|
+
child.on("error", (err) => {
|
|
1463
|
+
logger.error("proxy", `Failed to start proxy: ${err.message}`);
|
|
1464
|
+
});
|
|
1465
|
+
child.on("message", (msg) => {
|
|
1466
|
+
if (msg === "ready") {
|
|
1467
|
+
logger.info("proxy", `App proxy at http://localhost:${proxyPort} \u2192 http://localhost:${userDevPort}`);
|
|
1474
1468
|
}
|
|
1469
|
+
});
|
|
1470
|
+
return child;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// ../server/dist/services/overlay-script.js
|
|
1474
|
+
function injectOverlayScript(html, _upstreamPort, _proxyPort) {
|
|
1475
|
+
const overlayScript = `
|
|
1476
|
+
<script data-stashes-overlay>
|
|
1477
|
+
(function() {
|
|
1478
|
+
var highlightOverlay = null;
|
|
1479
|
+
var pickerEnabled = false;
|
|
1480
|
+
var precisionMode = false;
|
|
1481
|
+
|
|
1482
|
+
function createOverlay() {
|
|
1483
|
+
var overlay = document.createElement('div');
|
|
1484
|
+
overlay.id = 'stashes-highlight';
|
|
1485
|
+
overlay.style.cssText = 'position:fixed;pointer-events:none;border:2px solid #6366f1;background:rgba(99,102,241,0.1);z-index:99999;transition:all 0.1s ease;display:none;border-radius:4px;';
|
|
1486
|
+
var tooltip = document.createElement('div');
|
|
1487
|
+
tooltip.id = 'stashes-tooltip';
|
|
1488
|
+
tooltip.style.cssText = 'position:fixed;background:#1e1b4b;color:#e0e7ff;padding:4px 10px;border-radius:6px;font-size:11px;font-family:ui-monospace,monospace;z-index:100000;pointer-events:none;display:none;white-space:nowrap;box-shadow:0 4px 12px rgba(0,0,0,0.3);max-width:400px;overflow:hidden;text-overflow:ellipsis;';
|
|
1489
|
+
document.body.appendChild(overlay);
|
|
1490
|
+
document.body.appendChild(tooltip);
|
|
1491
|
+
return overlay;
|
|
1475
1492
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1493
|
+
|
|
1494
|
+
var SEMANTIC_TAGS = ['header','nav','main','section','article','aside','footer','form','dialog'];
|
|
1495
|
+
var LEAF_TAGS = ['h1','h2','h3','h4','h5','h6','p','button','a','input','textarea','select','img','svg','video','label','li','td','th','figcaption','blockquote','pre','code','span'];
|
|
1496
|
+
|
|
1497
|
+
function findTarget(el, precise) {
|
|
1498
|
+
if (precise) return el;
|
|
1499
|
+
// If the element itself is a meaningful leaf, select it directly
|
|
1500
|
+
var elTag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
1501
|
+
if (LEAF_TAGS.indexOf(elTag) !== -1) return el;
|
|
1502
|
+
var current = el;
|
|
1503
|
+
var best = el;
|
|
1504
|
+
while (current && current !== document.body) {
|
|
1505
|
+
var tag = current.tagName.toLowerCase();
|
|
1506
|
+
if (LEAF_TAGS.indexOf(tag) !== -1) { best = current; break; }
|
|
1507
|
+
if (SEMANTIC_TAGS.indexOf(tag) !== -1) { best = current; break; }
|
|
1508
|
+
if (current.id) { best = current; break; }
|
|
1509
|
+
if (current.getAttribute('role')) { best = current; break; }
|
|
1510
|
+
if (current.getAttribute('data-testid')) { best = current; break; }
|
|
1511
|
+
if (current.children && current.children.length > 1 && current.getBoundingClientRect().height > 50) {
|
|
1512
|
+
best = current;
|
|
1513
|
+
break;
|
|
1482
1514
|
}
|
|
1515
|
+
current = current.parentElement;
|
|
1483
1516
|
}
|
|
1484
|
-
|
|
1485
|
-
logger.info("pool", `reaping inactive: ${stashId}`);
|
|
1486
|
-
const entry = this.entries.get(stashId);
|
|
1487
|
-
this.killEntry(entry);
|
|
1488
|
-
this.entries.delete(stashId);
|
|
1489
|
-
this.usedPorts.delete(entry.port);
|
|
1490
|
-
this.worktreeManager.removePreviewForPool(stashId).catch((err) => {
|
|
1491
|
-
logger.warn("pool", `reap worktree cleanup failed for ${stashId}`, {
|
|
1492
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1493
|
-
});
|
|
1494
|
-
});
|
|
1495
|
-
this.broadcast({ type: "stash:preview_stopped", stashId });
|
|
1496
|
-
}
|
|
1517
|
+
return best;
|
|
1497
1518
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1519
|
+
|
|
1520
|
+
function describeElement(el) {
|
|
1521
|
+
var tag = el.tagName.toLowerCase();
|
|
1522
|
+
var id = el.id ? '#' + el.id : '';
|
|
1523
|
+
var role = el.getAttribute('role') || '';
|
|
1524
|
+
var testId = el.getAttribute('data-testid') || '';
|
|
1525
|
+
var cls = (el.className && typeof el.className === 'string') ? el.className.trim().split(/[ ]+/).slice(0, 2).join('.') : '';
|
|
1526
|
+
var text = (el.textContent || '').trim().substring(0, 40);
|
|
1527
|
+
var label = tag;
|
|
1528
|
+
if (SEMANTIC_TAGS.indexOf(tag) !== -1) label = tag.toUpperCase();
|
|
1529
|
+
if (id) label += ' ' + id;
|
|
1530
|
+
else if (cls) label += '.' + cls;
|
|
1531
|
+
if (role) label += ' [' + role + ']';
|
|
1532
|
+
if (testId) label += ' [' + testId + ']';
|
|
1533
|
+
if (!id && !cls && text) label += ' "' + text.substring(0, 25) + '"';
|
|
1534
|
+
return label;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function generateSelector(el) {
|
|
1538
|
+
if (el.id) return '#' + el.id;
|
|
1539
|
+
var parts = [];
|
|
1540
|
+
var current = el;
|
|
1541
|
+
var depth = 0;
|
|
1542
|
+
while (current && current !== document.body && depth < 5) {
|
|
1543
|
+
var sel = current.tagName.toLowerCase();
|
|
1544
|
+
if (current.id) { parts.unshift('#' + current.id); break; }
|
|
1545
|
+
if (current.className && typeof current.className === 'string') {
|
|
1546
|
+
var c = current.className.trim().split(/[ ]+/).slice(0, 2).join('.');
|
|
1547
|
+
if (c) sel += '.' + c;
|
|
1548
|
+
}
|
|
1549
|
+
parts.unshift(sel);
|
|
1550
|
+
current = current.parentElement;
|
|
1551
|
+
depth++;
|
|
1552
|
+
}
|
|
1553
|
+
return parts.join(' > ');
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function onMouseMove(e) {
|
|
1557
|
+
if (!pickerEnabled) return;
|
|
1558
|
+
if (!highlightOverlay) highlightOverlay = createOverlay();
|
|
1559
|
+
precisionMode = e.shiftKey;
|
|
1560
|
+
var target = findTarget(e.target, precisionMode);
|
|
1561
|
+
var overlay = document.getElementById('stashes-highlight');
|
|
1562
|
+
var tooltip = document.getElementById('stashes-tooltip');
|
|
1563
|
+
if (target) {
|
|
1564
|
+
var rect = target.getBoundingClientRect();
|
|
1565
|
+
overlay.style.display = 'block';
|
|
1566
|
+
overlay.style.top = rect.top + 'px';
|
|
1567
|
+
overlay.style.left = rect.left + 'px';
|
|
1568
|
+
overlay.style.width = rect.width + 'px';
|
|
1569
|
+
overlay.style.height = rect.height + 'px';
|
|
1570
|
+
overlay.style.borderColor = precisionMode ? '#f59e0b' : '#6366f1';
|
|
1571
|
+
tooltip.style.display = 'block';
|
|
1572
|
+
tooltip.style.top = Math.max(0, rect.top - 30) + 'px';
|
|
1573
|
+
tooltip.style.left = Math.max(0, rect.left) + 'px';
|
|
1574
|
+
tooltip.textContent = (precisionMode ? '[precise] ' : '') + describeElement(target);
|
|
1575
|
+
} else {
|
|
1576
|
+
overlay.style.display = 'none';
|
|
1577
|
+
tooltip.style.display = 'none';
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function onClick(e) {
|
|
1582
|
+
if (!pickerEnabled) return;
|
|
1583
|
+
e.preventDefault();
|
|
1584
|
+
e.stopPropagation();
|
|
1585
|
+
var target = findTarget(e.target, e.shiftKey);
|
|
1586
|
+
if (target) {
|
|
1587
|
+
var desc = describeElement(target);
|
|
1588
|
+
var selector = generateSelector(target);
|
|
1589
|
+
var tag = target.tagName.toLowerCase();
|
|
1590
|
+
var outerSnippet = target.outerHTML.substring(0, 500);
|
|
1591
|
+
window.parent.postMessage({
|
|
1592
|
+
type: 'stashes:component_selected',
|
|
1593
|
+
component: {
|
|
1594
|
+
name: desc,
|
|
1595
|
+
filePath: 'auto-detect',
|
|
1596
|
+
domSelector: selector,
|
|
1597
|
+
htmlSnippet: outerSnippet,
|
|
1598
|
+
tag: tag
|
|
1599
|
+
}
|
|
1600
|
+
}, '*');
|
|
1601
|
+
var overlay = document.getElementById('stashes-highlight');
|
|
1602
|
+
if (overlay) {
|
|
1603
|
+
overlay.style.borderColor = '#22c55e';
|
|
1604
|
+
overlay.style.background = 'rgba(34,197,94,0.1)';
|
|
1605
|
+
setTimeout(function() {
|
|
1606
|
+
overlay.style.borderColor = '#6366f1';
|
|
1607
|
+
overlay.style.background = 'rgba(99,102,241,0.1)';
|
|
1608
|
+
}, 500);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
window.addEventListener('message', function(e) {
|
|
1614
|
+
if (!e.data || !e.data.type) return;
|
|
1615
|
+
if (e.data.type === 'stashes:toggle_picker') {
|
|
1616
|
+
pickerEnabled = e.data.enabled;
|
|
1617
|
+
if (!pickerEnabled) {
|
|
1618
|
+
var ov = document.getElementById('stashes-highlight');
|
|
1619
|
+
var tp = document.getElementById('stashes-tooltip');
|
|
1620
|
+
if (ov) ov.style.display = 'none';
|
|
1621
|
+
if (tp) tp.style.display = 'none';
|
|
1622
|
+
}
|
|
1623
|
+
} else if (e.data.type === 'stashes:navigate_back') {
|
|
1624
|
+
history.back();
|
|
1625
|
+
} else if (e.data.type === 'stashes:navigate_forward') {
|
|
1626
|
+
history.forward();
|
|
1627
|
+
} else if (e.data.type === 'stashes:refresh') {
|
|
1628
|
+
location.reload();
|
|
1629
|
+
} else if (e.data.type === 'stashes:navigate_to') {
|
|
1630
|
+
window.location.href = e.data.url;
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
// Report current URL to parent for status bar display
|
|
1635
|
+
function reportUrl() {
|
|
1636
|
+
window.parent.postMessage({
|
|
1637
|
+
type: 'stashes:url_change',
|
|
1638
|
+
url: window.location.pathname + window.location.search + window.location.hash
|
|
1639
|
+
}, '*');
|
|
1640
|
+
}
|
|
1641
|
+
reportUrl();
|
|
1642
|
+
var origPush = history.pushState;
|
|
1643
|
+
history.pushState = function() {
|
|
1644
|
+
origPush.apply(this, arguments);
|
|
1645
|
+
setTimeout(reportUrl, 0);
|
|
1646
|
+
};
|
|
1647
|
+
var origReplace = history.replaceState;
|
|
1648
|
+
history.replaceState = function() {
|
|
1649
|
+
origReplace.apply(this, arguments);
|
|
1650
|
+
setTimeout(reportUrl, 0);
|
|
1651
|
+
};
|
|
1652
|
+
window.addEventListener('popstate', reportUrl);
|
|
1653
|
+
|
|
1654
|
+
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
1655
|
+
document.addEventListener('click', onClick, true);
|
|
1656
|
+
})();
|
|
1657
|
+
</script>`;
|
|
1658
|
+
if (html.includes("</body>")) {
|
|
1659
|
+
return html.replace("</body>", () => overlayScript + `
|
|
1660
|
+
</body>`);
|
|
1661
|
+
}
|
|
1662
|
+
return html + overlayScript;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// ../server/dist/services/preview-pool.js
|
|
1666
|
+
var DEV_PORT_OFFSET = 1000;
|
|
1667
|
+
|
|
1668
|
+
class PreviewPool {
|
|
1669
|
+
entries = new Map;
|
|
1670
|
+
usedPorts = new Set;
|
|
1671
|
+
maxSize;
|
|
1672
|
+
ttlMs;
|
|
1673
|
+
worktreeManager;
|
|
1674
|
+
broadcast;
|
|
1675
|
+
reaperInterval;
|
|
1676
|
+
constructor(worktreeManager, broadcast, maxSize = MAX_PREVIEW_SERVERS, ttlMs = PREVIEW_TTL_MS) {
|
|
1677
|
+
this.worktreeManager = worktreeManager;
|
|
1678
|
+
this.broadcast = broadcast;
|
|
1679
|
+
this.maxSize = maxSize;
|
|
1680
|
+
this.ttlMs = ttlMs;
|
|
1681
|
+
this.reaperInterval = setInterval(() => this.reap(), PREVIEW_REAPER_INTERVAL);
|
|
1682
|
+
}
|
|
1683
|
+
async getOrStart(stashId) {
|
|
1684
|
+
const existing = this.entries.get(stashId);
|
|
1685
|
+
if (existing) {
|
|
1686
|
+
existing.lastHeartbeat = Date.now();
|
|
1687
|
+
logger.info("pool", `warm hit: ${stashId} on port ${existing.port}`);
|
|
1688
|
+
return existing.port;
|
|
1689
|
+
}
|
|
1690
|
+
if (this.entries.size >= this.maxSize) {
|
|
1691
|
+
this.evictOldest();
|
|
1692
|
+
}
|
|
1693
|
+
const proxyPort = this.allocatePort();
|
|
1694
|
+
const devPort = proxyPort + DEV_PORT_OFFSET;
|
|
1695
|
+
const worktreePath = await this.worktreeManager.createPreviewForPool(stashId);
|
|
1696
|
+
const process2 = Bun.spawn({
|
|
1697
|
+
cmd: ["npm", "run", "dev"],
|
|
1698
|
+
cwd: worktreePath,
|
|
1699
|
+
stdin: "ignore",
|
|
1700
|
+
stdout: "pipe",
|
|
1701
|
+
stderr: "pipe",
|
|
1702
|
+
env: { ...Bun.env, PORT: String(devPort), BROWSER: "none" }
|
|
1703
|
+
});
|
|
1704
|
+
const proxyProcess = startAppProxy(devPort, proxyPort, injectOverlayScript);
|
|
1705
|
+
const entry = {
|
|
1706
|
+
stashId,
|
|
1707
|
+
port: proxyPort,
|
|
1708
|
+
process: process2,
|
|
1709
|
+
proxyProcess,
|
|
1710
|
+
worktreePath,
|
|
1711
|
+
lastHeartbeat: Date.now()
|
|
1712
|
+
};
|
|
1713
|
+
this.entries.set(stashId, entry);
|
|
1714
|
+
this.usedPorts.add(proxyPort);
|
|
1715
|
+
logger.info("pool", `cold start: ${stashId} dev=:${devPort} proxy=:${proxyPort}`, { poolSize: this.entries.size });
|
|
1716
|
+
await this.waitForPort(proxyPort, 60000);
|
|
1717
|
+
return proxyPort;
|
|
1718
|
+
}
|
|
1719
|
+
heartbeat(stashId) {
|
|
1720
|
+
const entry = this.entries.get(stashId);
|
|
1721
|
+
if (entry) {
|
|
1722
|
+
entry.lastHeartbeat = Date.now();
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
isWarm(stashId) {
|
|
1726
|
+
return this.entries.has(stashId);
|
|
1727
|
+
}
|
|
1728
|
+
getPort(stashId) {
|
|
1729
|
+
return this.entries.get(stashId)?.port ?? null;
|
|
1730
|
+
}
|
|
1731
|
+
async stop(stashId) {
|
|
1732
|
+
const entry = this.entries.get(stashId);
|
|
1733
|
+
if (!entry)
|
|
1734
|
+
return;
|
|
1735
|
+
logger.info("pool", `stopping: ${stashId} on port ${entry.port}`);
|
|
1736
|
+
this.killEntry(entry);
|
|
1737
|
+
this.entries.delete(stashId);
|
|
1738
|
+
this.usedPorts.delete(entry.port);
|
|
1739
|
+
try {
|
|
1740
|
+
await this.worktreeManager.removePreviewForPool(stashId);
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
logger.warn("pool", `worktree removal failed for ${stashId}`, {
|
|
1743
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
prefetchNeighbors(currentStashId, sortedStashIds) {
|
|
1748
|
+
const currentIndex = sortedStashIds.indexOf(currentStashId);
|
|
1749
|
+
if (currentIndex === -1 || sortedStashIds.length < 2)
|
|
1750
|
+
return;
|
|
1751
|
+
const neighbors = [];
|
|
1752
|
+
const prevIndex = (currentIndex - 1 + sortedStashIds.length) % sortedStashIds.length;
|
|
1753
|
+
const nextIndex = (currentIndex + 1) % sortedStashIds.length;
|
|
1754
|
+
if (!this.entries.has(sortedStashIds[prevIndex])) {
|
|
1755
|
+
neighbors.push(sortedStashIds[prevIndex]);
|
|
1756
|
+
}
|
|
1757
|
+
if (!this.entries.has(sortedStashIds[nextIndex])) {
|
|
1758
|
+
neighbors.push(sortedStashIds[nextIndex]);
|
|
1759
|
+
}
|
|
1760
|
+
for (const stashId of neighbors) {
|
|
1761
|
+
if (this.entries.size >= this.maxSize)
|
|
1762
|
+
break;
|
|
1763
|
+
logger.info("pool", `prefetching neighbor: ${stashId}`);
|
|
1764
|
+
this.getOrStart(stashId).then((port) => {
|
|
1765
|
+
this.broadcast({ type: "stash:port", stashId, port });
|
|
1766
|
+
}).catch((err) => {
|
|
1767
|
+
logger.warn("pool", `prefetch failed for ${stashId}`, {
|
|
1768
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1769
|
+
});
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
async shutdown() {
|
|
1774
|
+
logger.info("pool", `shutting down all`, { poolSize: this.entries.size });
|
|
1775
|
+
clearInterval(this.reaperInterval);
|
|
1776
|
+
const stashIds = [...this.entries.keys()];
|
|
1777
|
+
for (const stashId of stashIds) {
|
|
1778
|
+
await this.stop(stashId);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
reap() {
|
|
1782
|
+
const now = Date.now();
|
|
1783
|
+
const expired = [];
|
|
1784
|
+
for (const [stashId, entry] of this.entries) {
|
|
1785
|
+
if (now - entry.lastHeartbeat > this.ttlMs) {
|
|
1786
|
+
expired.push(stashId);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
for (const stashId of expired) {
|
|
1790
|
+
logger.info("pool", `reaping inactive: ${stashId}`);
|
|
1791
|
+
const entry = this.entries.get(stashId);
|
|
1792
|
+
this.killEntry(entry);
|
|
1793
|
+
this.entries.delete(stashId);
|
|
1794
|
+
this.usedPorts.delete(entry.port);
|
|
1795
|
+
this.worktreeManager.removePreviewForPool(stashId).catch((err) => {
|
|
1796
|
+
logger.warn("pool", `reap worktree cleanup failed for ${stashId}`, {
|
|
1797
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1798
|
+
});
|
|
1799
|
+
});
|
|
1800
|
+
this.broadcast({ type: "stash:preview_stopped", stashId });
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
evictOldest() {
|
|
1804
|
+
let oldest = null;
|
|
1500
1805
|
for (const entry of this.entries.values()) {
|
|
1501
1806
|
if (!oldest || entry.lastHeartbeat < oldest.lastHeartbeat) {
|
|
1502
1807
|
oldest = entry;
|
|
@@ -1527,6 +1832,9 @@ class PreviewPool {
|
|
|
1527
1832
|
try {
|
|
1528
1833
|
entry.process.kill();
|
|
1529
1834
|
} catch {}
|
|
1835
|
+
try {
|
|
1836
|
+
entry.proxyProcess.kill();
|
|
1837
|
+
} catch {}
|
|
1530
1838
|
}
|
|
1531
1839
|
async waitForPort(port, timeout) {
|
|
1532
1840
|
const start = Date.now();
|
|
@@ -2031,124 +2339,34 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
|
|
|
2031
2339
|
}
|
|
2032
2340
|
}
|
|
2033
2341
|
await stashService.message(event.projectId, event.chatId, event.message, event.referenceStashIds, event.componentContext);
|
|
2034
|
-
break;
|
|
2035
|
-
}
|
|
2036
|
-
case "interact":
|
|
2037
|
-
await stashService.switchPreview(event.stashId, event.sortedStashIds);
|
|
2038
|
-
break;
|
|
2039
|
-
case "preview_heartbeat":
|
|
2040
|
-
stashService.previewHeartbeat(event.stashId);
|
|
2041
|
-
break;
|
|
2042
|
-
case "apply_stash":
|
|
2043
|
-
await stashService.applyStash(event.stashId);
|
|
2044
|
-
break;
|
|
2045
|
-
case "delete_stash":
|
|
2046
|
-
await stashService.deleteStash(event.stashId);
|
|
2047
|
-
break;
|
|
2048
|
-
}
|
|
2049
|
-
} catch (err) {
|
|
2050
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2051
|
-
logger.error("ws", `handler failed for ${event.type}`, { error: errorMsg });
|
|
2052
|
-
if ("stashId" in event && event.stashId) {
|
|
2053
|
-
broadcast({ type: "stash:error", stashId: event.stashId, error: errorMsg });
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
},
|
|
2057
|
-
close(ws) {
|
|
2058
|
-
clients.delete(ws);
|
|
2059
|
-
logger.info("ws", "client disconnected", { remaining: clients.size });
|
|
2060
|
-
}
|
|
2061
|
-
};
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
// ../server/dist/services/app-proxy.js
|
|
2065
|
-
import { spawn as spawn5 } from "child_process";
|
|
2066
|
-
function startAppProxy(userDevPort, proxyPort, injectOverlay) {
|
|
2067
|
-
const overlayScript = injectOverlay("", userDevPort, proxyPort);
|
|
2068
|
-
const overlayEscaped = JSON.stringify(overlayScript);
|
|
2069
|
-
const proxyScript = `
|
|
2070
|
-
const http = require('http');
|
|
2071
|
-
const net = require('net');
|
|
2072
|
-
const zlib = require('zlib');
|
|
2073
|
-
const UPSTREAM = ${userDevPort};
|
|
2074
|
-
const OVERLAY = ${overlayEscaped};
|
|
2075
|
-
|
|
2076
|
-
const server = http.createServer((clientReq, clientRes) => {
|
|
2077
|
-
const opts = {
|
|
2078
|
-
hostname: 'localhost',
|
|
2079
|
-
port: UPSTREAM,
|
|
2080
|
-
path: clientReq.url,
|
|
2081
|
-
method: clientReq.method,
|
|
2082
|
-
headers: clientReq.headers,
|
|
2083
|
-
};
|
|
2084
|
-
const proxyReq = http.request(opts, (proxyRes) => {
|
|
2085
|
-
const ct = proxyRes.headers['content-type'] || '';
|
|
2086
|
-
if (ct.includes('text/html')) {
|
|
2087
|
-
// Buffer HTML to inject overlay
|
|
2088
|
-
const chunks = [];
|
|
2089
|
-
proxyRes.on('data', c => chunks.push(c));
|
|
2090
|
-
proxyRes.on('end', () => {
|
|
2091
|
-
let html = Buffer.concat(chunks);
|
|
2092
|
-
const enc = proxyRes.headers['content-encoding'];
|
|
2093
|
-
// Decompress if needed
|
|
2094
|
-
if (enc === 'gzip') {
|
|
2095
|
-
try { html = zlib.gunzipSync(html); } catch {}
|
|
2096
|
-
} else if (enc === 'br') {
|
|
2097
|
-
try { html = zlib.brotliDecompressSync(html); } catch {}
|
|
2098
|
-
} else if (enc === 'deflate') {
|
|
2099
|
-
try { html = zlib.inflateSync(html); } catch {}
|
|
2342
|
+
break;
|
|
2343
|
+
}
|
|
2344
|
+
case "interact":
|
|
2345
|
+
await stashService.switchPreview(event.stashId, event.sortedStashIds);
|
|
2346
|
+
break;
|
|
2347
|
+
case "preview_heartbeat":
|
|
2348
|
+
stashService.previewHeartbeat(event.stashId);
|
|
2349
|
+
break;
|
|
2350
|
+
case "apply_stash":
|
|
2351
|
+
await stashService.applyStash(event.stashId);
|
|
2352
|
+
break;
|
|
2353
|
+
case "delete_stash":
|
|
2354
|
+
await stashService.deleteStash(event.stashId);
|
|
2355
|
+
break;
|
|
2100
2356
|
}
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
proxyRes.pipe(clientRes);
|
|
2113
|
-
}
|
|
2114
|
-
proxyRes.on('error', () => clientRes.end());
|
|
2115
|
-
});
|
|
2116
|
-
proxyReq.on('error', () => { try { clientRes.writeHead(502); clientRes.end(); } catch {} });
|
|
2117
|
-
clientReq.pipe(proxyReq);
|
|
2118
|
-
});
|
|
2119
|
-
|
|
2120
|
-
// WebSocket upgrades: raw TCP pipe
|
|
2121
|
-
server.on('upgrade', (req, socket, head) => {
|
|
2122
|
-
const upstream = net.createConnection(UPSTREAM, 'localhost', () => {
|
|
2123
|
-
const lines = [req.method + ' ' + req.url + ' HTTP/1.1'];
|
|
2124
|
-
for (const [k, v] of Object.entries(req.headers)) {
|
|
2125
|
-
lines.push(k + ': ' + (Array.isArray(v) ? v.join(', ') : v));
|
|
2126
|
-
}
|
|
2127
|
-
upstream.write(lines.join('\\r\\n') + '\\r\\n\\r\\n');
|
|
2128
|
-
if (head.length) upstream.write(head);
|
|
2129
|
-
socket.pipe(upstream);
|
|
2130
|
-
upstream.pipe(socket);
|
|
2131
|
-
});
|
|
2132
|
-
upstream.on('error', () => socket.destroy());
|
|
2133
|
-
socket.on('error', () => upstream.destroy());
|
|
2134
|
-
});
|
|
2135
|
-
|
|
2136
|
-
server.listen(${proxyPort}, () => {
|
|
2137
|
-
if (process.send) process.send('ready');
|
|
2138
|
-
});
|
|
2139
|
-
`;
|
|
2140
|
-
const child = spawn5("node", ["-e", proxyScript], {
|
|
2141
|
-
stdio: ["ignore", "inherit", "inherit", "ipc"]
|
|
2142
|
-
});
|
|
2143
|
-
child.on("error", (err) => {
|
|
2144
|
-
logger.error("proxy", `Failed to start proxy: ${err.message}`);
|
|
2145
|
-
});
|
|
2146
|
-
child.on("message", (msg) => {
|
|
2147
|
-
if (msg === "ready") {
|
|
2148
|
-
logger.info("proxy", `App proxy at http://localhost:${proxyPort} \u2192 http://localhost:${userDevPort}`);
|
|
2357
|
+
} catch (err) {
|
|
2358
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2359
|
+
logger.error("ws", `handler failed for ${event.type}`, { error: errorMsg });
|
|
2360
|
+
if ("stashId" in event && event.stashId) {
|
|
2361
|
+
broadcast({ type: "stash:error", stashId: event.stashId, error: errorMsg });
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
},
|
|
2365
|
+
close(ws) {
|
|
2366
|
+
clients.delete(ws);
|
|
2367
|
+
logger.info("ws", "client disconnected", { remaining: clients.size });
|
|
2149
2368
|
}
|
|
2150
|
-
}
|
|
2151
|
-
return child;
|
|
2369
|
+
};
|
|
2152
2370
|
}
|
|
2153
2371
|
|
|
2154
2372
|
// ../server/dist/index.js
|
|
@@ -2226,187 +2444,6 @@ function startServer(projectPath, userDevPort, port = STASHES_PORT) {
|
|
|
2226
2444
|
logger.info("server", `Project: ${projectPath}`);
|
|
2227
2445
|
return server;
|
|
2228
2446
|
}
|
|
2229
|
-
function injectOverlayScript(html, _upstreamPort, _proxyPort) {
|
|
2230
|
-
const overlayScript = `
|
|
2231
|
-
<script data-stashes-overlay>
|
|
2232
|
-
(function() {
|
|
2233
|
-
var highlightOverlay = null;
|
|
2234
|
-
var pickerEnabled = false;
|
|
2235
|
-
var precisionMode = false;
|
|
2236
|
-
|
|
2237
|
-
function createOverlay() {
|
|
2238
|
-
var overlay = document.createElement('div');
|
|
2239
|
-
overlay.id = 'stashes-highlight';
|
|
2240
|
-
overlay.style.cssText = 'position:fixed;pointer-events:none;border:2px solid #6366f1;background:rgba(99,102,241,0.1);z-index:99999;transition:all 0.1s ease;display:none;border-radius:4px;';
|
|
2241
|
-
var tooltip = document.createElement('div');
|
|
2242
|
-
tooltip.id = 'stashes-tooltip';
|
|
2243
|
-
tooltip.style.cssText = 'position:fixed;background:#1e1b4b;color:#e0e7ff;padding:4px 10px;border-radius:6px;font-size:11px;font-family:ui-monospace,monospace;z-index:100000;pointer-events:none;display:none;white-space:nowrap;box-shadow:0 4px 12px rgba(0,0,0,0.3);max-width:400px;overflow:hidden;text-overflow:ellipsis;';
|
|
2244
|
-
document.body.appendChild(overlay);
|
|
2245
|
-
document.body.appendChild(tooltip);
|
|
2246
|
-
return overlay;
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
var SEMANTIC_TAGS = ['header','nav','main','section','article','aside','footer','form','dialog'];
|
|
2250
|
-
var LEAF_TAGS = ['h1','h2','h3','h4','h5','h6','p','button','a','input','textarea','select','img','svg','video','label','li','td','th','figcaption','blockquote','pre','code','span'];
|
|
2251
|
-
|
|
2252
|
-
function findTarget(el, precise) {
|
|
2253
|
-
if (precise) return el;
|
|
2254
|
-
// If the element itself is a meaningful leaf, select it directly
|
|
2255
|
-
var elTag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
2256
|
-
if (LEAF_TAGS.indexOf(elTag) !== -1) return el;
|
|
2257
|
-
var current = el;
|
|
2258
|
-
var best = el;
|
|
2259
|
-
while (current && current !== document.body) {
|
|
2260
|
-
var tag = current.tagName.toLowerCase();
|
|
2261
|
-
if (LEAF_TAGS.indexOf(tag) !== -1) { best = current; break; }
|
|
2262
|
-
if (SEMANTIC_TAGS.indexOf(tag) !== -1) { best = current; break; }
|
|
2263
|
-
if (current.id) { best = current; break; }
|
|
2264
|
-
if (current.getAttribute('role')) { best = current; break; }
|
|
2265
|
-
if (current.getAttribute('data-testid')) { best = current; break; }
|
|
2266
|
-
if (current.children && current.children.length > 1 && current.getBoundingClientRect().height > 50) {
|
|
2267
|
-
best = current;
|
|
2268
|
-
break;
|
|
2269
|
-
}
|
|
2270
|
-
current = current.parentElement;
|
|
2271
|
-
}
|
|
2272
|
-
return best;
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
function describeElement(el) {
|
|
2276
|
-
var tag = el.tagName.toLowerCase();
|
|
2277
|
-
var id = el.id ? '#' + el.id : '';
|
|
2278
|
-
var role = el.getAttribute('role') || '';
|
|
2279
|
-
var testId = el.getAttribute('data-testid') || '';
|
|
2280
|
-
var cls = (el.className && typeof el.className === 'string') ? el.className.trim().split(/[ ]+/).slice(0, 2).join('.') : '';
|
|
2281
|
-
var text = (el.textContent || '').trim().substring(0, 40);
|
|
2282
|
-
var label = tag;
|
|
2283
|
-
if (SEMANTIC_TAGS.indexOf(tag) !== -1) label = tag.toUpperCase();
|
|
2284
|
-
if (id) label += ' ' + id;
|
|
2285
|
-
else if (cls) label += '.' + cls;
|
|
2286
|
-
if (role) label += ' [' + role + ']';
|
|
2287
|
-
if (testId) label += ' [' + testId + ']';
|
|
2288
|
-
if (!id && !cls && text) label += ' "' + text.substring(0, 25) + '"';
|
|
2289
|
-
return label;
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
function generateSelector(el) {
|
|
2293
|
-
if (el.id) return '#' + el.id;
|
|
2294
|
-
var parts = [];
|
|
2295
|
-
var current = el;
|
|
2296
|
-
var depth = 0;
|
|
2297
|
-
while (current && current !== document.body && depth < 5) {
|
|
2298
|
-
var sel = current.tagName.toLowerCase();
|
|
2299
|
-
if (current.id) { parts.unshift('#' + current.id); break; }
|
|
2300
|
-
if (current.className && typeof current.className === 'string') {
|
|
2301
|
-
var c = current.className.trim().split(/[ ]+/).slice(0, 2).join('.');
|
|
2302
|
-
if (c) sel += '.' + c;
|
|
2303
|
-
}
|
|
2304
|
-
parts.unshift(sel);
|
|
2305
|
-
current = current.parentElement;
|
|
2306
|
-
depth++;
|
|
2307
|
-
}
|
|
2308
|
-
return parts.join(' > ');
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
function onMouseMove(e) {
|
|
2312
|
-
if (!pickerEnabled) return;
|
|
2313
|
-
if (!highlightOverlay) highlightOverlay = createOverlay();
|
|
2314
|
-
precisionMode = e.shiftKey;
|
|
2315
|
-
var target = findTarget(e.target, precisionMode);
|
|
2316
|
-
var overlay = document.getElementById('stashes-highlight');
|
|
2317
|
-
var tooltip = document.getElementById('stashes-tooltip');
|
|
2318
|
-
if (target) {
|
|
2319
|
-
var rect = target.getBoundingClientRect();
|
|
2320
|
-
overlay.style.display = 'block';
|
|
2321
|
-
overlay.style.top = rect.top + 'px';
|
|
2322
|
-
overlay.style.left = rect.left + 'px';
|
|
2323
|
-
overlay.style.width = rect.width + 'px';
|
|
2324
|
-
overlay.style.height = rect.height + 'px';
|
|
2325
|
-
overlay.style.borderColor = precisionMode ? '#f59e0b' : '#6366f1';
|
|
2326
|
-
tooltip.style.display = 'block';
|
|
2327
|
-
tooltip.style.top = Math.max(0, rect.top - 30) + 'px';
|
|
2328
|
-
tooltip.style.left = Math.max(0, rect.left) + 'px';
|
|
2329
|
-
tooltip.textContent = (precisionMode ? '[precise] ' : '') + describeElement(target);
|
|
2330
|
-
} else {
|
|
2331
|
-
overlay.style.display = 'none';
|
|
2332
|
-
tooltip.style.display = 'none';
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
function onClick(e) {
|
|
2337
|
-
if (!pickerEnabled) return;
|
|
2338
|
-
e.preventDefault();
|
|
2339
|
-
e.stopPropagation();
|
|
2340
|
-
var target = findTarget(e.target, e.shiftKey);
|
|
2341
|
-
if (target) {
|
|
2342
|
-
var desc = describeElement(target);
|
|
2343
|
-
var selector = generateSelector(target);
|
|
2344
|
-
var tag = target.tagName.toLowerCase();
|
|
2345
|
-
var outerSnippet = target.outerHTML.substring(0, 500);
|
|
2346
|
-
window.parent.postMessage({
|
|
2347
|
-
type: 'stashes:component_selected',
|
|
2348
|
-
component: {
|
|
2349
|
-
name: desc,
|
|
2350
|
-
filePath: 'auto-detect',
|
|
2351
|
-
domSelector: selector,
|
|
2352
|
-
htmlSnippet: outerSnippet,
|
|
2353
|
-
tag: tag
|
|
2354
|
-
}
|
|
2355
|
-
}, '*');
|
|
2356
|
-
var overlay = document.getElementById('stashes-highlight');
|
|
2357
|
-
if (overlay) {
|
|
2358
|
-
overlay.style.borderColor = '#22c55e';
|
|
2359
|
-
overlay.style.background = 'rgba(34,197,94,0.1)';
|
|
2360
|
-
setTimeout(function() {
|
|
2361
|
-
overlay.style.borderColor = '#6366f1';
|
|
2362
|
-
overlay.style.background = 'rgba(99,102,241,0.1)';
|
|
2363
|
-
}, 500);
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
window.addEventListener('message', function(e) {
|
|
2369
|
-
if (e.data && e.data.type === 'stashes:toggle_picker') {
|
|
2370
|
-
pickerEnabled = e.data.enabled;
|
|
2371
|
-
if (!pickerEnabled) {
|
|
2372
|
-
var ov = document.getElementById('stashes-highlight');
|
|
2373
|
-
var tp = document.getElementById('stashes-tooltip');
|
|
2374
|
-
if (ov) ov.style.display = 'none';
|
|
2375
|
-
if (tp) tp.style.display = 'none';
|
|
2376
|
-
}
|
|
2377
|
-
}
|
|
2378
|
-
});
|
|
2379
|
-
|
|
2380
|
-
// Report current URL to parent for status bar display
|
|
2381
|
-
function reportUrl() {
|
|
2382
|
-
window.parent.postMessage({
|
|
2383
|
-
type: 'stashes:url_change',
|
|
2384
|
-
url: window.location.pathname + window.location.search + window.location.hash
|
|
2385
|
-
}, '*');
|
|
2386
|
-
}
|
|
2387
|
-
reportUrl();
|
|
2388
|
-
var origPush = history.pushState;
|
|
2389
|
-
history.pushState = function() {
|
|
2390
|
-
origPush.apply(this, arguments);
|
|
2391
|
-
setTimeout(reportUrl, 0);
|
|
2392
|
-
};
|
|
2393
|
-
var origReplace = history.replaceState;
|
|
2394
|
-
history.replaceState = function() {
|
|
2395
|
-
origReplace.apply(this, arguments);
|
|
2396
|
-
setTimeout(reportUrl, 0);
|
|
2397
|
-
};
|
|
2398
|
-
window.addEventListener('popstate', reportUrl);
|
|
2399
|
-
|
|
2400
|
-
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
2401
|
-
document.addEventListener('click', onClick, true);
|
|
2402
|
-
})();
|
|
2403
|
-
</script>`;
|
|
2404
|
-
if (html.includes("</body>")) {
|
|
2405
|
-
return html.replace("</body>", () => overlayScript + `
|
|
2406
|
-
</body>`);
|
|
2407
|
-
}
|
|
2408
|
-
return html + overlayScript;
|
|
2409
|
-
}
|
|
2410
2447
|
|
|
2411
2448
|
// ../server/dist/services/detector.js
|
|
2412
2449
|
import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
|