@pablopunk/webr 0.3.10 → 0.3.12

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.
@@ -345,6 +345,10 @@ async function setProjectIcon(project, png) {
345
345
  body: png
346
346
  })).ok) throw new Error("Could not save this icon.");
347
347
  }
348
+ async function fetchIconCandidates(project) {
349
+ const response = await fetch(`/api/projects/${encodeURIComponent(project.machineId ?? "local")}/${encodeURIComponent(project.id)}/icon-candidates`);
350
+ return response.ok ? await response.json() : [];
351
+ }
348
352
  async function resetProjectIcon(project) {
349
353
  if (!(await fetch(projectIconUrl(project), { method: "DELETE" })).ok) throw new Error("Could not reset this icon.");
350
354
  }
@@ -1524,8 +1528,7 @@ function NewThreadView({ projects, machines, selectedProjectId, onOpenSidebar })
1524
1528
  };
1525
1529
  const machineProjects = projects.filter((project) => Object.hasOwn(machine.projectPaths, project.id)).map((project) => ({
1526
1530
  ...project,
1527
- path: machine.projectPaths[project.id],
1528
- iconUrl: machineId === "local" ? project.iconUrl : void 0
1531
+ path: machine.projectPaths[project.id]
1529
1532
  }));
1530
1533
  const canUseWorktree = catalog.data?.isGitRepo === true;
1531
1534
  const selectedHarness = machine.harnesses.find((choice) => choice.id === harness);
