@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.
- package/dist/client/_astro/{App.CJakfIju.css → App.8h0zS6BR.css} +1 -1
- package/dist/client/_astro/{App.bmHm9djw.js → App.VJmP4d0F.js} +2 -2
- package/dist/server/chunks/{App_BbzCRPiT.mjs → App_CKe0Z9nu.mjs} +44 -2
- package/dist/server/chunks/{_id___SMAFJxg.mjs → _id__Bk-s-r2G.mjs} +1 -1
- package/dist/server/chunks/{_tab__O2UOM0DH.mjs → _tab__BmEFH_DR.mjs} +1 -1
- package/dist/server/chunks/{new_n3R-7e9Z.mjs → new_Cpp90K8n.mjs} +1 -1
- package/dist/server/chunks/{settings_CP_J5pGZ.mjs → settings_CwcEfyk0.mjs} +1 -1
- package/dist/server/entry.mjs +5 -5
- package/package.json +1 -1
- package/src/server/host.ts +15 -0
- package/src/server/runtime/target.ts +2 -1
- package/src/server/transport/herdr-target.ts +23 -6
- package/src/server/transport/icons.ts +27 -9
- package/src/server/transport/remote-icons.ts +58 -0
|
@@ -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 "./
|
|
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 "./
|
|
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 "./
|
|
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 "./
|
|
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,
|
package/dist/server/entry.mjs
CHANGED
|
@@ -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/
|
|
2430
|
-
var _page2 = () => import("./chunks/
|
|
2431
|
-
var _page3 = () => import("./chunks/
|
|
2432
|
-
var _page4 = () => import("./chunks/
|
|
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.
|
|
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
package/src/server/host.ts
CHANGED
|
@@ -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 {
|
|
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,
|
|
132
|
-
: this.dependencies.process('ssh', [...sshOptions, this.profile.host!, [command, ...args].map(quoteShell).join(' ')], undefined,
|
|
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 (
|
|
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 =
|
|
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
|
|
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)
|
|
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
|
|
70
|
-
const icon
|
|
71
|
-
|
|
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
|
+
}
|