stashes 0.1.38 → 0.1.39

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/mcp.js CHANGED
@@ -1558,7 +1558,291 @@ var apiRoutes = app;
1558
1558
  import { readFileSync as readFileSync4, existsSync as existsSync8 } from "fs";
1559
1559
  import { join as join8 } from "path";
1560
1560
 
1561
+ // ../server/dist/services/app-proxy.js
1562
+ import { spawn as spawn5 } from "child_process";
1563
+ function startAppProxy(userDevPort, proxyPort, injectOverlay) {
1564
+ const overlayScript = injectOverlay("", userDevPort, proxyPort);
1565
+ const overlayEscaped = JSON.stringify(overlayScript);
1566
+ const proxyScript = `
1567
+ const http = require('http');
1568
+ const net = require('net');
1569
+ const zlib = require('zlib');
1570
+ const UPSTREAM = ${userDevPort};
1571
+ const OVERLAY = ${overlayEscaped};
1572
+
1573
+ const server = http.createServer((clientReq, clientRes) => {
1574
+ const opts = {
1575
+ hostname: 'localhost',
1576
+ port: UPSTREAM,
1577
+ path: clientReq.url,
1578
+ method: clientReq.method,
1579
+ headers: clientReq.headers,
1580
+ };
1581
+ const proxyReq = http.request(opts, (proxyRes) => {
1582
+ const ct = proxyRes.headers['content-type'] || '';
1583
+ if (ct.includes('text/html')) {
1584
+ // Buffer HTML to inject overlay
1585
+ const chunks = [];
1586
+ proxyRes.on('data', c => chunks.push(c));
1587
+ proxyRes.on('end', () => {
1588
+ let html = Buffer.concat(chunks);
1589
+ const enc = proxyRes.headers['content-encoding'];
1590
+ // Decompress if needed
1591
+ if (enc === 'gzip') {
1592
+ try { html = zlib.gunzipSync(html); } catch {}
1593
+ } else if (enc === 'br') {
1594
+ try { html = zlib.brotliDecompressSync(html); } catch {}
1595
+ } else if (enc === 'deflate') {
1596
+ try { html = zlib.inflateSync(html); } catch {}
1597
+ }
1598
+ const hdrs = { ...proxyRes.headers };
1599
+ delete hdrs['content-length'];
1600
+ delete hdrs['content-encoding'];
1601
+ delete hdrs['transfer-encoding'];
1602
+ clientRes.writeHead(proxyRes.statusCode, hdrs);
1603
+ clientRes.write(html);
1604
+ clientRes.end(OVERLAY);
1605
+ });
1606
+ } else {
1607
+ // Non-HTML: stream through unchanged
1608
+ clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
1609
+ proxyRes.pipe(clientRes);
1610
+ }
1611
+ proxyRes.on('error', () => clientRes.end());
1612
+ });
1613
+ proxyReq.on('error', () => { try { clientRes.writeHead(502); clientRes.end(); } catch {} });
1614
+ clientReq.pipe(proxyReq);
1615
+ });
1616
+
1617
+ // WebSocket upgrades: raw TCP pipe
1618
+ server.on('upgrade', (req, socket, head) => {
1619
+ const upstream = net.createConnection(UPSTREAM, 'localhost', () => {
1620
+ const lines = [req.method + ' ' + req.url + ' HTTP/1.1'];
1621
+ for (const [k, v] of Object.entries(req.headers)) {
1622
+ lines.push(k + ': ' + (Array.isArray(v) ? v.join(', ') : v));
1623
+ }
1624
+ upstream.write(lines.join('\\r\\n') + '\\r\\n\\r\\n');
1625
+ if (head.length) upstream.write(head);
1626
+ socket.pipe(upstream);
1627
+ upstream.pipe(socket);
1628
+ });
1629
+ upstream.on('error', () => socket.destroy());
1630
+ socket.on('error', () => upstream.destroy());
1631
+ });
1632
+
1633
+ server.listen(${proxyPort}, () => {
1634
+ if (process.send) process.send('ready');
1635
+ });
1636
+ `;
1637
+ const child = spawn5("node", ["-e", proxyScript], {
1638
+ stdio: ["ignore", "inherit", "inherit", "ipc"]
1639
+ });
1640
+ child.on("error", (err) => {
1641
+ logger.error("proxy", `Failed to start proxy: ${err.message}`);
1642
+ });
1643
+ child.on("message", (msg) => {
1644
+ if (msg === "ready") {
1645
+ logger.info("proxy", `App proxy at http://localhost:${proxyPort} \u2192 http://localhost:${userDevPort}`);
1646
+ }
1647
+ });
1648
+ return child;
1649
+ }
1650
+
1651
+ // ../server/dist/services/overlay-script.js
1652
+ function injectOverlayScript(html, _upstreamPort, _proxyPort) {
1653
+ const overlayScript = `
1654
+ <script data-stashes-overlay>
1655
+ (function() {
1656
+ var highlightOverlay = null;
1657
+ var pickerEnabled = false;
1658
+ var precisionMode = false;
1659
+
1660
+ function createOverlay() {
1661
+ var overlay = document.createElement('div');
1662
+ overlay.id = 'stashes-highlight';
1663
+ 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;';
1664
+ var tooltip = document.createElement('div');
1665
+ tooltip.id = 'stashes-tooltip';
1666
+ 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;';
1667
+ document.body.appendChild(overlay);
1668
+ document.body.appendChild(tooltip);
1669
+ return overlay;
1670
+ }
1671
+
1672
+ var SEMANTIC_TAGS = ['header','nav','main','section','article','aside','footer','form','dialog'];
1673
+ 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'];
1674
+
1675
+ function findTarget(el, precise) {
1676
+ if (precise) return el;
1677
+ // If the element itself is a meaningful leaf, select it directly
1678
+ var elTag = el.tagName ? el.tagName.toLowerCase() : '';
1679
+ if (LEAF_TAGS.indexOf(elTag) !== -1) return el;
1680
+ var current = el;
1681
+ var best = el;
1682
+ while (current && current !== document.body) {
1683
+ var tag = current.tagName.toLowerCase();
1684
+ if (LEAF_TAGS.indexOf(tag) !== -1) { best = current; break; }
1685
+ if (SEMANTIC_TAGS.indexOf(tag) !== -1) { best = current; break; }
1686
+ if (current.id) { best = current; break; }
1687
+ if (current.getAttribute('role')) { best = current; break; }
1688
+ if (current.getAttribute('data-testid')) { best = current; break; }
1689
+ if (current.children && current.children.length > 1 && current.getBoundingClientRect().height > 50) {
1690
+ best = current;
1691
+ break;
1692
+ }
1693
+ current = current.parentElement;
1694
+ }
1695
+ return best;
1696
+ }
1697
+
1698
+ function describeElement(el) {
1699
+ var tag = el.tagName.toLowerCase();
1700
+ var id = el.id ? '#' + el.id : '';
1701
+ var role = el.getAttribute('role') || '';
1702
+ var testId = el.getAttribute('data-testid') || '';
1703
+ var cls = (el.className && typeof el.className === 'string') ? el.className.trim().split(/[ ]+/).slice(0, 2).join('.') : '';
1704
+ var text = (el.textContent || '').trim().substring(0, 40);
1705
+ var label = tag;
1706
+ if (SEMANTIC_TAGS.indexOf(tag) !== -1) label = tag.toUpperCase();
1707
+ if (id) label += ' ' + id;
1708
+ else if (cls) label += '.' + cls;
1709
+ if (role) label += ' [' + role + ']';
1710
+ if (testId) label += ' [' + testId + ']';
1711
+ if (!id && !cls && text) label += ' "' + text.substring(0, 25) + '"';
1712
+ return label;
1713
+ }
1714
+
1715
+ function generateSelector(el) {
1716
+ if (el.id) return '#' + el.id;
1717
+ var parts = [];
1718
+ var current = el;
1719
+ var depth = 0;
1720
+ while (current && current !== document.body && depth < 5) {
1721
+ var sel = current.tagName.toLowerCase();
1722
+ if (current.id) { parts.unshift('#' + current.id); break; }
1723
+ if (current.className && typeof current.className === 'string') {
1724
+ var c = current.className.trim().split(/[ ]+/).slice(0, 2).join('.');
1725
+ if (c) sel += '.' + c;
1726
+ }
1727
+ parts.unshift(sel);
1728
+ current = current.parentElement;
1729
+ depth++;
1730
+ }
1731
+ return parts.join(' > ');
1732
+ }
1733
+
1734
+ function onMouseMove(e) {
1735
+ if (!pickerEnabled) return;
1736
+ if (!highlightOverlay) highlightOverlay = createOverlay();
1737
+ precisionMode = e.shiftKey;
1738
+ var target = findTarget(e.target, precisionMode);
1739
+ var overlay = document.getElementById('stashes-highlight');
1740
+ var tooltip = document.getElementById('stashes-tooltip');
1741
+ if (target) {
1742
+ var rect = target.getBoundingClientRect();
1743
+ overlay.style.display = 'block';
1744
+ overlay.style.top = rect.top + 'px';
1745
+ overlay.style.left = rect.left + 'px';
1746
+ overlay.style.width = rect.width + 'px';
1747
+ overlay.style.height = rect.height + 'px';
1748
+ overlay.style.borderColor = precisionMode ? '#f59e0b' : '#6366f1';
1749
+ tooltip.style.display = 'block';
1750
+ tooltip.style.top = Math.max(0, rect.top - 30) + 'px';
1751
+ tooltip.style.left = Math.max(0, rect.left) + 'px';
1752
+ tooltip.textContent = (precisionMode ? '[precise] ' : '') + describeElement(target);
1753
+ } else {
1754
+ overlay.style.display = 'none';
1755
+ tooltip.style.display = 'none';
1756
+ }
1757
+ }
1758
+
1759
+ function onClick(e) {
1760
+ if (!pickerEnabled) return;
1761
+ e.preventDefault();
1762
+ e.stopPropagation();
1763
+ var target = findTarget(e.target, e.shiftKey);
1764
+ if (target) {
1765
+ var desc = describeElement(target);
1766
+ var selector = generateSelector(target);
1767
+ var tag = target.tagName.toLowerCase();
1768
+ var outerSnippet = target.outerHTML.substring(0, 500);
1769
+ window.parent.postMessage({
1770
+ type: 'stashes:component_selected',
1771
+ component: {
1772
+ name: desc,
1773
+ filePath: 'auto-detect',
1774
+ domSelector: selector,
1775
+ htmlSnippet: outerSnippet,
1776
+ tag: tag
1777
+ }
1778
+ }, '*');
1779
+ var overlay = document.getElementById('stashes-highlight');
1780
+ if (overlay) {
1781
+ overlay.style.borderColor = '#22c55e';
1782
+ overlay.style.background = 'rgba(34,197,94,0.1)';
1783
+ setTimeout(function() {
1784
+ overlay.style.borderColor = '#6366f1';
1785
+ overlay.style.background = 'rgba(99,102,241,0.1)';
1786
+ }, 500);
1787
+ }
1788
+ }
1789
+ }
1790
+
1791
+ window.addEventListener('message', function(e) {
1792
+ if (!e.data || !e.data.type) return;
1793
+ if (e.data.type === 'stashes:toggle_picker') {
1794
+ pickerEnabled = e.data.enabled;
1795
+ if (!pickerEnabled) {
1796
+ var ov = document.getElementById('stashes-highlight');
1797
+ var tp = document.getElementById('stashes-tooltip');
1798
+ if (ov) ov.style.display = 'none';
1799
+ if (tp) tp.style.display = 'none';
1800
+ }
1801
+ } else if (e.data.type === 'stashes:navigate_back') {
1802
+ history.back();
1803
+ } else if (e.data.type === 'stashes:navigate_forward') {
1804
+ history.forward();
1805
+ } else if (e.data.type === 'stashes:refresh') {
1806
+ location.reload();
1807
+ } else if (e.data.type === 'stashes:navigate_to') {
1808
+ window.location.href = e.data.url;
1809
+ }
1810
+ });
1811
+
1812
+ // Report current URL to parent for status bar display
1813
+ function reportUrl() {
1814
+ window.parent.postMessage({
1815
+ type: 'stashes:url_change',
1816
+ url: window.location.pathname + window.location.search + window.location.hash
1817
+ }, '*');
1818
+ }
1819
+ reportUrl();
1820
+ var origPush = history.pushState;
1821
+ history.pushState = function() {
1822
+ origPush.apply(this, arguments);
1823
+ setTimeout(reportUrl, 0);
1824
+ };
1825
+ var origReplace = history.replaceState;
1826
+ history.replaceState = function() {
1827
+ origReplace.apply(this, arguments);
1828
+ setTimeout(reportUrl, 0);
1829
+ };
1830
+ window.addEventListener('popstate', reportUrl);
1831
+
1832
+ document.addEventListener('mousemove', onMouseMove, { passive: true });
1833
+ document.addEventListener('click', onClick, true);
1834
+ })();
1835
+ </script>`;
1836
+ if (html.includes("</body>")) {
1837
+ return html.replace("</body>", () => overlayScript + `
1838
+ </body>`);
1839
+ }
1840
+ return html + overlayScript;
1841
+ }
1842
+
1561
1843
  // ../server/dist/services/preview-pool.js
