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/mcp.js
CHANGED
|
@@ -1498,18 +1498,35 @@ app.get("/chats", (c) => {
|
|
|
1498
1498
|
app.post("/chats", async (c) => {
|
|
1499
1499
|
const persistence = getPersistence();
|
|
1500
1500
|
const project = ensureProject(persistence);
|
|
1501
|
-
const { title } = await c.req.json();
|
|
1501
|
+
const { title, referencedStashIds } = await c.req.json();
|
|
1502
1502
|
const chatCount = persistence.listChats(project.id).length;
|
|
1503
1503
|
const chat = {
|
|
1504
1504
|
id: `chat_${crypto.randomUUID().substring(0, 8)}`,
|
|
1505
1505
|
projectId: project.id,
|
|
1506
1506
|
title: title?.trim() || `Chat ${chatCount + 1}`,
|
|
1507
|
+
referencedStashIds: referencedStashIds ?? [],
|
|
1507
1508
|
createdAt: new Date().toISOString(),
|
|
1508
1509
|
updatedAt: new Date().toISOString()
|
|
1509
1510
|
};
|
|
1510
1511
|
persistence.saveChat(chat);
|
|
1511
1512
|
return c.json({ data: chat }, 201);
|
|
1512
1513
|
});
|
|
1514
|
+
app.patch("/chats/:chatId", async (c) => {
|
|
1515
|
+
const persistence = getPersistence();
|
|
1516
|
+
const project = ensureProject(persistence);
|
|
1517
|
+
const chatId = c.req.param("chatId");
|
|
1518
|
+
const chat = persistence.getChat(project.id, chatId);
|
|
1519
|
+
if (!chat)
|
|
1520
|
+
return c.json({ error: "Chat not found" }, 404);
|
|
1521
|
+
const body = await c.req.json();
|
|
1522
|
+
const updated = {
|
|
1523
|
+
...chat,
|
|
1524
|
+
...body.referencedStashIds !== undefined ? { referencedStashIds: body.referencedStashIds } : {},
|
|
1525
|
+
updatedAt: new Date().toISOString()
|
|
1526
|
+
};
|
|
1527
|
+
persistence.saveChat(updated);
|
|
1528
|
+
return c.json({ data: updated });
|
|
1529
|
+
});
|
|
1513
1530
|
app.get("/chats/:chatId", (c) => {
|
|
1514
1531
|
const persistence = getPersistence();
|
|
1515
1532
|
const project = ensureProject(persistence);
|
|
@@ -1518,7 +1535,8 @@ app.get("/chats/:chatId", (c) => {
|
|
|
1518
1535
|
if (!chat)
|
|
1519
1536
|
return c.json({ error: "Chat not found" }, 404);
|
|
1520
1537
|
const messages = persistence.getChatMessages(project.id, chatId);
|
|
1521
|
-
const
|
|
1538
|
+
const refIds = new Set(chat.referencedStashIds ?? []);
|
|
1539
|
+
const stashes = persistence.listStashes(project.id).filter((s) => s.originChatId === chatId || refIds.has(s.id));
|
|
1522
1540
|
return c.json({ data: { ...chat, messages, stashes } });
|
|
1523
1541
|
});
|
|
1524
1542
|
app.delete("/chats/:chatId", (c) => {
|
|
@@ -1558,7 +1576,291 @@ var apiRoutes = app;
|
|
|
1558
1576
|
import { readFileSync as readFileSync4, existsSync as existsSync8 } from "fs";
|
|
1559
1577
|
import { join as join8 } from "path";
|
|
1560
1578
|
|
|
1579
|
+
// ../server/dist/services/app-proxy.js
|
|
1580
|
+
import { spawn as spawn5 } from "child_process";
|
|
1581
|
+
function startAppProxy(userDevPort, proxyPort, injectOverlay) {
|
|
1582
|
+
const overlayScript = injectOverlay("", userDevPort, proxyPort);
|
|
1583
|
+
const overlayEscaped = JSON.stringify(overlayScript);
|
|
1584
|
+
const proxyScript = `
|
|
1585
|
+
const http = require('http');
|
|
1586
|
+
const net = require('net');
|
|
1587
|
+
const zlib = require('zlib');
|
|
1588
|
+
const UPSTREAM = ${userDevPort};
|
|
1589
|
+
const OVERLAY = ${overlayEscaped};
|
|
1590
|
+
|
|
1591
|
+
const server = http.createServer((clientReq, clientRes) => {
|
|
1592
|
+
const opts = {
|
|
1593
|
+
hostname: 'localhost',
|
|
1594
|
+
port: UPSTREAM,
|
|
1595
|
+
path: clientReq.url,
|
|
1596
|
+
method: clientReq.method,
|
|
1597
|
+
headers: clientReq.headers,
|
|
1598
|
+
};
|
|
1599
|
+
const proxyReq = http.request(opts, (proxyRes) => {
|
|
1600
|
+
const ct = proxyRes.headers['content-type'] || '';
|
|
1601
|
+
if (ct.includes('text/html')) {
|
|
1602
|
+
// Buffer HTML to inject overlay
|
|
1603
|
+
const chunks = [];
|
|
1604
|
+
proxyRes.on('data', c => chunks.push(c));
|
|
1605
|
+
proxyRes.on('end', () => {
|
|
1606
|
+
let html = Buffer.concat(chunks);
|
|
1607
|
+
const enc = proxyRes.headers['content-encoding'];
|
|
1608
|
+
// Decompress if needed
|
|
1609
|
+
if (enc === 'gzip') {
|
|
1610
|
+
try { html = zlib.gunzipSync(html); } catch {}
|
|
1611
|
+
} else if (enc === 'br') {
|
|
1612
|
+
try { html = zlib.brotliDecompressSync(html); } catch {}
|
|
1613
|
+
} else if (enc === 'deflate') {
|
|
1614
|
+
try { html = zlib.inflateSync(html); } catch {}
|
|
1615
|
+
}
|
|
1616
|
+
const hdrs = { ...proxyRes.headers };
|
|
1617
|
+
delete hdrs['content-length'];
|
|
1618
|
+
delete hdrs['content-encoding'];
|
|
1619
|
+
delete hdrs['transfer-encoding'];
|
|
1620
|
+
clientRes.writeHead(proxyRes.statusCode, hdrs);
|
|
1621
|
+
clientRes.write(html);
|
|
1622
|
+
clientRes.end(OVERLAY);
|
|
1623
|
+
});
|
|
1624
|
+
} else {
|
|
1625
|
+
// Non-HTML: stream through unchanged
|
|
1626
|
+
clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
1627
|
+
proxyRes.pipe(clientRes);
|
|
1628
|
+
}
|
|
1629
|
+
proxyRes.on('error', () => clientRes.end());
|
|
1630
|
+
});
|
|
1631
|
+
proxyReq.on('error', () => { try { clientRes.writeHead(502); clientRes.end(); } catch {} });
|
|
1632
|
+
clientReq.pipe(proxyReq);
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
// WebSocket upgrades: raw TCP pipe
|
|
1636
|
+
server.on('upgrade', (req, socket, head) => {
|
|
1637
|
+
const upstream = net.createConnection(UPSTREAM, 'localhost', () => {
|
|
1638
|
+
const lines = [req.method + ' ' + req.url + ' HTTP/1.1'];
|
|
1639
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
1640
|
+
lines.push(k + ': ' + (Array.isArray(v) ? v.join(', ') : v));
|
|
1641
|
+
}
|
|
1642
|
+
upstream.write(lines.join('\\r\\n') + '\\r\\n\\r\\n');
|
|
1643
|
+
if (head.length) upstream.write(head);
|
|
1644
|
+
socket.pipe(upstream);
|
|
1645
|
+
upstream.pipe(socket);
|
|
1646
|
+
});
|
|
1647
|
+
upstream.on('error', () => socket.destroy());
|
|
1648
|
+
socket.on('error', () => upstream.destroy());
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
server.listen(${proxyPort}, () => {
|
|
1652
|
+
if (process.send) process.send('ready');
|
|
1653
|
+
});
|
|
1654
|
+
`;
|
|
1655
|
+
const child = spawn5("node", ["-e", proxyScript], {
|
|
1656
|
+
stdio: ["ignore", "inherit", "inherit", "ipc"]
|
|
1657
|
+
});
|
|
1658
|
+
child.on("error", (err) => {
|
|
1659
|
+
logger.error("proxy", `Failed to start proxy: ${err.message}`);
|
|
1660
|
+
});
|
|
1661
|
+
child.on("message", (msg) => {
|
|
1662
|
+
if (msg === "ready") {
|
|
1663
|
+
logger.info("proxy", `App proxy at http://localhost:${proxyPort} \u2192 http://localhost:${userDevPort}`);
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
return child;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// ../server/dist/services/overlay-script.js
|
|
1670
|
+
function injectOverlayScript(html, _upstreamPort, _proxyPort) {
|
|
1671
|
+
const overlayScript = `
|
|
1672
|
+
<script data-stashes-overlay>
|
|
1673
|
+
(function() {
|
|
1674
|
+
var highlightOverlay = null;
|
|
1675
|
+
var pickerEnabled = false;
|
|
1676
|
+
var precisionMode = false;
|
|
1677
|
+
|
|
1678
|
+
function createOverlay() {
|
|
1679
|
+
var overlay = document.createElement('div');
|
|
1680
|
+
overlay.id = 'stashes-highlight';
|
|
1681
|
+
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;';
|
|
1682
|
+
var tooltip = document.createElement('div');
|
|
1683
|
+
tooltip.id = 'stashes-tooltip';
|
|
1684
|
+
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;';
|
|
1685
|
+
document.body.appendChild(overlay);
|
|
1686
|
+
document.body.appendChild(tooltip);
|
|
1687
|
+
return overlay;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
var SEMANTIC_TAGS = ['header','nav','main','section','article','aside','footer','form','dialog'];
|
|
1691
|
+
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'];
|
|
1692
|
+
|
|
1693
|
+
function findTarget(el, precise) {
|
|
1694
|
+
if (precise) return el;
|
|
1695
|
+
// If the element itself is a meaningful leaf, select it directly
|
|
1696
|
+
var elTag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
1697
|
+
if (LEAF_TAGS.indexOf(elTag) !== -1) return el;
|
|
1698
|
+
var current = el;
|
|
1699
|
+
var best = el;
|
|
1700
|
+
while (current && current !== document.body) {
|
|
1701
|
+
var tag = current.tagName.toLowerCase();
|
|
1702
|
+
if (LEAF_TAGS.indexOf(tag) !== -1) { best = current; break; }
|
|
1703
|
+
if (SEMANTIC_TAGS.indexOf(tag) !== -1) { best = current; break; }
|
|
1704
|
+
if (current.id) { best = current; break; }
|
|
1705
|
+
if (current.getAttribute('role')) { best = current; break; }
|
|
1706
|
+
if (current.getAttribute('data-testid')) { best = current; break; }
|
|
1707
|
+
if (current.children && current.children.length > 1 && current.getBoundingClientRect().height > 50) {
|
|
1708
|
+
best = current;
|
|
1709
|
+
break;
|
|
1710
|
+
}
|
|
1711
|
+
current = current.parentElement;
|
|
1712
|
+
}
|
|
1713
|
+
return best;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function describeElement(el) {
|
|
1717
|
+
var tag = el.tagName.toLowerCase();
|
|
1718
|
+
var id = el.id ? '#' + el.id : '';
|
|
1719
|
+
var role = el.getAttribute('role') || '';
|
|
1720
|
+
var testId = el.getAttribute('data-testid') || '';
|
|
1721
|
+
var cls = (el.className && typeof el.className === 'string') ? el.className.trim().split(/[ ]+/).slice(0, 2).join('.') : '';
|
|
1722
|
+
var text = (el.textContent || '').trim().substring(0, 40);
|
|
1723
|
+
var label = tag;
|
|
1724
|
+
if (SEMANTIC_TAGS.indexOf(tag) !== -1) label = tag.toUpperCase();
|
|
1725
|
+
if (id) label += ' ' + id;
|
|
1726
|
+
else if (cls) label += '.' + cls;
|
|
1727
|
+
if (role) label += ' [' + role + ']';
|
|
1728
|
+
if (testId) label += ' [' + testId + ']';
|
|
1729
|
+
if (!id && !cls && text) label += ' "' + text.substring(0, 25) + '"';
|
|
1730
|
+
return label;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function generateSelector(el) {
|
|
1734
|
+
if (el.id) return '#' + el.id;
|
|
1735
|
+
var parts = [];
|
|
1736
|
+
var current = el;
|
|
1737
|
+
var depth = 0;
|
|
1738
|
+
while (current && current !== document.body && depth < 5) {
|
|
1739
|
+
var sel = current.tagName.toLowerCase();
|
|
1740
|
+
if (current.id) { parts.unshift('#' + current.id); break; }
|
|
1741
|
+
if (current.className && typeof current.className === 'string') {
|
|
1742
|
+
var c = current.className.trim().split(/[ ]+/).slice(0, 2).join('.');
|
|
1743
|
+
if (c) sel += '.' + c;
|
|
1744
|
+
}
|
|
1745
|
+
parts.unshift(sel);
|
|
1746
|
+
current = current.parentElement;
|
|
1747
|
+
depth++;
|
|
1748
|
+
}
|
|
1749
|
+
return parts.join(' > ');
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function onMouseMove(e) {
|
|
1753
|
+
if (!pickerEnabled) return;
|
|
1754
|
+
if (!highlightOverlay) highlightOverlay = createOverlay();
|
|
1755
|
+
precisionMode = e.shiftKey;
|
|
1756
|
+
var target = findTarget(e.target, precisionMode);
|
|
1757
|
+
var overlay = document.getElementById('stashes-highlight');
|
|
1758
|
+
var tooltip = document.getElementById('stashes-tooltip');
|
|
1759
|
+
if (target) {
|
|
1760
|
+
var rect = target.getBoundingClientRect();
|
|
1761
|
+
overlay.style.display = 'block';
|
|
1762
|
+
overlay.style.top = rect.top + 'px';
|
|
1763
|
+
overlay.style.left = rect.left + 'px';
|
|
1764
|
+
overlay.style.width = rect.width + 'px';
|
|
1765
|
+
overlay.style.height = rect.height + 'px';
|
|
1766
|
+
overlay.style.borderColor = precisionMode ? '#f59e0b' : '#6366f1';
|
|
1767
|
+
tooltip.style.display = 'block';
|
|
1768
|
+
tooltip.style.top = Math.max(0, rect.top - 30) + 'px';
|
|
1769
|
+
tooltip.style.left = Math.max(0, rect.left) + 'px';
|
|
1770
|
+
tooltip.textContent = (precisionMode ? '[precise] ' : '') + describeElement(target);
|
|
1771
|
+
} else {
|
|
1772
|
+
overlay.style.display = 'none';
|
|
1773
|
+
tooltip.style.display = 'none';
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function onClick(e) {
|
|
1778
|
+
if (!pickerEnabled) return;
|
|
1779
|
+
e.preventDefault();
|
|
1780
|
+
e.stopPropagation();
|
|
1781
|
+
var target = findTarget(e.target, e.shiftKey);
|
|
1782
|
+
if (target) {
|
|
1783
|
+
var desc = describeElement(target);
|
|
1784
|
+
var selector = generateSelector(target);
|
|
1785
|
+
var tag = target.tagName.toLowerCase();
|
|
1786
|
+
var outerSnippet = target.outerHTML.substring(0, 500);
|
|
1787
|
+
window.parent.postMessage({
|
|
1788
|
+
type: 'stashes:component_selected',
|
|
1789
|
+
component: {
|
|
1790
|
+
name: desc,
|
|
1791
|
+
filePath: 'auto-detect',
|
|
1792
|
+
domSelector: selector,
|
|
1793
|
+
htmlSnippet: outerSnippet,
|
|
1794
|
+
tag: tag
|
|
1795
|
+
}
|
|
1796
|
+
}, '*');
|
|
1797
|
+
var overlay = document.getElementById('stashes-highlight');
|
|
1798
|
+
if (overlay) {
|
|
1799
|
+
overlay.style.borderColor = '#22c55e';
|
|
1800
|
+
overlay.style.background = 'rgba(34,197,94,0.1)';
|
|
1801
|
+
setTimeout(function() {
|
|
1802
|
+
overlay.style.borderColor = '#6366f1';
|
|
1803
|
+
overlay.style.background = 'rgba(99,102,241,0.1)';
|
|
1804
|
+
}, 500);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
window.addEventListener('message', function(e) {
|
|
1810
|
+
if (!e.data || !e.data.type) return;
|
|
1811
|
+
if (e.data.type === 'stashes:toggle_picker') {
|
|
1812
|
+
pickerEnabled = e.data.enabled;
|
|
1813
|
+
if (!pickerEnabled) {
|
|
1814
|
+
var ov = document.getElementById('stashes-highlight');
|
|
1815
|
+
var tp = document.getElementById('stashes-tooltip');
|
|
1816
|
+
if (ov) ov.style.display = 'none';
|
|
1817
|
+
if (tp) tp.style.display = 'none';
|
|
1818
|
+
}
|
|
1819
|
+
} else if (e.data.type === 'stashes:navigate_back') {
|
|
1820
|
+
history.back();
|
|
1821
|
+
} else if (e.data.type === 'stashes:navigate_forward') {
|
|
1822
|
+
history.forward();
|
|
1823
|
+
} else if (e.data.type === 'stashes:refresh') {
|
|
1824
|
+
location.reload();
|
|
1825
|
+
} else if (e.data.type === 'stashes:navigate_to') {
|
|
1826
|
+
window.location.href = e.data.url;
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1830
|
+
// Report current URL to parent for status bar display
|
|
1831
|
+
function reportUrl() {
|
|
1832
|
+
window.parent.postMessage({
|
|
1833
|
+
type: 'stashes:url_change',
|
|
1834
|
+
url: window.location.pathname + window.location.search + window.location.hash
|
|
1835
|
+
}, '*');
|
|
1836
|
+
}
|
|
1837
|
+
reportUrl();
|
|
1838
|
+
var origPush = history.pushState;
|
|
1839
|
+
history.pushState = function() {
|
|
1840
|
+
origPush.apply(this, arguments);
|
|
1841
|
+
setTimeout(reportUrl, 0);
|
|
1842
|
+
};
|
|
1843
|
+
var origReplace = history.replaceState;
|
|
1844
|
+
history.replaceState = function() {
|
|
1845
|
+
origReplace.apply(this, arguments);
|
|
1846
|
+
setTimeout(reportUrl, 0);
|
|
1847
|
+
};
|
|
1848
|
+
window.addEventListener('popstate', reportUrl);
|
|
1849
|
+
|
|
1850
|
+
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
1851
|
+
document.addEventListener('click', onClick, true);
|
|
1852
|
+
})();
|
|
1853
|
+
</script>`;
|
|
1854
|
+
if (html.includes("</body>")) {
|
|
1855
|
+
return html.replace("</body>", () => overlayScript + `
|
|
1856
|
+
</body>`);
|
|
1857
|
+
}
|
|
1858
|
+
return html + overlayScript;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1561
1861
|
// ../server/dist/services/preview-pool.js
|
|
1862
|
+
var DEV_PORT_OFFSET = 1000;
|
|
1863
|
+
|
|
1562
1864
|
class PreviewPool {
|
|
1563
1865
|
entries = new Map;
|
|
1564
1866
|
usedPorts = new Set;
|
|
@@ -1584,7 +1886,8 @@ class PreviewPool {
|
|
|
1584
1886
|
if (this.entries.size >= this.maxSize) {
|
|
1585
1887
|
this.evictOldest();
|
|
1586
1888
|
}
|
|
1587
|
-
const
|
|
1889
|
+
const proxyPort = this.allocatePort();
|
|
1890
|
+
const devPort = proxyPort + DEV_PORT_OFFSET;
|
|
1588
1891
|
const worktreePath = await this.worktreeManager.createPreviewForPool(stashId);
|
|
1589
1892
|
const process2 = Bun.spawn({
|
|
1590
1893
|
cmd: ["npm", "run", "dev"],
|
|
@@ -1592,20 +1895,22 @@ class PreviewPool {
|
|
|
1592
1895
|
stdin: "ignore",
|
|
1593
1896
|
stdout: "pipe",
|
|
1594
1897
|
stderr: "pipe",
|
|
1595
|
-
env: { ...Bun.env, PORT: String(
|
|
1898
|
+
env: { ...Bun.env, PORT: String(devPort), BROWSER: "none" }
|
|
1596
1899
|
});
|
|
1900
|
+
const proxyProcess = startAppProxy(devPort, proxyPort, injectOverlayScript);
|
|
1597
1901
|
const entry = {
|
|
1598
1902
|
stashId,
|
|
1599
|
-
port,
|
|
1903
|
+
port: proxyPort,
|
|
1600
1904
|
process: process2,
|
|
1905
|
+
proxyProcess,
|
|
1601
1906
|
worktreePath,
|
|
1602
1907
|
lastHeartbeat: Date.now()
|
|
1603
1908
|
};
|
|
1604
1909
|
this.entries.set(stashId, entry);
|
|
1605
|
-
this.usedPorts.add(
|
|
1606
|
-
logger.info("pool", `cold start: ${stashId}
|
|
1607
|
-
await this.waitForPort(
|
|
1608
|
-
return
|
|
1910
|
+
this.usedPorts.add(proxyPort);
|
|
1911
|
+
logger.info("pool", `cold start: ${stashId} dev=:${devPort} proxy=:${proxyPort}`, { poolSize: this.entries.size });
|
|
1912
|
+
await this.waitForPort(proxyPort, 60000);
|
|
1913
|
+
return proxyPort;
|
|
1609
1914
|
}
|
|
1610
1915
|
heartbeat(stashId) {
|
|
1611
1916
|
const entry = this.entries.get(stashId);
|
|
@@ -1723,6 +2028,9 @@ class PreviewPool {
|
|
|
1723
2028
|
try {
|
|
1724
2029
|
entry.process.kill();
|
|
1725
2030
|
} catch {}
|
|
2031
|
+
try {
|
|
2032
|
+
entry.proxyProcess.kill();
|
|
2033
|
+
} catch {}
|
|
1726
2034
|
}
|
|
1727
2035
|
async waitForPort(port, timeout) {
|
|
1728
2036
|
const start = Date.now();
|
|
@@ -2227,124 +2535,34 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
|
|
|
2227
2535
|
}
|
|
2228
2536
|
}
|
|
2229
2537
|
await stashService.message(event.projectId, event.chatId, event.message, event.referenceStashIds, event.componentContext);
|
|
2230
|
-
break;
|
|
2231
|
-
}
|
|
2232
|
-
case "interact":
|
|
2233
|
-
await stashService.switchPreview(event.stashId, event.sortedStashIds);
|
|
2234
|
-
break;
|
|
2235
|
-
case "preview_heartbeat":
|
|
2236
|
-
stashService.previewHeartbeat(event.stashId);
|
|
2237
|
-
break;
|
|
2238
|
-
case "apply_stash":
|
|
2239
|
-
await stashService.applyStash(event.stashId);
|
|
2240
|
-
break;
|
|
2241
|
-
case "delete_stash":
|
|
2242
|
-
await stashService.deleteStash(event.stashId);
|
|
2243
|
-
break;
|
|
2244
|
-
}
|
|
2245
|
-
} catch (err) {
|
|
2246
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2247
|
-
logger.error("ws", `handler failed for ${event.type}`, { error: errorMsg });
|
|
2248
|
-
if ("stashId" in event && event.stashId) {
|
|
2249
|
-
broadcast({ type: "stash:error", stashId: event.stashId, error: errorMsg });
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
},
|
|
2253
|
-
close(ws) {
|
|
2254
|
-
clients.delete(ws);
|
|
2255
|
-
logger.info("ws", "client disconnected", { remaining: clients.size });
|
|
2256
|
-
}
|
|
2257
|
-
};
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
// ../server/dist/services/app-proxy.js
|
|
2261
|
-
import { spawn as spawn5 } from "child_process";
|
|
2262
|
-
function startAppProxy(userDevPort, proxyPort, injectOverlay) {
|
|
2263
|
-
const overlayScript = injectOverlay("", userDevPort, proxyPort);
|
|
2264
|
-
const overlayEscaped = JSON.stringify(overlayScript);
|
|
2265
|
-
const proxyScript = `
|
|
2266
|
-
const http = require('http');
|
|
2267
|
-
const net = require('net');
|
|
2268
|
-
const zlib = require('zlib');
|
|
2269
|
-
const UPSTREAM = ${userDevPort};
|
|
2270
|
-
const OVERLAY = ${overlayEscaped};
|
|
2271
|
-
|
|
2272
|
-
const server = http.createServer((clientReq, clientRes) => {
|
|
2273
|
-
const opts = {
|
|
2274
|
-
hostname: 'localhost',
|
|
2275
|
-
port: UPSTREAM,
|
|
2276
|
-
path: clientReq.url,
|
|
2277
|
-
method: clientReq.method,
|
|
2278
|
-
headers: clientReq.headers,
|
|
2279
|
-
};
|
|
2280
|
-
const proxyReq = http.request(opts, (proxyRes) => {
|
|
2281
|
-
const ct = proxyRes.headers['content-type'] || '';
|
|
2282
|
-
if (ct.includes('text/html')) {
|
|
2283
|
-
// Buffer HTML to inject overlay
|
|
2284
|
-
const chunks = [];
|
|
2285
|
-
proxyRes.on('data', c => chunks.push(c));
|
|
2286
|
-
proxyRes.on('end', () => {
|
|
2287
|
-
let html = Buffer.concat(chunks);
|
|
2288
|
-
const enc = proxyRes.headers['content-encoding'];
|
|
2289
|
-
// Decompress if needed
|
|
2290
|
-
if (enc === 'gzip') {
|
|
2291
|
-
try { html = zlib.gunzipSync(html); } catch {}
|
|
2292
|
-
} else if (enc === 'br') {
|
|
2293
|
-
try { html = zlib.brotliDecompressSync(html); } catch {}
|
|
2294
|
-
} else if (enc === 'deflate') {
|
|
2295
|
-
try { html = zlib.inflateSync(html); } catch {}
|
|
2538
|
+
break;
|
|
2539
|
+
}
|
|
2540
|
+
case "interact":
|
|
2541
|
+
await stashService.switchPreview(event.stashId, event.sortedStashIds);
|
|
2542
|
+
break;
|
|
2543
|
+
case "preview_heartbeat":
|
|
2544
|
+
stashService.previewHeartbeat(event.stashId);
|
|
2545
|
+
break;
|
|
2546
|
+
case "apply_stash":
|
|
2547
|
+
await stashService.applyStash(event.stashId);
|
|
2548
|
+
break;
|
|
2549
|
+
case "delete_stash":
|
|
2550
|
+
await stashService.deleteStash(event.stashId);
|
|
2551
|
+
break;
|
|
2296
2552
|
}
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
proxyRes.pipe(clientRes);
|
|
2309
|
-
}
|
|
2310
|
-
proxyRes.on('error', () => clientRes.end());
|
|
2311
|
-
});
|
|
2312
|
-
proxyReq.on('error', () => { try { clientRes.writeHead(502); clientRes.end(); } catch {} });
|
|
2313
|
-
clientReq.pipe(proxyReq);
|
|
2314
|
-
});
|
|
2315
|
-
|
|
2316
|
-
// WebSocket upgrades: raw TCP pipe
|
|
2317
|
-
server.on('upgrade', (req, socket, head) => {
|
|
2318
|
-
const upstream = net.createConnection(UPSTREAM, 'localhost', () => {
|
|
2319
|
-
const lines = [req.method + ' ' + req.url + ' HTTP/1.1'];
|
|
2320
|
-
for (const [k, v] of Object.entries(req.headers)) {
|
|
2321
|
-
lines.push(k + ': ' + (Array.isArray(v) ? v.join(', ') : v));
|
|
2322
|
-
}
|
|
2323
|
-
upstream.write(lines.join('\\r\\n') + '\\r\\n\\r\\n');
|
|
2324
|
-
if (head.length) upstream.write(head);
|
|
2325
|
-
socket.pipe(upstream);
|
|
2326
|
-
upstream.pipe(socket);
|
|
2327
|
-
});
|
|
2328
|
-
upstream.on('error', () => socket.destroy());
|
|
2329
|
-
socket.on('error', () => upstream.destroy());
|
|
2330
|
-
});
|
|
2331
|
-
|
|
2332
|
-
server.listen(${proxyPort}, () => {
|
|
2333
|
-
if (process.send) process.send('ready');
|
|
2334
|
-
});
|
|
2335
|
-
`;
|
|
2336
|
-
const child = spawn5("node", ["-e", proxyScript], {
|
|
2337
|
-
stdio: ["ignore", "inherit", "inherit", "ipc"]
|
|
2338
|
-
});
|
|
2339
|
-
child.on("error", (err) => {
|
|
2340
|
-
logger.error("proxy", `Failed to start proxy: ${err.message}`);
|
|
2341
|
-
});
|
|
2342
|
-
child.on("message", (msg) => {
|
|
2343
|
-
if (msg === "ready") {
|
|
2344
|
-
logger.info("proxy", `App proxy at http://localhost:${proxyPort} \u2192 http://localhost:${userDevPort}`);
|
|
2553
|
+
} catch (err) {
|
|
2554
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2555
|
+
logger.error("ws", `handler failed for ${event.type}`, { error: errorMsg });
|
|
2556
|
+
if ("stashId" in event && event.stashId) {
|
|
2557
|
+
broadcast({ type: "stash:error", stashId: event.stashId, error: errorMsg });
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
},
|
|
2561
|
+
close(ws) {
|
|
2562
|
+
clients.delete(ws);
|
|
2563
|
+
logger.info("ws", "client disconnected", { remaining: clients.size });
|
|
2345
2564
|
}
|
|
2346
|
-
}
|
|
2347
|
-
return child;
|
|
2565
|
+
};
|
|
2348
2566
|
}
|
|
2349
2567
|
|
|
2350
2568
|
// ../server/dist/index.js
|
|
@@ -2422,187 +2640,6 @@ function startServer(projectPath, userDevPort, port = STASHES_PORT) {
|
|
|
2422
2640
|
logger.info("server", `Project: ${projectPath}`);
|
|
2423
2641
|
return server;
|
|
2424
2642
|
}
|
|
2425
|
-
function injectOverlayScript(html, _upstreamPort, _proxyPort) {
|
|
2426
|
-
const overlayScript = `
|
|
2427
|
-
<script data-stashes-overlay>
|
|
2428
|
-
(function() {
|
|
2429
|
-
var highlightOverlay = null;
|
|
2430
|
-
var pickerEnabled = false;
|
|
2431
|
-
var precisionMode = false;
|
|
2432
|
-
|
|
2433
|
-
function createOverlay() {
|
|
2434
|
-
var overlay = document.createElement('div');
|
|
2435
|
-
overlay.id = 'stashes-highlight';
|
|
2436
|
-
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;';
|
|
2437
|
-
var tooltip = document.createElement('div');
|
|
2438
|
-
tooltip.id = 'stashes-tooltip';
|
|
2439
|
-
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;';
|
|
2440
|
-
document.body.appendChild(overlay);
|
|
2441
|
-
document.body.appendChild(tooltip);
|
|
2442
|
-
return overlay;
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
|
-
var SEMANTIC_TAGS = ['header','nav','main','section','article','aside','footer','form','dialog'];
|
|
2446
|
-
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'];
|
|
2447
|
-
|
|
2448
|
-
function findTarget(el, precise) {
|
|
2449
|
-
if (precise) return el;
|
|
2450
|
-
// If the element itself is a meaningful leaf, select it directly
|
|
2451
|
-
var elTag = el.tagName ? el.tagName.toLowerCase() : '';
|
|
2452
|
-
if (LEAF_TAGS.indexOf(elTag) !== -1) return el;
|
|
2453
|
-
var current = el;
|
|
2454
|
-
var best = el;
|
|
2455
|
-
while (current && current !== document.body) {
|
|
2456
|
-
var tag = current.tagName.toLowerCase();
|
|
2457
|
-
if (LEAF_TAGS.indexOf(tag) !== -1) { best = current; break; }
|
|
2458
|
-
if (SEMANTIC_TAGS.indexOf(tag) !== -1) { best = current; break; }
|
|
2459
|
-
if (current.id) { best = current; break; }
|
|
2460
|
-
if (current.getAttribute('role')) { best = current; break; }
|
|
2461
|
-
if (current.getAttribute('data-testid')) { best = current; break; }
|
|
2462
|
-
if (current.children && current.children.length > 1 && current.getBoundingClientRect().height > 50) {
|
|
2463
|
-
best = current;
|
|
2464
|
-
break;
|
|
2465
|
-
}
|
|
2466
|
-
current = current.parentElement;
|
|
2467
|
-
}
|
|
2468
|
-
return best;
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
function describeElement(el) {
|
|
2472
|
-
var tag = el.tagName.toLowerCase();
|
|
2473
|
-
var id = el.id ? '#' + el.id : '';
|
|
2474
|
-
var role = el.getAttribute('role') || '';
|
|
2475
|
-
var testId = el.getAttribute('data-testid') || '';
|
|
2476
|
-
var cls = (el.className && typeof el.className === 'string') ? el.className.trim().split(/[ ]+/).slice(0, 2).join('.') : '';
|
|
2477
|
-
var text = (el.textContent || '').trim().substring(0, 40);
|
|
2478
|
-
var label = tag;
|
|
2479
|
-
if (SEMANTIC_TAGS.indexOf(tag) !== -1) label = tag.toUpperCase();
|
|
2480
|
-
if (id) label += ' ' + id;
|
|
2481
|
-
else if (cls) label += '.' + cls;
|
|
2482
|
-
if (role) label += ' [' + role + ']';
|
|
2483
|
-
if (testId) label += ' [' + testId + ']';
|
|
2484
|
-
if (!id && !cls && text) label += ' "' + text.substring(0, 25) + '"';
|
|
2485
|
-
return label;
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
function generateSelector(el) {
|
|
2489
|
-
if (el.id) return '#' + el.id;
|
|
2490
|
-
var parts = [];
|
|
2491
|
-
var current = el;
|
|
2492
|
-
var depth = 0;
|
|
2493
|
-
while (current && current !== document.body && depth < 5) {
|
|
2494
|
-
var sel = current.tagName.toLowerCase();
|
|
2495
|
-
if (current.id) { parts.unshift('#' + current.id); break; }
|
|
2496
|
-
if (current.className && typeof current.className === 'string') {
|
|
2497
|
-
var c = current.className.trim().split(/[ ]+/).slice(0, 2).join('.');
|
|
2498
|
-
if (c) sel += '.' + c;
|
|
2499
|
-
}
|
|
2500
|
-
parts.unshift(sel);
|
|
2501
|
-
current = current.parentElement;
|
|
2502
|
-
depth++;
|
|
2503
|
-
}
|
|
2504
|
-
return parts.join(' > ');
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
|
-
function onMouseMove(e) {
|
|
2508
|
-
if (!pickerEnabled) return;
|
|
2509
|
-
if (!highlightOverlay) highlightOverlay = createOverlay();
|
|
2510
|
-
precisionMode = e.shiftKey;
|
|
2511
|
-
var target = findTarget(e.target, precisionMode);
|
|
2512
|
-
var overlay = document.getElementById('stashes-highlight');
|
|
2513
|
-
var tooltip = document.getElementById('stashes-tooltip');
|
|
2514
|
-
if (target) {
|
|
2515
|
-
var rect = target.getBoundingClientRect();
|
|
2516
|
-
overlay.style.display = 'block';
|
|
2517
|
-
overlay.style.top = rect.top + 'px';
|
|
2518
|
-
overlay.style.left = rect.left + 'px';
|
|
2519
|
-
overlay.style.width = rect.width + 'px';
|
|
2520
|
-
overlay.style.height = rect.height + 'px';
|
|
2521
|
-
overlay.style.borderColor = precisionMode ? '#f59e0b' : '#6366f1';
|
|
2522
|
-
tooltip.style.display = 'block';
|
|
2523
|
-
tooltip.style.top = Math.max(0, rect.top - 30) + 'px';
|
|
2524
|
-
tooltip.style.left = Math.max(0, rect.left) + 'px';
|
|
2525
|
-
tooltip.textContent = (precisionMode ? '[precise] ' : '') + describeElement(target);
|
|
2526
|
-
} else {
|
|
2527
|
-
overlay.style.display = 'none';
|
|
2528
|
-
tooltip.style.display = 'none';
|
|
2529
|
-
}
|
|
2530
|
-
}
|
|
2531
|
-
|
|
2532
|
-
function onClick(e) {
|
|
2533
|
-
if (!pickerEnabled) return;
|
|
2534
|
-
e.preventDefault();
|
|
2535
|
-
e.stopPropagation();
|
|
2536
|
-
var target = findTarget(e.target, e.shiftKey);
|
|
2537
|
-
if (target) {
|
|
2538
|
-
var desc = describeElement(target);
|
|
2539
|
-
var selector = generateSelector(target);
|
|
2540
|
-
var tag = target.tagName.toLowerCase();
|
|
2541
|
-
var outerSnippet = target.outerHTML.substring(0, 500);
|
|
2542
|
-
window.parent.postMessage({
|
|
2543
|
-
type: 'stashes:component_selected',
|
|
2544
|
-
component: {
|
|
2545
|
-
name: desc,
|
|
2546
|
-
filePath: 'auto-detect',
|
|
2547
|
-
domSelector: selector,
|
|
2548
|
-
htmlSnippet: outerSnippet,
|
|
2549
|
-
tag: tag
|
|
2550
|
-
}
|
|
2551
|
-
}, '*');
|
|
2552
|
-
var overlay = document.getElementById('stashes-highlight');
|
|
2553
|
-
if (overlay) {
|
|
2554
|
-
overlay.style.borderColor = '#22c55e';
|
|
2555
|
-
overlay.style.background = 'rgba(34,197,94,0.1)';
|
|
2556
|
-
setTimeout(function() {
|
|
2557
|
-
overlay.style.borderColor = '#6366f1';
|
|
2558
|
-
overlay.style.background = 'rgba(99,102,241,0.1)';
|
|
2559
|
-
}, 500);
|
|
2560
|
-
}
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2564
|
-
window.addEventListener('message', function(e) {
|
|
2565
|
-
if (e.data && e.data.type === 'stashes:toggle_picker') {
|
|
2566
|
-
pickerEnabled = e.data.enabled;
|
|
2567
|
-
if (!pickerEnabled) {
|
|
2568
|
-
var ov = document.getElementById('stashes-highlight');
|
|
2569
|
-
var tp = document.getElementById('stashes-tooltip');
|
|
2570
|
-
if (ov) ov.style.display = 'none';
|
|
2571
|
-
if (tp) tp.style.display = 'none';
|
|
2572
|
-
}
|
|
2573
|
-
}
|
|
2574
|
-
});
|
|
2575
|
-
|
|
2576
|
-
// Report current URL to parent for status bar display
|
|
2577
|
-
function reportUrl() {
|
|
2578
|
-
window.parent.postMessage({
|
|
2579
|
-
type: 'stashes:url_change',
|
|
2580
|
-
url: window.location.pathname + window.location.search + window.location.hash
|
|
2581
|
-
}, '*');
|
|
2582
|
-
}
|
|
2583
|
-
reportUrl();
|
|
2584
|
-
var origPush = history.pushState;
|
|
2585
|
-
history.pushState = function() {
|
|
2586
|
-
origPush.apply(this, arguments);
|
|
2587
|
-
setTimeout(reportUrl, 0);
|
|
2588
|
-
};
|
|
2589
|
-
var origReplace = history.replaceState;
|
|
2590
|
-
history.replaceState = function() {
|
|
2591
|
-
origReplace.apply(this, arguments);
|
|
2592
|
-
setTimeout(reportUrl, 0);
|
|
2593
|
-
};
|
|
2594
|
-
window.addEventListener('popstate', reportUrl);
|
|
2595
|
-
|
|
2596
|
-
document.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
2597
|
-
document.addEventListener('click', onClick, true);
|
|
2598
|
-
})();
|
|
2599
|
-
</script>`;
|
|
2600
|
-
if (html.includes("</body>")) {
|
|
2601
|
-
return html.replace("</body>", () => overlayScript + `
|
|
2602
|
-
</body>`);
|
|
2603
|
-
}
|
|
2604
|
-
return html + overlayScript;
|
|
2605
|
-
}
|
|
2606
2643
|
|
|
2607
2644
|
// ../mcp/src/tools/browse.ts
|
|
2608
2645
|
import open from "open";
|