@@ -2457,6 +2460,22 @@ function ProjectIconDialog({ project, open, onClose, onError }) {
2457
2460
  const picker = useRef(null);
2458
2461
  const [dragging, setDragging] = useState(false);
2459
2462
  const [busy, setBusy] = useState(false);
2463
+ const [candidates, setCandidates] = useState([]);
2464
+ useEffect(() => {
2465
+ if (!open) return;
2466
+ let current = true;
2467
+ fetchIconCandidates(project).then((found) => {
2468
+ if (current) setCandidates(found);
2469
+ }).catch(() => {});
2470
+ return () => {
2471
+ current = false;
2472
+ setCandidates([]);
2473
+ };
2474
+ }, [
2475
+ open,
2476
+ project.id,
2477
+ project.machineId
2478
+ ]);
2460
2479
  const run = async (action) => {
2461
2480
  setBusy(true);
2462
2481
  try {
@@ -2471,6 +2490,7 @@ function ProjectIconDialog({ project, open, onClose, onError }) {
2471
2490
  const upload = (file) => {
2472
2491
  if (file) run(async () => setProjectIcon(project, await squarePng(file)));
2473
2492
  };
2493
+ const choose = (candidate) => void run(async () => setProjectIcon(project, await squarePng(await (await fetch(candidate.url)).blob())));
2474
2494
  return /* @__PURE__ */ jsx(Dialog.Root, {
2475
2495
  open,
2476
2496
  onOpenChange: (next) => {
@@ -2483,6 +2503,10 @@ function ProjectIconDialog({ project, open, onClose, onError }) {
2483
2503
  className: "dialog-message",
2484
2504
  children: ["Change icon for ", project.name]
2485
2505
  }),
2506
+ /* @__PURE__ */ jsxs("div", {
2507
+ className: "icon-current",
2508
+ children: [/* @__PURE__ */ jsx(ProjectIcon, { project }), /* @__PURE__ */ jsx("span", { children: "Current icon" })]
2509
+ }),
2486
2510
  /* @__PURE__ */ jsxs("button", {
2487
2511
  type: "button",
2488
2512
  className: `icon-dropzone ${dragging ? "is-dragging" : ""}`,
@@ -2513,6 +2537,24 @@ function ProjectIconDialog({ project, open, onClose, onError }) {
2513
2537
  event.target.value = "";
2514
2538
  }
2515
2539
  }),
2540
+ candidates.length > 0 && /* @__PURE__ */ jsx("div", {
2541
+ className: "icon-candidates",
2542
+ role: "group",
2543
+ "aria-label": "Images found in this project",
2544
+ children: candidates.map((candidate) => /* @__PURE__ */ jsx("button", {
2545
+ type: "button",
2546
+ className: "icon-candidate",
2547
+ disabled: busy,
2548
+ title: candidate.name,
2549
+ "aria-label": `Use ${candidate.name}`,
2550
+ onClick: () => choose(candidate),
2551
+ children: /* @__PURE__ */ jsx("img", {
2552
+ src: candidate.url,
2553
+ alt: "",
2554
+ loading: "lazy"
2555
+ })
2556
+ }, candidate.url))
2557
+ }),
2516
2558
  /* @__PURE__ */ jsxs("div", {
2517
2559
  className: "dialog-actions",
2518
2560
  children: [/* @__PURE__ */ jsx("button", {
@@ -1,6 +1,6 @@
1
1
  import { S as createAstro, V as __exportAll, i as renderComponent, u as renderTemplate } from "./server_dfdLOyV2.mjs";
2
2
  import { t as createComponent } from "./compiler_BnrQvXXD.mjs";
3
- import { n as $$Base, t as App } from "./App_BbzCRPiT.mjs";
3
+ import { n as $$Base, t as App } from "./App_CKe0Z9nu.mjs";
4
4
  //#region src/pages/threads/[id].astro
5
5
  var _id__exports = /* @__PURE__ */ __exportAll({
6
6
  default: () => $$Id,
@@ -1,6 +1,6 @@
1
1
  import { S as createAstro, V as __exportAll, i as renderComponent, u as renderTemplate } from "./server_dfdLOyV2.mjs";
2
2
  import { t as createComponent } from "./compiler_BnrQvXXD.mjs";
3
- import { n as $$Base, t as App } from "./App_BbzCRPiT.mjs";
3
+ import { n as $$Base, t as App } from "./App_CKe0Z9nu.mjs";
4
4
  //#region src/pages/sessions/[session]/tabs/[tab].astro
5
5
  var _tab__exports = /* @__PURE__ */ __exportAll({
6
6
  default: () => $$Tab,
@@ -1,6 +1,6 @@
1
1
  import { S as createAstro, V as __exportAll, i as renderComponent, u as renderTemplate } from "./server_dfdLOyV2.mjs";
2
2
  import { t as createComponent } from "./compiler_BnrQvXXD.mjs";
3
- import { n as $$Base, t as App } from "./App_BbzCRPiT.mjs";
3
+ import { n as $$Base, t as App } from "./App_CKe0Z9nu.mjs";
4
4
  //#region src/pages/new.astro
5
5
  var new_exports = /* @__PURE__ */ __exportAll({
6
6
  default: () => $$New,
@@ -1,6 +1,6 @@
1
1
  import { S as createAstro, V as __exportAll, i as renderComponent, u as renderTemplate } from "./server_dfdLOyV2.mjs";
2
2
  import { t as createComponent } from "./compiler_BnrQvXXD.mjs";
3
- import { n as $$Base, t as App } from "./App_BbzCRPiT.mjs";
3
+ import { n as $$Base, t as App } from "./App_CKe0Z9nu.mjs";
4
4
  //#region src/pages/settings.astro
5
5
  var settings_exports = /* @__PURE__ */ __exportAll({
6
6
  default: () => $$Settings,
@@ -2426,10 +2426,10 @@ var renderers = [Object.assign({
2426
2426
  //#endregion
2427
2427
  //#region \0virtual:astro:pages
2428
2428
  var _page0 = () => import("./chunks/node_C8FqXy_n.mjs").then((n) => n.t);
2429
- var _page1 = () => import("./chunks/new_n3R-7e9Z.mjs");
2430
- var _page2 = () => import("./chunks/_tab__O2UOM0DH.mjs");
2431
- var _page3 = () => import("./chunks/settings_CP_J5pGZ.mjs");
2432
- var _page4 = () => import("./chunks/_id___SMAFJxg.mjs");
2429
+ var _page1 = () => import("./chunks/new_Cpp90K8n.mjs");
2430
+ var _page2 = () => import("./chunks/_tab__BmEFH_DR.mjs");
2431
+ var _page3 = () => import("./chunks/settings_CwcEfyk0.mjs");
2432
+ var _page4 = () => import("./chunks/_id__Bk-s-r2G.mjs");
2433
2433
  var _page5 = () => import("./chunks/index_Bvv1gpI0.mjs");
2434
2434
  var pageMap = /* @__PURE__ */ new Map([
2435
2435
  ["node_modules/.pnpm/astro@7.3.5_@types+node@22.20.4_tsx@4.23.15_yaml@2.9.1/node_modules/astro/dist/assets/endpoint/node.js", _page0],
@@ -2441,7 +2441,7 @@ var pageMap = /* @__PURE__ */ new Map([
2441
2441
  ]);
2442
2442
  //#endregion
2443
2443
  //#region \0virtual:astro:manifest
2444
- var _manifest = deserializeManifest({"rootDir":"file:///home/runner/work/webr/webr/","cacheDir":"file:///home/runner/work/webr/webr/node_modules/.astro/","outDir":"file:///home/runner/work/webr/webr/dist/","srcDir":"file:///home/runner/work/webr/webr/src/","publicDir":"file:///home/runner/work/webr/webr/public/","buildClientDir":"file:///home/runner/work/webr/webr/dist/client/","buildServerDir":"file:///home/runner/work/webr/webr/dist/server/","adapterName":"@astrojs/node","assetsDir":"_astro","routes":[{"file":"","links":[],"scripts":[],"styles":[],"routeData":{"type":"page","component":"_server-islands.astro","params":["name"],"segments":[[{"content":"_server-islands","dynamic":false,"spread":false}],[{"content":"name","dynamic":true,"spread":false}]],"pattern":"^\\/_server-islands\\/([^/]+?)\\/?$","prerender":false,"isIndex":false,"fallbackRoutes":[],"route":"/_server-islands/[name]","origin":"internal","distURL":[],"_meta":{"trailingSlash":"ignore"}}},{"file":"","links":[],"scripts":[],"styles":[],"routeData":{"route":"/_image","component":"node_modules/.pnpm/astro@7.3.5_@types+node@22.20.4_tsx@4.23.15_yaml@2.9.1/node_modules/astro/dist/assets/endpoint/node.js","params":[],"pathname":"/_image","pattern":"^\\/_image\\/?$","segments":[[{"content":"_image","dynamic":false,"spread":false}]],"type":"endpoint","prerender":false,"fallbackRoutes":[],"distURL":[],"isIndex":false,"origin":"internal","_meta":{"trailingSlash":"ignore"}}},{"file":"","links":[],"scripts":[],"styles":[{"type":"external","src":"_astro/App.CJakfIju.css"}],"routeData":{"route":"/new","isIndex":false,"type":"page","pattern":"^\\/new\\/?$","segments":[[{"content":"new","dynamic":false,"spread":false}]],"params":[],"component":"src/pages/new.astro","pathname":"/new","prerender":false,"fallbackRoutes":[],"distURL":[],"origin":"project","_meta":{"trailingSlash":"ignore"}}},{"file":"","links":[],"scripts":[],"styles":[{"type":"external","src":"_astro/App.CJakfIju.css"}],"routeData":{"route":"/sessions/[session]/tabs/[tab]","isIndex":false,"type":"page","pattern":"^\\/sessions\\/([^/]+?)\\/tabs\\/([^/]+?)\\/?$","segments":[[{"content":"sessions","dynamic":false,"spread":false}],[{"content":"session","dynamic":true,"spread":false}],[{"content":"tabs","dynamic":false,"spread":false}],[{"content":"tab","dynamic":true,"spread":false}]],"params":["session","tab"],"component":"src/pages/sessions/[session]/tabs/[tab].astro","prerender":false,"fallbackRoutes":[],"distURL":[],"origin":"project","_meta":{"trailingSlash":"ignore"}}},{"file":"","links":[],"scripts":[],"styles":[{"type":"external","src":"_astro/App.CJakfIju.css"}],"routeData":{"route":"/settings","isIndex":false,"type":"page","pattern":"^\\/settings\\/?$","segments":[[{"content":"settings","dynamic":false,"spread":false}]],"params":[],"component":"src/pages/settings.astro","pathname":"/settings","prerender":false,"fallbackRoutes":[],"distURL":[],"origin":"project","_meta":{"trailingSlash":"ignore"}}},{"file":"","links":[],"scripts":[],"styles":[{"type":"external","src":"_astro/App.CJakfIju.css"}],"routeData":{"route":"/threads/[id]","isIndex":false,"type":"page","pattern":"^\\/threads\\/([^/]+?)\\/?$","segments":[[{"content":"threads","dynamic":false,"spread":false}],[{"content":"id","dynamic":true,"spread":false}]],"params":["id"],"component":"src/pages/threads/[id].astro","prerender":false,"fallbackRoutes":[],"distURL":[],"origin":"project","_meta":{"trailingSlash":"ignore"}}},{"file":"","links":[],"scripts":[],"styles":[],"routeData":{"route":"/","isIndex":true,"type":"page","pattern":"^\\/$","segments":[],"params":[],"component":"src/pages/index.astro","pathname":"/","prerender":false,"fallbackRoutes":[],"distURL":[],"origin":"project","_meta":{"trailingSlash":"ignore"}}}],"serverLike":true,"middlewareMode":"classic","base":"/","trailingSlash":"ignore","compressHTML":"jsx","componentMetadata":[["/home/runner/work/webr/webr/src/pages/new.astro",{"propagation":"none","containsHead":true}],["/home/runner/work/webr/webr/src/pages/sessions/[session]/tabs/[tab].astro",{"propagation":"none","containsHead":true}],["/home/runner/work/webr/webr/src/pages/settings.astro",{"propagation":"none","containsHead":true}],["/home/runner/work/webr/webr/src/pages/threads/[id].astro",{"propagation":"none","containsHead":true}]],"renderers":[],"clientDirectives":[["idle","(()=>{var l=(n,t)=>{let i=async()=>{await(await n())()},e=typeof t.value==\"object\"?t.value:void 0,s={timeout:e==null?void 0:e.timeout};\"requestIdleCallback\"in window?window.requestIdleCallback(i,s):setTimeout(i,s.timeout||200)};(self.Astro||(self.Astro={})).idle=l;window.dispatchEvent(new Event(\"astro:idle\"));})();"],["load","(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).load=e;window.dispatchEvent(new Event(\"astro:load\"));})();"],["media","(()=>{var n=(a,t)=>{let i=async()=>{await(await a())()};if(t.value){let e=matchMedia(t.value);e.matches?i():e.addEventListener(\"change\",i,{once:!0})}};(self.Astro||(self.Astro={})).media=n;window.dispatchEvent(new Event(\"astro:media\"));})();"],["only","(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event(\"astro:only\"));})();"],["visible","(()=>{var a=(s,i,o)=>{let r=async()=>{await(await s())()},t=typeof i.value==\"object\"?i.value:void 0,c={rootMargin:t==null?void 0:t.rootMargin},n=new IntersectionObserver(e=>{for(let l of e)if(l.isIntersecting){n.disconnect(),r();break}},c);for(let e of o.children)n.observe(e)};(self.Astro||(self.Astro={})).visible=a;window.dispatchEvent(new Event(\"astro:visible\"));})();"]],"entryModules":{"\u0000noop-middleware":"virtual_astro_middleware.mjs","\u0000virtual:astro:server-island-manifest":"chunks/_virtual_astro_server-island-manifest_C1Q2srgE.mjs","\u0000virtual:astro:session-driver":"chunks/_virtual_astro_session-driver_Dp7RXjOf.mjs","\u0000virtual:astro:actions/noop-entrypoint":"chunks/noop-entrypoint_Z3zFhrGC.mjs","/home/runner/work/webr/webr/node_modules/.pnpm/@astrojs+react@7.0.0_@types+node@22.20.4_@types+react-dom@19.3.0_@types+react@19.3.0__@_02aa85dceaccf3d763325676dd8540a8/node_modules/@astrojs/react/dist/vnode-children.js":"chunks/vnode-children_BSi0H4PL.mjs","@astrojs/node/server.js":"entry.mjs","\u0000virtual:astro:page:src/pages/threads/[id]@_@astro":"chunks/_id___SMAFJxg.mjs","\u0000virtual:astro:page:src/pages/sessions/[session]/tabs/[tab]@_@astro":"chunks/_tab__O2UOM0DH.mjs","\u0000virtual:astro:page:src/pages/index@_@astro":"chunks/index_Bvv1gpI0.mjs","\u0000virtual:astro:page:src/pages/new@_@astro":"chunks/new_n3R-7e9Z.mjs","\u0000virtual:astro:page:node_modules/.pnpm/astro@7.3.5_@types+node@22.20.4_tsx@4.23.15_yaml@2.9.1/node_modules/astro/dist/assets/endpoint/node@_@js":"chunks/node_C8FqXy_n.mjs","\u0000virtual:astro:page:src/pages/settings@_@astro":"chunks/settings_CP_J5pGZ.mjs","/home/runner/work/webr/webr/node_modules/.pnpm/astro@7.3.5_@types+node@22.20.4_tsx@4.23.15_yaml@2.9.1/node_modules/astro/dist/assets/services/sharp.js":"chunks/sharp_8J8zk4Wd.mjs","/home/runner/work/webr/webr/src/components/App.tsx":"_astro/App.bmHm9djw.js","/home/runner/work/webr/webr/node_modules/.pnpm/astro@7.3.5_@types+node@22.20.4_tsx@4.23.15_yaml@2.9.1/node_modules/astro/components/ClientRouter.astro?astro&type=script&index=0&lang.ts":"_astro/ClientRouter.astro_astro_type_script_index_0_lang.CTgtIFyY.js","@astrojs/react/client.js":"_astro/client.BDkzSRUN.js","/home/runner/work/webr/webr/node_modules/.pnpm/@xterm+addon-fit@0.10.0_@xterm+xterm@5.5.0/node_modules/@xterm/addon-fit/lib/addon-fit.js":"_astro/addon-fit.jgKqzwky.js","/home/runner/work/webr/webr/node_modules/.pnpm/@xterm+addon-web-links@0.11.0_@xterm+xterm@5.5.0/node_modules/@xterm/addon-web-links/lib/addon-web-links.js":"_astro/addon-web-links.C9lyQRxI.js","/home/runner/work/webr/webr/node_modules/.pnpm/@xterm+addon-webgl@0.18.0_@xterm+xterm@5.5.0/node_modules/@xterm/addon-webgl/lib/addon-webgl.js":"_astro/addon-webgl.Bd5ox92E.js","/home/runner/work/webr/webr/node_modules/.pnpm/@xterm+xterm@5.5.0/node_modules/@xterm/xterm/lib/xterm.js":"_astro/xterm.B-l5AIf6.js","astro:scripts/before-hydration.js":""},"inlinedScripts":[],"assets":["/apple-touch-icon.png","/brand-mark.png","/favicon-16.png","/favicon-32.png","/favicon.ico","/icon-192.png","/icon-512.png","/icon-maskable-512.png","/manifest.webmanifest","/_astro/App.bmHm9djw.js","/_astro/ClientRouter.astro_astro_type_script_index_0_lang.CTgtIFyY.js","/_astro/addon-fit.jgKqzwky.js","/_astro/addon-web-links.C9lyQRxI.js","/_astro/addon-webgl.Bd5ox92E.js","/_astro/client.BDkzSRUN.js","/_astro/client.Bbg9OBwT.js","/_astro/client.CLSsMr2g.js","/_astro/xterm.B-l5AIf6.js","/_astro/App.CJakfIju.css","/_astro/jetbrains-mono.CFJaiWaY.woff2","/_astro/symbols-nerd.CEdkKAK8.woff2"],"buildFormat":"directory","checkOrigin":true,"actionBodySizeLimit":1048576,"serverIslandBodySizeLimit":1048576,"allowedDomains":[],"key":"wMBFmhe+pn2DnaCSXSWVokwTbr5rGkrH+IRu6kzYs+A=","sessionConfig":{"driver":"unstorage/drivers/fs-lite","options":{"base":"/home/runner/work/webr/webr/node_modules/.astro/sessions"}},"image":{},"devToolbar":{"enabled":false,"debugInfoOutput":""},"logLevel":"info","shouldInjectCspMetaTags":false});
2444
+ var _manifest = deserializeManifest({"rootDir":"file:///home/runner/work/webr/webr/","cacheDir":"file:///home/runner/work/webr/webr/node_modules/.astro/","outDir":"file:///home/runner/work/webr/webr/dist/","srcDir":"file:///home/runner/work/webr/webr/src/","publicDir":"file:///home/runner/work/webr/webr/public/","buildClientDir":"file:///home/runner/work/webr/webr/dist/client/","buildServerDir":"file:///home/runner/work/webr/webr/dist/server/","adapterName":"@astrojs/node","assetsDir":"_astro","routes":[{"file":"","links":[],"scripts":[],"styles":[],"routeData":{"type":"page","component":"_server-islands.astro","params":["name"],"segments":[[{"content":"_server-islands","dynamic":false,"spread":false}],[{"content":"name","dynamic":true,"spread":false}]],"pattern":"^\\/_server-islands\\/([^/]+?)\\/?$","prerender":false,"isIndex":false,"fallbackRoutes":[],"route":"/_server-islands/[name]","origin":"internal","distURL":[],"_meta":{"trailingSlash":"ignore"}}},{"file":"","links":[],"scripts":[],"styles":[],"routeData":{"route":"/_image","component":"node_modules/.pnpm/astro@7.3.5_@types+node@22.20.4_tsx@4.23.15_yaml@2.9.1/node_modules/astro/dist/assets/endpoint/node.js","params":[],"pathname":"/_image","pattern":"^\\/_image\\/?$","segments":[[{"content":"_image","dynamic":false,"spread":false}]],"type":"endpoint","prerender":false,"fallbackRoutes":[],"distURL":[],"isIndex":false,"origin":"internal","_meta":{"trailingSlash":"ignore"}}},{"file":"","links":[],"scripts":[],"styles":[{"type":"external","src":"_astro/App.8h0zS6BR.css"}],"routeData":{"route":"/new","isIndex":false,"type":"page","pattern":"^\\/new\\/?$","segments":[[{"content":"new","dynamic":false,"spread":false}]],"params":[],"component":"src/pages/new.astro","pathname":"/new","prerender":false,"fallbackRoutes":[],"distURL":[],"origin":"project","_meta":{"trailingSlash":"ignore"}}},{"file":"","links":[],"scripts":[],"styles":[{"type":"external","src":"_astro/App.8h0zS6BR.css"}],"routeData":{"route":"/sessions/[session]/tabs/[tab]","isIndex":false,"type":"page","pattern":"^\\/sessions\\/([^/]+?)\\/tabs\\/([^/]+?)\\/?$","segments":[[{"content":"sessions","dynamic":false,"spread":false}],[{"content":"session","dynamic":true,"spread":false}],[{"content":"tabs","dynamic":false,"spread":false}],[{"content":"tab","dynamic":true,"spread":false}]],"params":["session","tab"],"component":"src/pages/sessions/[session]/tabs/[tab].astro","prerender":false,"fallbackRoutes":[],"distURL":[],"origin":"project","_meta":{"trailingSlash":"ignore"}}},{"file":"","links":[],"scripts":[],"styles":[{"type":"external","src":"_astro/App.8h0zS6BR.css"}],"routeData":{"route":"/settings","isIndex":false,"type":"page","pattern":"^\\/settings\\/?$","segments":[[{"content":"settings","dynamic":false,"spread":false}]],"params":[],"component":"src/pages/settings.astro","pathname":"/settings","prerender":false,"fallbackRoutes":[],"distURL":[],"origin":"project","_meta":{"trailingSlash":"ignore"}}},{"file":"","links":[],"scripts":[],"styles":[{"type":"external","src":"_astro/App.8h0zS6BR.css"}],"routeData":{"route":"/threads/[id]","isIndex":false,"type":"page","pattern":"^\\/threads\\/([^/]+?)\\/?$","segments":[[{"content":"threads","dynamic":false,"spread":false}],[{"content":"id","dynamic":true,"spread":false}]],"params":["id"],"component":"src/pages/threads/[id].astro","prerender":false,"fallbackRoutes":[],"distURL":[],"origin":"project","_meta":{"trailingSlash":"ignore"}}},{"file":"","links":[],"scripts":[],"styles":[],"routeData":{"route":"/","isIndex":true,"type":"page","pattern":"^\\/$","segments":[],"params":[],"component":"src/pages/index.astro","pathname":"/","prerender":false,"fallbackRoutes":[],"distURL":[],"origin":"project","_meta":{"trailingSlash":"ignore"}}}],"serverLike":true,"middlewareMode":"classic","base":"/","trailingSlash":"ignore","compressHTML":"jsx","componentMetadata":[["/home/runner/work/webr/webr/src/pages/new.astro",{"propagation":"none","containsHead":true}],["/home/runner/work/webr/webr/src/pages/sessions/[session]/tabs/[tab].astro",{"propagation":"none","containsHead":true}],["/home/runner/work/webr/webr/src/pages/settings.astro",{"propagation":"none","containsHead":true}],["/home/runner/work/webr/webr/src/pages/threads/[id].astro",{"propagation":"none","containsHead":true}]],"renderers":[],"clientDirectives":[["idle","(()=>{var l=(n,t)=>{let i=async()=>{await(await n())()},e=typeof t.value==\"object\"?t.value:void 0,s={timeout:e==null?void 0:e.timeout};\"requestIdleCallback\"in window?window.requestIdleCallback(i,s):setTimeout(i,s.timeout||200)};(self.Astro||(self.Astro={})).idle=l;window.dispatchEvent(new Event(\"astro:idle\"));})();"],["load","(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).load=e;window.dispatchEvent(new Event(\"astro:load\"));})();"],["media","(()=>{var n=(a,t)=>{let i=async()=>{await(await a())()};if(t.value){let e=matchMedia(t.value);e.matches?i():e.addEventListener(\"change\",i,{once:!0})}};(self.Astro||(self.Astro={})).media=n;window.dispatchEvent(new Event(\"astro:media\"));})();"],["only","(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event(\"astro:only\"));})();"],["visible","(()=>{var a=(s,i,o)=>{let r=async()=>{await(await s())()},t=typeof i.value==\"object\"?i.value:void 0,c={rootMargin:t==null?void 0:t.rootMargin},n=new IntersectionObserver(e=>{for(let l of e)if(l.isIntersecting){n.disconnect(),r();break}},c);for(let e of o.children)n.observe(e)};(self.Astro||(self.Astro={})).visible=a;window.dispatchEvent(new Event(\"astro:visible\"));})();"]],"entryModules":{"\u0000noop-middleware":"virtual_astro_middleware.mjs","\u0000virtual:astro:server-island-manifest":"chunks/_virtual_astro_server-island-manifest_C1Q2srgE.mjs","\u0000virtual:astro:session-driver":"chunks/_virtual_astro_session-driver_Dp7RXjOf.mjs","\u0000virtual:astro:actions/noop-entrypoint":"chunks/noop-entrypoint_Z3zFhrGC.mjs","/home/runner/work/webr/webr/node_modules/.pnpm/@astrojs+react@7.0.0_@types+node@22.20.4_@types+react-dom@19.3.0_@types+react@19.3.0__@_02aa85dceaccf3d763325676dd8540a8/node_modules/@astrojs/react/dist/vnode-children.js":"chunks/vnode-children_BSi0H4PL.mjs","@astrojs/node/server.js":"entry.mjs","\u0000virtual:astro:page:src/pages/threads/[id]@_@astro":"chunks/_id__Bk-s-r2G.mjs","\u0000virtual:astro:page:src/pages/sessions/[session]/tabs/[tab]@_@astro":"chunks/_tab__BmEFH_DR.mjs","\u0000virtual:astro:page:src/pages/index@_@astro":"chunks/index_Bvv1gpI0.mjs","\u0000virtual:astro:page:src/pages/new@_@astro":"chunks/new_Cpp90K8n.mjs","\u0000virtual:astro:page:node_modules/.pnpm/astro@7.3.5_@types+node@22.20.4_tsx@4.23.15_yaml@2.9.1/node_modules/astro/dist/assets/endpoint/node@_@js":"chunks/node_C8FqXy_n.mjs","\u0000virtual:astro:page:src/pages/settings@_@astro":"chunks/settings_CwcEfyk0.mjs","/home/runner/work/webr/webr/node_modules/.pnpm/astro@7.3.5_@types+node@22.20.4_tsx@4.23.15_yaml@2.9.1/node_modules/astro/dist/assets/services/sharp.js":"chunks/sharp_8J8zk4Wd.mjs","/home/runner/work/webr/webr/src/components/App.tsx":"_astro/App.VJmP4d0F.js","/home/runner/work/webr/webr/node_modules/.pnpm/astro@7.3.5_@types+node@22.20.4_tsx@4.23.15_yaml@2.9.1/node_modules/astro/components/ClientRouter.astro?astro&type=script&index=0&lang.ts":"_astro/ClientRouter.astro_astro_type_script_index_0_lang.CTgtIFyY.js","@astrojs/react/client.js":"_astro/client.BDkzSRUN.js","/home/runner/work/webr/webr/node_modules/.pnpm/@xterm+addon-fit@0.10.0_@xterm+xterm@5.5.0/node_modules/@xterm/addon-fit/lib/addon-fit.js":"_astro/addon-fit.jgKqzwky.js","/home/runner/work/webr/webr/node_modules/.pnpm/@xterm+addon-web-links@0.11.0_@xterm+xterm@5.5.0/node_modules/@xterm/addon-web-links/lib/addon-web-links.js":"_astro/addon-web-links.C9lyQRxI.js","/home/runner/work/webr/webr/node_modules/.pnpm/@xterm+addon-webgl@0.18.0_@xterm+xterm@5.5.0/node_modules/@xterm/addon-webgl/lib/addon-webgl.js":"_astro/addon-webgl.Bd5ox92E.js","/home/runner/work/webr/webr/node_modules/.pnpm/@xterm+xterm@5.5.0/node_modules/@xterm/xterm/lib/xterm.js":"_astro/xterm.B-l5AIf6.js","astro:scripts/before-hydration.js":""},"inlinedScripts":[],"assets":["/apple-touch-icon.png","/brand-mark.png","/favicon-16.png","/favicon-32.png","/favicon.ico","/icon-192.png","/icon-512.png","/icon-maskable-512.png","/manifest.webmanifest","/_astro/App.VJmP4d0F.js","/_astro/ClientRouter.astro_astro_type_script_index_0_lang.CTgtIFyY.js","/_astro/addon-fit.jgKqzwky.js","/_astro/addon-web-links.C9lyQRxI.js","/_astro/addon-webgl.Bd5ox92E.js","/_astro/client.BDkzSRUN.js","/_astro/client.Bbg9OBwT.js","/_astro/client.CLSsMr2g.js","/_astro/xterm.B-l5AIf6.js","/_astro/App.8h0zS6BR.css","/_astro/jetbrains-mono.CFJaiWaY.woff2","/_astro/symbols-nerd.CEdkKAK8.woff2"],"buildFormat":"directory","checkOrigin":true,"actionBodySizeLimit":1048576,"serverIslandBodySizeLimit":1048576,"allowedDomains":[],"key":"Tfus3sS7FZ5+gRAEKgrjaIoXdZDyRDj6cvgUZ/a5r3w=","sessionConfig":{"driver":"unstorage/drivers/fs-lite","options":{"base":"/home/runner/work/webr/webr/node_modules/.astro/sessions"}},"image":{},"devToolbar":{"enabled":false,"debugInfoOutput":""},"logLevel":"info","shouldInjectCspMetaTags":false});
2445
2445
  var manifestRoutes = _manifest.routes;
2446
2446
  var manifest = Object.assign(_manifest, {
2447
2447
  renderers,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pablopunk/webr",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Control your Herdr agents from anywhere.",
5
5
  "keywords": [
6
6
  "herdr",
@@ -111,6 +111,21 @@ export async function createHost(manager: RuntimeManager, origin: string, ssr?:
111
111
  if (icon.contentType === 'image/svg+xml') reply.header('Content-Security-Policy', "sandbox; default-src 'none'; style-src 'unsafe-inline'");
112
112
  return reply.type(icon.contentType).send(icon.bytes);
113
113
  });
114
+ const candidateParams = z.object({ machineId: opaqueId, projectId: opaqueId });
115
+ app.get('/api/projects/:machineId/:projectId/icon-candidates', async (request) => {
116
+ const { machineId, projectId } = candidateParams.parse(request.params);
117
+ const found = await manager.supervisors.get(machineId)?.target.iconCandidates?.(projectId) ?? [];
118
+ return found.map((candidate, index) => ({ name: candidate.name, url: `/api/projects/${encodeURIComponent(machineId)}/${encodeURIComponent(projectId)}/icon-candidates/${index}` }));
119
+ });
120
+ app.get('/api/projects/:machineId/:projectId/icon-candidates/:index', async (request, reply) => {
121
+ const { machineId, projectId, index } = candidateParams.extend({ index: z.coerce.number().int().min(0).max(99) }).parse(request.params);
122
+ const candidate = (await manager.supervisors.get(machineId)?.target.iconCandidates?.(projectId))?.[index];
123
+ if (!candidate) return reply.code(404).send();
124
+ const bytes = await candidate.load().catch(() => undefined);
125
+ if (!bytes) return reply.code(404).send();
126
+ if (candidate.contentType === 'image/svg+xml') reply.header('Content-Security-Policy', "sandbox; default-src 'none'; style-src 'unsafe-inline'");
127
+ return reply.type(candidate.contentType).send(bytes);
128
+ });
114
129
  app.post('/api/threads', async (request, reply) => {
115
130
  const input = launchInput.parse(request.body);
116
131
  const key = z.uuid().parse(request.headers['idempotency-key']);
@@ -3,7 +3,7 @@ import type { NativeSnapshot } from '../protocol/native';
3
3
  import type { TerminalFrame, TerminalStream } from '../terminal/cli';
4
4
  import type { LaunchInput } from '../../shared/runtime';
5
5
  import type { WorktreeEntry } from './herdr-actions';
6
- import type { Icon } from '../transport/icons';
6
+ import type { Icon, IconCandidate } from '../transport/icons';
7
7
 
8
8
  export type TerminalDirection = 'right' | 'down';
9
9
  export type LaunchLocation = { projectId: string; localId?: string; logicalId?: string; path: string; workspaceId: string };
@@ -22,6 +22,7 @@ export type TargetAdapter = {
22
22
  catalog(projectId?: string): Promise<Machine>;
23
23
  canLaunch?(input: LaunchInput): boolean;
24
24
  icon?(projectId: string): Promise<Icon | undefined>;
25
+ iconCandidates?(projectId: string): Promise<IconCandidate[]>;
25
26
  worktrees?(projectId: string): Promise<WorktreeEntry[]>;
26
27
  openWorktree?(projectId: string, path: string): Promise<void>;
27
28
  suggestDirectories?(prefix: string): Promise<string[]>;
@@ -13,7 +13,8 @@ import { harnessName } from '../../lib/models';
13
13
  import { acceptsModelFlag, herdrAgentKinds, isHerdrAgentKind } from '../../shared/agent-kinds';
14
14
  import { modelListings, parseModelListing } from './model-discovery';
15
15
  import { HerdrActions } from '../runtime/herdr-actions';
16
- import { readApprovedIcon, type Icon } from './icons';
16
+ import { listRemoteIconCandidates, readRemoteIcon } from './remote-icons';
17
+ import { listIconCandidates, readApprovedIcon, type Icon, type IconCandidate } from './icons';
17
18
  import { targetFingerprint } from './identity';
18
19
  import { scopedProjectId } from '../../shared/projects';
19
20
  import { validateInstalledSchema } from '../protocol/validate';
@@ -41,6 +42,7 @@ export class HerdrTarget implements TargetAdapter {
41
42
  private connecting?: Promise<SocketApi>;
42
43
  private streams = new Set<ReturnType<typeof openCliStream>>();
43
44
  private icons = new Map<string, { expires: number; value: Promise<Icon | undefined> }>();
45
+ private candidates = new Map<string, { expires: number; value: Promise<IconCandidate[]> }>();
44
46
  constructor(private profile: TargetProfile, private dependencies: Dependencies = { process: boundedProcess, cli: openCliStream }) {
45
47
  if (profile.transport === 'ssh') this.storeUpload = (fileName, bytes) => uploadOverSsh(profile.host!, fileName, bytes);
46
48
  this.id = profile.id; this.name = profile.name; this.session = profile.session;
@@ -126,10 +128,17 @@ export class HerdrTarget implements TargetAdapter {
126
128
  .then((models) => this.discoveredModels.set(kind, { models, expires: Date.now() + MODEL_CACHE_TTL_MS }))
127
129
  .finally(() => this.pendingModels.delete(kind));
128
130
  }
129
- private runOnTarget(command: string, args: string[]) {
131
+ private runOnTarget(command: string, args: string[], timeoutMs = MODEL_LISTING_TIMEOUT_MS, outputLimit = MODEL_LISTING_LIMIT) {
130
132
  return this.profile.transport === 'local'
131
- ? this.dependencies.process(command, args, undefined, MODEL_LISTING_TIMEOUT_MS, MODEL_LISTING_LIMIT)
132
- : this.dependencies.process('ssh', [...sshOptions, this.profile.host!, [command, ...args].map(quoteShell).join(' ')], undefined, MODEL_LISTING_TIMEOUT_MS, MODEL_LISTING_LIMIT);
133
+ ? this.dependencies.process(command, args, undefined, timeoutMs, outputLimit)
134
+ : this.dependencies.process('ssh', [...sshOptions, this.profile.host!, [command, ...args].map(quoteShell).join(' ')], undefined, timeoutMs, outputLimit);
135
+ }
136
+ private runForIcons = (command: string, args: string[], timeoutMs: number, outputLimit: number) => this.runOnTarget(command, args, timeoutMs, outputLimit);
137
+ private async findIcon(path: string) {
138
+ return this.profile.transport === 'local' ? readApprovedIcon(path) : readRemoteIcon(this.runForIcons, await this.expandHome(path));
139
+ }
140
+ private async findIconCandidates(path: string) {
141
+ return this.profile.transport === 'local' ? listIconCandidates(path) : listRemoteIconCandidates(this.runForIcons, await this.expandHome(path));
133
142
  }
134
143
  private async expandHome(path: string) {
135
144
  if (this.profile.transport === 'local') return expandHome(path);
@@ -143,12 +152,20 @@ export class HerdrTarget implements TargetAdapter {
143
152
  suggestDirectories(prefix: string) { return suggestDirectories(prefix, this.profile.transport === 'local' ? listLocalDirectories : this.listRemoteDirectories); }
144
153
  async icon(projectId: string) {
145
154
  const location = this.locations.find((location) => location.projectId === projectId);
146
- if (this.profile.transport !== 'local' || !location) return;
155
+ if (!location) return;
147
156
  const cached = this.icons.get(projectId);
148
157
  if (cached && cached.expires > Date.now()) return cached.value;
149
- const value = readApprovedIcon(location.path);
158
+ const value = this.findIcon(location.path);
150
159
  this.icons.set(projectId, { expires: Date.now() + 60_000, value }); return value;
151
160
  }
161
+ async iconCandidates(projectId: string) {
162
+ const location = this.locations.find((location) => location.projectId === projectId);
163
+ if (!location) return [];
164
+ const cached = this.candidates.get(projectId);
165
+ if (cached && cached.expires > Date.now()) return cached.value;
166
+ const value = this.findIconCandidates(location.path);
167
+ this.candidates.set(projectId, { expires: Date.now() + 60_000, value }); return value;
168
+ }
152
169
  openTerminal: TargetAdapter['openTerminal'] = (terminalId, mode, cols, rows, takeover, onFrame, onClose) => {
153
170
  if (!this.compatible || mode === 'control' && !this.writable || mode === 'observe' && takeover) throw new Error('terminal_control_unavailable');
154
171
  const command = this.command(['terminal', 'session', mode, terminalId, '--cols', String(cols), '--rows', String(rows), ...(takeover ? ['--takeover'] : [])]);
@@ -1,17 +1,19 @@
1
1
  import { readdir, realpath, stat, readFile } from 'node:fs/promises';
2
2
  import { basename, dirname, extname, isAbsolute, join, relative, sep } from 'node:path';
3
3
 
4
- const contentTypes: Record<string, string> = { '.ico': 'image/x-icon', '.png': 'image/png', '.svg': 'image/svg+xml', '.webp': 'image/webp' };
5
- const maxBytes = 2 * 1024 * 1024;
6
- const directories = ['build/Icon.icon/Assets', '', 'public', 'static', 'assets', 'web', 'web/public', 'app/public', 'src/assets', 'src/app', 'frontend/public'];
4
+ export const contentTypes: Record<string, string> = { '.ico': 'image/x-icon', '.png': 'image/png', '.svg': 'image/svg+xml', '.webp': 'image/webp' };
5
+ export const maxBytes = 2 * 1024 * 1024;
6
+ export const directories = ['build/Icon.icon/Assets', '', 'public', 'static', 'assets', 'web', 'web/public', 'app/public', 'src/assets', 'src/app', 'frontend/public'];
7
7
  export type Icon = { bytes: Buffer; contentType: string };
8
+ export type IconCandidate = { name: string; contentType: string; rank: number; load(): Promise<Buffer> };
9
+ export const maxCandidates = 12;
8
10
 
9
11
  function withinRoot(root: string, path: string) {
10
12
  const part = relative(root, path);
11
13
  return part !== '..' && !part.startsWith(`..${sep}`) && !isAbsolute(part);
12
14
  }
13
15
 
14
- function rank(name: string) {
16
+ export function rank(name: string) {
15
17
  const extension = extname(name).toLowerCase();
16
18
  if (!contentTypes[extension]) return Infinity;
17
19
  const stem = name.slice(0, -extension.length).toLowerCase();
@@ -24,7 +26,7 @@ function rank(name: string) {
24
26
  return Infinity;
25
27
  }
26
28
 
27
- async function iconInDirectory(root: string, directory: string): Promise<Icon | undefined> {
29
+ async function* iconsInDirectory(root: string, directory: string): AsyncGenerator<IconCandidate & Icon> {
28
30
  let actual: string;
29
31
  try { actual = await realpath(directory); } catch { return; }
30
32
  if (!withinRoot(root, actual)) return;
@@ -40,7 +42,7 @@ async function iconInDirectory(root: string, directory: string): Promise<Icon |
40
42
  const info = await stat(file);
41
43
  if (!info.isFile() || info.size <= 0 || info.size > maxBytes) continue;
42
44
  const bytes = await readFile(file);
43
- if (bytes.length <= maxBytes) return { bytes, contentType: contentTypes[extname(entry.name).toLowerCase()] };
45
+ if (bytes.length <= maxBytes) yield { bytes, contentType: contentTypes[extname(entry.name).toLowerCase()], name: relative(root, file), rank: rank(entry.name), load: async () => bytes };
44
46
  } catch {}
45
47
  }
46
48
  }
@@ -63,11 +65,27 @@ async function sourceDirectories(root: string) {
63
65
  } catch { return []; }
64
66
  }
65
67
 
68
+ async function searchDirectories(root: string) {
69
+ return [...directories.map((part) => join(root, part)), ...await sourceDirectories(root), ...await appDirectories(root)];
70
+ }
71
+
66
72
  export async function readApprovedIcon(path: string): Promise<Icon | undefined> {
67
73
  let root: string;
68
74
  try { root = await realpath(path); } catch { return; }
69
- for (const directory of [...directories.map((part) => join(root, part)), ...await sourceDirectories(root), ...await appDirectories(root)]) {
70
- const icon = await iconInDirectory(root, directory);
71
- if (icon) return icon;
75
+ for (const directory of await searchDirectories(root)) {
76
+ for await (const icon of iconsInDirectory(root, directory)) return icon;
77
+ }
78
+ }
79
+
80
+ export async function listIconCandidates(path: string): Promise<IconCandidate[]> {
81
+ let root: string;
82
+ try { root = await realpath(path); } catch { return []; }
83
+ const seen = new Set<string>(); const found: IconCandidate[] = [];
84
+ for (const directory of await searchDirectories(root)) {
85
+ for await (const icon of iconsInDirectory(root, directory)) {
86
+ if (seen.has(icon.name)) continue;
87
+ seen.add(icon.name); found.push({ name: icon.name, contentType: icon.contentType, rank: icon.rank, load: icon.load });
88
+ }
72
89
  }
90
+ return found.sort((a, b) => a.rank - b.rank).slice(0, maxCandidates);
73
91
  }
@@ -0,0 +1,58 @@
1
+ import { basename, dirname, extname } from 'node:path';
2
+ import { contentTypes, maxBytes, maxCandidates, rank, type Icon, type IconCandidate } from './icons';
3
+
4
+ export type RunRemote = (command: string, args: string[], timeoutMs: number, outputLimit: number) => Promise<string>;
5
+ const listTimeoutMs = 10_000;
6
+ const listLimit = 256 * 1024;
7
+ const readTimeoutMs = 20_000;
8
+ const readLimit = Math.ceil(maxBytes * 4 / 3) + 1024;
9
+ const maxListed = 2000;
10
+ const safeRelativePath = /^(?!\/)(?!.*(^|\/)\.\.(\/|$))[^\0\r\n]+$/;
11
+
12
+ const listScript = [
13
+ 'cd "$1" 2>/dev/null || exit 0',
14
+ 'for d in build/Icon.icon/Assets . public static assets web web/public app/public src/assets src/app frontend/public src/*/public apps/*/public apps/*/static apps/*/assets apps/*/src/assets apps/*/src/app; do',
15
+ ' [ -d "$d" ] && [ ! -L "$d" ] || continue',
16
+ ' for f in "$d"/*; do [ -f "$f" ] && [ ! -L "$f" ] && printf \'%s\\n\' "$f"; done',
17
+ 'done',
18
+ ].join('\n');
19
+ const readScript = 'cd "$1" && [ -f "$2" ] && [ ! -L "$2" ] && head -c ' + (maxBytes + 1) + ' "$2" | base64';
20
+
21
+ type Listed = { name: string; directoryOrder: number; rank: number };
22
+
23
+ async function listIconFiles(run: RunRemote, root: string): Promise<Listed[]> {
24
+ const lines = (await run('/bin/sh', ['-c', listScript, 'sh', root], listTimeoutMs, listLimit).catch(() => '')).split('\n').slice(0, maxListed);
25
+ const directoryOrder = new Map<string, number>();
26
+ const listed: Listed[] = [];
27
+ for (const line of lines) {
28
+ const name = line.startsWith('./') ? line.slice(2) : line;
29
+ if (!safeRelativePath.test(name) || !Number.isFinite(rank(basename(name)))) continue;
30
+ const directory = dirname(name);
31
+ if (!directoryOrder.has(directory)) directoryOrder.set(directory, directoryOrder.size);
32
+ listed.push({ name, directoryOrder: directoryOrder.get(directory)!, rank: rank(basename(name)) });
33
+ }
34
+ return listed;
35
+ }
36
+
37
+ async function readIconFile(run: RunRemote, root: string, name: string): Promise<Buffer | undefined> {
38
+ const bytes = Buffer.from(await run('/bin/sh', ['-c', readScript, 'sh', root, name], readTimeoutMs, readLimit).catch(() => ''), 'base64');
39
+ return bytes.length > 0 && bytes.length <= maxBytes ? bytes : undefined;
40
+ }
41
+
42
+ const contentTypeOf = (name: string) => contentTypes[extname(name).toLowerCase()];
43
+
44
+ export async function readRemoteIcon(run: RunRemote, root: string): Promise<Icon | undefined> {
45
+ const inPreferredOrder = (await listIconFiles(run, root)).sort((a, b) => a.directoryOrder - b.directoryOrder || a.rank - b.rank || a.name.localeCompare(b.name));
46
+ for (const file of inPreferredOrder.slice(0, maxCandidates)) {
47
+ const bytes = await readIconFile(run, root, file.name);
48
+ if (bytes) return { bytes, contentType: contentTypeOf(file.name) };
49
+ }
50
+ }
51
+
52
+ export async function listRemoteIconCandidates(run: RunRemote, root: string): Promise<IconCandidate[]> {
53
+ const byLikelihood = (await listIconFiles(run, root)).sort((a, b) => a.rank - b.rank || a.directoryOrder - b.directoryOrder || a.name.localeCompare(b.name));
54
+ return byLikelihood.slice(0, maxCandidates).map((file) => ({
55
+ name: file.name, contentType: contentTypeOf(file.name), rank: file.rank,
56
+ load: async () => { const bytes = await readIconFile(run, root, file.name); if (!bytes) throw new Error('icon_unavailable'); return bytes; },
57
+ }));
58
+ }