1844
+ var DEV_PORT_OFFSET = 1000;
1845
+
1562
1846
  class PreviewPool {
1563
1847
  entries = new Map;
1564
1848
  usedPorts = new Set;
@@ -1584,7 +1868,8 @@ class PreviewPool {
1584
1868
  if (this.entries.size >= this.maxSize) {
1585
1869
  this.evictOldest();
1586
1870
  }
1587
- const port = this.allocatePort();
1871
+ const proxyPort = this.allocatePort();
1872
+ const devPort = proxyPort + DEV_PORT_OFFSET;
1588
1873
  const worktreePath = await this.worktreeManager.createPreviewForPool(stashId);
1589
1874
  const process2 = Bun.spawn({
1590
1875
  cmd: ["npm", "run", "dev"],
@@ -1592,20 +1877,22 @@ class PreviewPool {
1592
1877
  stdin: "ignore",
1593
1878
  stdout: "pipe",
1594
1879
  stderr: "pipe",
1595
- env: { ...Bun.env, PORT: String(port), BROWSER: "none" }
1880
+ env: { ...Bun.env, PORT: String(devPort), BROWSER: "none" }
1596
1881
  });
1882
+ const proxyProcess = startAppProxy(devPort, proxyPort, injectOverlayScript);
1597
1883
  const entry = {
1598
1884
  stashId,
1599
- port,
1885
+ port: proxyPort,
1600
1886
  process: process2,
1887
+ proxyProcess,
1601
1888
  worktreePath,
1602
1889
  lastHeartbeat: Date.now()
1603
1890
  };
1604
1891
  this.entries.set(stashId, entry);
1605
- this.usedPorts.add(port);
1606
- logger.info("pool", `cold start: ${stashId} on port ${port}`, { poolSize: this.entries.size });
1607
- await this.waitForPort(port, 60000);
1608
- return port;
1892
+ this.usedPorts.add(proxyPort);
1893
+ logger.info("pool", `cold start: ${stashId} dev=:${devPort} proxy=:${proxyPort}`, { poolSize: this.entries.size });
1894
+ await this.waitForPort(proxyPort, 60000);
1895
+ return proxyPort;
1609
1896
  }
1610
1897
  heartbeat(stashId) {
1611
1898
  const entry = this.entries.get(stashId);
@@ -1723,6 +2010,9 @@ class PreviewPool {
1723
2010
  try {
1724
2011
  entry.process.kill();
1725
2012
  } catch {}
2013
+ try {
2014
+ entry.proxyProcess.kill();
2015
+ } catch {}
1726
2016
  }
1727
2017
  async waitForPort(port, timeout) {
1728
2018
  const start = Date.now();
@@ -2227,124 +2517,34 @@ function createWebSocketHandler(projectPath, userDevPort, appProxyPort) {
2227
2517
  }
2228
2518
  }
2229
2519
  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 {}
2520
+ break;
2521
+ }
2522
+ case "interact":
2523
+ await stashService.switchPreview(event.stashId, event.sortedStashIds);
2524
+ break;
2525
+ case "preview_heartbeat":
2526
+ stashService.previewHeartbeat(event.stashId);
2527
+ break;
2528
+ case "apply_stash":
2529
+ await stashService.applyStash(event.stashId);
2530
+ break;
2531
+ case "delete_stash":
2532
+ await stashService.deleteStash(event.stashId);
2533
+ break;
2296
2534
  }
2297
- const hdrs = { ...proxyRes.headers };
2298
- delete hdrs['content-length'];
2299
- delete hdrs['content-encoding'];
2300
- delete hdrs['transfer-encoding'];
2301
- clientRes.writeHead(proxyRes.statusCode, hdrs);
2302
- clientRes.write(html);
2303
- clientRes.end(OVERLAY);
2304
- });
2305
- } else {
2306
- // Non-HTML: stream through unchanged
2307
- clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
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}`);
2535
+ } catch (err) {
2536
+ const errorMsg = err instanceof Error ? err.message : String(err);
2537
+ logger.error("ws", `handler failed for ${event.type}`, { error: errorMsg });
2538
+ if ("stashId" in event && event.stashId) {
2539
+ broadcast({ type: "stash:error", stashId: event.stashId, error: errorMsg });
2540
+ }
2541
+ }
2542
+ },
2543
+ close(ws) {
2544
+ clients.delete(ws);
2545
+ logger.info("ws", "client disconnected", { remaining: clients.size });
2345
2546
  }
2346
- });
2347
- return child;
2547
+ };
2348
2548
  }
2349
2549
 
2350
2550
  // ../server/dist/index.js
@@ -2422,187 +2622,6 @@ function startServer(projectPath, userDevPort, port = STASHES_PORT) {
2422
2622
  logger.info("server", `Project: ${projectPath}`);
2423
2623
  return server;
2424
2624
  }
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
2625
 
2607
2626
  // ../mcp/src/tools/browse.ts
2608
2627
  import open from "open";