@pablopunk/webr 0.1.6 → 0.2.0

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.
@@ -1,7 +1,7 @@
1
1
  import { r as __exportAll } from "./rolldown-runtime_BMI-E3GI.mjs";
2
2
  import { S as createAstro, i as renderComponent, u as renderTemplate } from "./server_CnGYDmxV.mjs";
3
3
  import { t as createComponent } from "./compiler_BiU10Vds.mjs";
4
- import { n as $$Base, t as App } from "./App_CmESMUHE.mjs";
4
+ import { n as $$Base, t as App } from "./App_C2vmjHiY.mjs";
5
5
  //#region src/pages/new.astro
6
6
  var new_exports = /* @__PURE__ */ __exportAll({
7
7
  default: () => $$New,
@@ -1,7 +1,7 @@
1
1
  import { r as __exportAll } from "./rolldown-runtime_BMI-E3GI.mjs";
2
2
  import { S as createAstro, i as renderComponent, u as renderTemplate } from "./server_CnGYDmxV.mjs";
3
3
  import { t as createComponent } from "./compiler_BiU10Vds.mjs";
4
- import { n as $$Base, t as App } from "./App_CmESMUHE.mjs";
4
+ import { n as $$Base, t as App } from "./App_C2vmjHiY.mjs";
5
5
  //#region src/pages/settings.astro
6
6
  var settings_exports = /* @__PURE__ */ __exportAll({
7
7
  default: () => $$Settings,
@@ -2427,10 +2427,10 @@ var renderers = [Object.assign({
2427
2427
  //#endregion
2428
2428
  //#region \0virtual:astro:pages
2429
2429
  var _page0 = () => import("./chunks/node_BcIF-BVp.mjs").then((n) => n.t);
2430
- var _page1 = () => import("./chunks/new_NXgn74q9.mjs");
2431
- var _page2 = () => import("./chunks/_tab__D-smYrCo.mjs");
2432
- var _page3 = () => import("./chunks/settings_D1jcCWzt.mjs");
2433
- var _page4 = () => import("./chunks/_id__CQzMt2xL.mjs");
2430
+ var _page1 = () => import("./chunks/new_bTXSW7C3.mjs");
2431
+ var _page2 = () => import("./chunks/_tab__WmsbMlfC.mjs");
2432
+ var _page3 = () => import("./chunks/settings_DwzWmJBe.mjs");
2433
+ var _page4 = () => import("./chunks/_id__BimjoFCd.mjs");
2434
2434
  var _page5 = () => import("./chunks/index_DRfLd22k.mjs");
2435
2435
  var pageMap = /* @__PURE__ */ new Map([
2436
2436
  ["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],
@@ -2442,7 +2442,7 @@ var pageMap = /* @__PURE__ */ new Map([
2442
2442
  ]);
2443
2443
  //#endregion
2444
2444
  //#region \0virtual:astro:manifest
2445
- 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.1PYvyCIo.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.1PYvyCIo.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.1PYvyCIo.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.1PYvyCIo.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__CQzMt2xL.mjs","\u0000virtual:astro:page:src/pages/sessions/[session]/tabs/[tab]@_@astro":"chunks/_tab__D-smYrCo.mjs","/home/runner/work/webr/webr/node_modules/.pnpm/sharp@0.35.5_@types+node@22.20.4/node_modules/sharp/dist/index.mjs":"chunks/dist_CpTFMtqT.mjs","\u0000virtual:astro:page:src/pages/index@_@astro":"chunks/index_DRfLd22k.mjs","\u0000virtual:astro:page:src/pages/new@_@astro":"chunks/new_NXgn74q9.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_BcIF-BVp.mjs","\u0000virtual:astro:page:src/pages/settings@_@astro":"chunks/settings_D1jcCWzt.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_qIaxzsTp.mjs","/home/runner/work/webr/webr/src/components/App.tsx":"_astro/App.B3aX6cef.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+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.B3aX6cef.js","/_astro/ClientRouter.astro_astro_type_script_index_0_lang.CTgtIFyY.js","/_astro/addon-fit.jgKqzwky.js","/_astro/addon-web-links.C9lyQRxI.js","/_astro/client.BDkzSRUN.js","/_astro/client.Bbg9OBwT.js","/_astro/client.CLSsMr2g.js","/_astro/xterm.B-l5AIf6.js","/_astro/App.1PYvyCIo.css","/_astro/jetbrains-mono.CFJaiWaY.woff2","/_astro/symbols-nerd.CEdkKAK8.woff2"],"buildFormat":"directory","checkOrigin":true,"actionBodySizeLimit":1048576,"serverIslandBodySizeLimit":1048576,"allowedDomains":[],"key":"5xt2blgxRfJ3NG9MkXy57UVS3zt1EKOycbRTt7HZqU0=","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
+ 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.1VFY1ClT.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.1VFY1ClT.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.1VFY1ClT.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.1VFY1ClT.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__BimjoFCd.mjs","\u0000virtual:astro:page:src/pages/sessions/[session]/tabs/[tab]@_@astro":"chunks/_tab__WmsbMlfC.mjs","/home/runner/work/webr/webr/node_modules/.pnpm/sharp@0.35.5_@types+node@22.20.4/node_modules/sharp/dist/index.mjs":"chunks/dist_CpTFMtqT.mjs","\u0000virtual:astro:page:src/pages/index@_@astro":"chunks/index_DRfLd22k.mjs","\u0000virtual:astro:page:src/pages/new@_@astro":"chunks/new_bTXSW7C3.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_BcIF-BVp.mjs","\u0000virtual:astro:page:src/pages/settings@_@astro":"chunks/settings_DwzWmJBe.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_qIaxzsTp.mjs","/home/runner/work/webr/webr/src/components/App.tsx":"_astro/App.CGyf2WDA.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+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.CGyf2WDA.js","/_astro/ClientRouter.astro_astro_type_script_index_0_lang.CTgtIFyY.js","/_astro/addon-fit.jgKqzwky.js","/_astro/addon-web-links.C9lyQRxI.js","/_astro/client.BDkzSRUN.js","/_astro/client.Bbg9OBwT.js","/_astro/client.CLSsMr2g.js","/_astro/xterm.B-l5AIf6.js","/_astro/App.1VFY1ClT.css","/_astro/jetbrains-mono.CFJaiWaY.woff2","/_astro/symbols-nerd.CEdkKAK8.woff2"],"buildFormat":"directory","checkOrigin":true,"actionBodySizeLimit":1048576,"serverIslandBodySizeLimit":1048576,"allowedDomains":[],"key":"vvK85JK/xnlGvvXG68Q9YW77Bippg1hPKqC86TYY3U8=","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});
2446
2446
  var manifestRoutes = _manifest.routes;
2447
2447
  var manifest = Object.assign(_manifest, {
2448
2448
  renderers,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pablopunk/webr",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Control your Herdr agents from anywhere.",
5
5
  "keywords": [
6
6
  "herdr",
@@ -84,5 +84,8 @@
84
84
  "typescript": "5.9.3",
85
85
  "vitest": "5.0.3",
86
86
  "ws": "8.22.0"
87
+ },
88
+ "optionalDependencies": {
89
+ "sherpa-onnx-node": "^1.13.8"
87
90
  }
88
91
  }
package/server/cli.ts CHANGED
@@ -1,7 +1,5 @@
1
- import { join } from 'node:path';
2
1
  import { parseArgs } from 'node:util';
3
2
  import { renderUnicode } from 'uqr';
4
- import { webrHome } from './home';
5
3
  import { runServer } from './run';
6
4
  import { explicitPort, knownPort } from './port';
7
5
  import { defaultDeps, installPlugin, pluginStatus, uninstallWebrPlugin, updatePlugin } from './plugin';
@@ -48,7 +46,6 @@ function applyServerFlags(values: ReturnType<typeof parseCommand>['values']) {
48
46
  if (values.port) process.env.PORT = String(explicitPort(['--port', values.port], {}));
49
47
  if (values.host) process.env.HOST = values.host;
50
48
  if (values.origin) process.env.WEBR_ORIGIN = values.origin;
51
- process.env.WEBR_DATABASE ??= join(webrHome(), 'gateway.sqlite');
52
49
  }
53
50
 
54
51
  function printUpdateHint() {
@@ -7,6 +7,7 @@ type Address = Parameters<typeof isPortOpen>[0];
7
7
 
8
8
  const STOP_ATTEMPTS = 50;
9
9
  const STOP_POLL_MS = 100;
10
+ const START_ATTEMPTS = 100;
10
11
 
11
12
  const pidPath = (stateDir: string) => join(stateDir, PID_FILE);
12
13
  const sleep = (ms: number) => new Promise((done) => setTimeout(done, ms));
@@ -30,10 +31,18 @@ async function waitUntilClosed(address: Address) {
30
31
  return false;
31
32
  }
32
33
 
34
+ async function waitUntilListeningWhileAlive(pid: number, address: Address) {
35
+ for (let attempt = 0; attempt < START_ATTEMPTS && isAlive(pid); attempt++) {
36
+ if (await isPortOpen(address)) return true;
37
+ await sleep(STOP_POLL_MS);
38
+ }
39
+ return false;
40
+ }
41
+
33
42
  export async function stopPluginWebr(stateDir: string, address: Address) {
34
43
  const pid = recordedPid(stateDir);
35
44
  rmSync(pidPath(stateDir), { force: true });
36
- if (!pid || !isAlive(pid) || !(await isPortOpen(address))) return false;
45
+ if (!pid || !isAlive(pid) || !(await waitUntilListeningWhileAlive(pid, address))) return false;
37
46
  process.kill(pid, 'SIGTERM');
38
47
  return waitUntilClosed(address);
39
48
  }
package/server/run.ts CHANGED
@@ -6,8 +6,13 @@ import { HerdrTarget } from '../src/server/transport/herdr-target';
6
6
  import { createHost } from '../src/server/host';
7
7
  import { publicUrls } from '../src/server/public-urls';
8
8
  import { detectTailscaleHost } from '../src/server/tailscale-host';
9
+ import { ensureTailscaleHttps } from '../src/server/tailscale-serve';
9
10
  import { isLoopbackHost } from '../src/server/auth/access';
10
11
  import { resolvePort } from './port';
12
+ import { webrHome } from './home';
13
+ import { Transcriber } from '../src/server/voice/transcriber';
14
+ import { modelDirectory } from '../src/server/voice/model';
15
+ import { join } from 'node:path';
11
16
  import { builtSsr, devSsr } from './ssr';
12
17
 
13
18
  const WILDCARD_HOSTS = ['0.0.0.0', '::'];
@@ -20,13 +25,15 @@ export async function runServer() {
20
25
  if (new URL(origin).origin !== origin || new URL(origin).username || new URL(origin).password) throw new Error('WEBR_ORIGIN must be a normalized HTTP or HTTPS origin without credentials or a path');
21
26
  if (!['http:', 'https:'].includes(new URL(origin).protocol)) throw new Error('WEBR_ORIGIN must use HTTP or HTTPS');
22
27
  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Invalid port');
23
- const database = new MetadataDatabase(process.env.WEBR_DATABASE ?? '.data/gateway.sqlite');
28
+ const database = new MetadataDatabase(process.env.WEBR_DATABASE ?? join(webrHome(), 'gateway.sqlite'));
24
29
  const profiles = await loadRegistry(process.env.WEBR_TARGETS);
25
30
  const manager = new RuntimeManager(database, profiles.map((profile) => new HerdrTarget(profile)));
26
31
  const ssr = process.env.WEBR_DEV === '1' ? await devSsr(port, host) : await builtSsr();
27
32
  const tls = tlsEnabled ? { cert: await readFile(process.env.WEBR_TLS_CERT!), key: await readFile(process.env.WEBR_TLS_KEY!) } : undefined;
28
- const urls = publicUrls(origin, host, port, undefined, await detectTailscaleHost());
29
- const app = await createHost(manager, origin, ssr.handler, tls, undefined, { publicUrls: urls });
33
+ const managesTailscale = !process.env.WEBR_ORIGIN && [...WILDCARD_HOSTS, '127.0.0.1'].includes(host);
34
+ const tailscale = managesTailscale ? await ensureTailscaleHttps(port) : undefined;
35
+ const urls = [...(tailscale?.status === 'ready' ? [tailscale.url] : []), ...publicUrls(origin, host, port, undefined, await detectTailscaleHost())];
36
+ const app = await createHost(manager, origin, ssr.handler, tls, undefined, { publicUrls: urls, tailscale: tailscale?.status }, { devHmrPort: ssr.hmrPort, transcriber: new Transcriber(modelDirectory(webrHome())) });
30
37
  await app.listen({ host, port });
31
38
  manager.start();
32
39
  console.log(`Webr listening at ${origin}`);
package/server/ssr.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { pathToFileURL } from 'node:url';
3
3
  import { distRoot } from '../src/server/dist';
4
+ import { HMR_PATH } from '../src/server/dev-hmr-proxy';
4
5
 
5
6
  type Locals = Record<string, unknown>;
6
- export type Ssr = { handler: (request: object, response: unknown, next: unknown, locals: Locals) => void; stop: () => Promise<void> };
7
+ export type Ssr = { handler: (request: object, response: unknown, next: unknown, locals: Locals) => void; stop: () => Promise<void>; hmrPort?: number };
7
8
 
8
9
  const astroLocalsSymbol = Symbol.for('astro.locals');
9
10
 
@@ -14,10 +15,10 @@ export async function builtSsr(): Promise<Ssr> {
14
15
 
15
16
  export async function devSsr(port: number, host = '127.0.0.1'): Promise<Ssr> {
16
17
  const { dev } = await import('astro');
17
- const astro = await dev({ server: { port: port + 2, host }, vite: { server: { allowedHosts: true, hmr: { port: port + 1 } } } });
18
+ const astro = await dev({ server: { port: port + 2, host }, vite: { server: { allowedHosts: true, hmr: { path: HMR_PATH } } } });
18
19
  const handler: Ssr['handler'] = (request, response, _next, locals) => {
19
20
  Reflect.set(request, astroLocalsSymbol, locals);
20
21
  astro.handle(request as never, response as never);
21
22
  };
22
- return { handler, stop: () => astro.stop() };
23
+ return { handler, stop: () => astro.stop(), hmrPort: port + 2 };
23
24
  }
@@ -4,7 +4,7 @@ const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]']);
4
4
  const FORWARDING_HEADERS = ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-real-ip', 'forwarded', 'via'];
5
5
 
6
6
  const hostnameOf = (host: string | undefined) => { try { return new URL(`http://${host}`).hostname; } catch { return undefined; } };
7
- const isLoopbackAddress = (address: string | undefined) => !!address && (address === '::1' || address.startsWith('127.') || address.startsWith('::ffff:127.'));
7
+ export const isLoopbackAddress = (address: string | undefined) => !!address && (address === '::1' || address.startsWith('127.') || address.startsWith('::ffff:127.'));
8
8
  const isForwarded = (headers: IncomingHttpHeaders) => FORWARDING_HEADERS.some((name) => name in headers) || Object.keys(headers).some((name) => name.startsWith('tailscale-'));
9
9
  const isIpLiteral = (hostname: string) => hostname.startsWith('[') || /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname);
10
10
  const isNameControlledByNetwork = (hostname: string) => hostname.endsWith('.local') || hostname.endsWith('.ts.net');
@@ -2,14 +2,14 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
2
2
  import type { WebSocket } from 'ws';
3
3
  import { z } from 'zod';
4
4
  import type { MetadataDatabase } from '../storage/database';
5
- import { isLocalRequest, sourceAddress } from './access';
5
+ import { isLocalRequest, isLoopbackAddress, sourceAddress } from './access';
6
6
  import { AuditLog } from './audit';
7
7
  import { ApproverPresence } from './presence';
8
8
  import { pairPage } from './pair-page';
9
9
  import { PairingService, RateLimiter, formatInviteToken } from './pairing';
10
10
  import { SessionStore, clearedSessionCookie, deviceNameFrom, sessionCookie, type DeviceSession } from './sessions';
11
11
 
12
- export type AuthOptions = { publicUrls?: string[] };
12
+ export type AuthOptions = { publicUrls?: string[]; tailscale?: 'ready' | 'https-disabled' | 'unavailable' };
13
13
  export type Access = { local: boolean; session?: DeviceSession };
14
14
 
15
15
  const publicRoutes: [method: string, path: RegExp][] = [
@@ -47,8 +47,15 @@ export class Auth {
47
47
  return reply.code(401).send({ error: 'unauthorized' });
48
48
  }
49
49
 
50
+ private isHttpsThroughLocalProxy(request: FastifyRequest) { return request.headers['x-forwarded-proto'] === 'https' && isLoopbackAddress(request.socket.remoteAddress); }
51
+
52
+ private protocolOf(request: FastifyRequest) {
53
+ if (request.headers.host === this.origin.host) return this.origin.protocol;
54
+ return this.isHttpsThroughLocalProxy(request) ? 'https:' : `${request.protocol}:`;
55
+ }
56
+
50
57
  expectedOrigin(request: FastifyRequest) {
51
- return `${request.headers.host === this.origin.host ? this.origin.protocol : `${request.protocol}:`}//${request.headers.host}`;
58
+ return `${this.protocolOf(request)}//${request.headers.host}`;
52
59
  }
53
60
 
54
61
  routes(app: FastifyInstance) {
@@ -97,6 +104,7 @@ export class Auth {
97
104
  this.audit.record('invite_created', { source: sourceOf(request) });
98
105
  return { token: formatInviteToken(invite.token), expiresAt: invite.expiresAt, urls: (this.options.publicUrls ?? []).map((base) => `${base}/#token=${invite.token}`) };
99
106
  });
107
+ app.get('/api/remote/tailscale', async () => ({ status: this.options.tailscale ?? 'ready' }));
100
108
  app.get('/api/audit', async () => this.audit.recent());
101
109
  app.get('/api/devices', async (request) => this.sessions.list().map((device) => ({ ...device, current: device.id === this.access(request).session?.id })));
102
110
  app.post('/api/devices/:id/revoke', async (request, reply) => {
@@ -120,7 +128,7 @@ export class Auth {
120
128
  return publicRoutes.some(([method, pattern]) => method === request.method && pattern.test(path));
121
129
  }
122
130
 
123
- private isSecure(request: FastifyRequest) { return this.origin.protocol === 'https:' && request.headers.host === this.origin.host; }
131
+ private isSecure(request: FastifyRequest) { return this.protocolOf(request) === 'https:'; }
124
132
 
125
133
  private grant(request: FastifyRequest, reply: FastifyReply, deviceName: string) {
126
134
  reply.header('Set-Cookie', sessionCookie(this.sessions.create(deviceName).token, this.isSecure(request)));
@@ -0,0 +1,16 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import { WebSocket } from 'ws';
3
+
4
+ export const HMR_PATH = '/__vite_hmr';
5
+ const HMR_PROTOCOL = 'vite-hmr';
6
+
7
+ export function registerDevHmrProxy(app: FastifyInstance, vitePort: number) {
8
+ app.get(HMR_PATH, { websocket: true }, (browser) => {
9
+ const vite = new WebSocket(`ws://127.0.0.1:${vitePort}${HMR_PATH}`, HMR_PROTOCOL);
10
+ const forwardedWhileConnecting: [Buffer, boolean][] = [];
11
+ browser.on('message', (data, isBinary) => { if (vite.readyState === WebSocket.OPEN) vite.send(data, { binary: isBinary }); else forwardedWhileConnecting.push([data as Buffer, isBinary]); });
12
+ vite.on('open', () => forwardedWhileConnecting.forEach(([data, isBinary]) => vite.send(data, { binary: isBinary })));
13
+ vite.on('message', (data, isBinary) => browser.send(data, { binary: isBinary }));
14
+ for (const [from, to] of [[browser, vite], [vite, browser]] as const) { from.on('close', () => to.close()); from.on('error', () => to.close()); }
15
+ });
16
+ }
@@ -11,12 +11,15 @@ import { createWorkspace } from './runtime/workspace';
11
11
  import { perfLog, perfLogEnabled } from './perf-log';
12
12
  import { Auth, type AuthOptions } from './auth/auth';
13
13
  import { isTrustedHost } from './auth/access';
14
+ import { registerDevHmrProxy } from './dev-hmr-proxy';
15
+ import { registerVoiceRoutes } from './voice/routes';
16
+ import type { Transcriber } from './voice/transcriber';
14
17
  import { MAX_IMAGE_BYTES, UploadStore, uploadContentTypes } from './uploads';
15
18
 
16
19
  const localOwner = 'local';
17
20
  type SsrHandler = (request: FastifyRequest['raw'], response: import('node:http').ServerResponse, next: (error?: unknown) => void, locals: Record<string, unknown>) => void;
18
21
 
19
- export async function createHost(manager: RuntimeManager, origin: string, ssr?: SsrHandler, tls?: { key: Buffer; cert: Buffer }, uploads = new UploadStore(), authOptions: AuthOptions = {}) {
22
+ export async function createHost(manager: RuntimeManager, origin: string, ssr?: SsrHandler, tls?: { key: Buffer; cert: Buffer }, uploads = new UploadStore(), authOptions: AuthOptions = {}, { devHmrPort, transcriber }: { devHmrPort?: number; transcriber?: Transcriber } = {}) {
20
23
  const app = Fastify({ logger: false, bodyLimit: 16 * 1024, ...(tls ? { https: tls } : {}), requestTimeout: 10_000 });
21
24
  const configuredOrigin = new URL(origin);
22
25
  const auth = new Auth(manager.database, configuredOrigin, authOptions);
@@ -43,6 +46,8 @@ export async function createHost(manager: RuntimeManager, origin: string, ssr?:
43
46
  await app.register(websocket, { options: { maxPayload: 32 * 1024, perMessageDeflate: false } });
44
47
  const hub = registerWebsockets(app, manager);
45
48
  auth.routes(app);
49
+ if (devHmrPort) registerDevHmrProxy(app, devHmrPort);
50
+ if (transcriber) registerVoiceRoutes(app, transcriber);
46
51
  app.get('/api/catalog/machines', async () => manager.bootstrap().machines);
47
52
  app.get('/api/catalog/:machineId', async (request) => {
48
53
  const { machineId } = z.object({ machineId: opaqueId }).parse(request.params);
@@ -115,6 +120,10 @@ export async function createHost(manager: RuntimeManager, origin: string, ssr?:
115
120
  const input = z.object({ machineId: opaqueId, archived: z.boolean() }).strict().parse(request.body);
116
121
  manager.archive(input.machineId, id, input.archived); return { archived: input.archived };
117
122
  });
123
+ app.post('/api/projects/rename', async (request) => {
124
+ const input = z.object({ machineId: opaqueId, logicalId: z.string().min(1).max(1000), name: z.string().trim().min(1).max(90).refine((name) => !/[\x00-\x1f\x7f]/.test(name)) }).strict().parse(request.body);
125
+ manager.renameProject(input.machineId, input.logicalId, input.name); return { name: input.name };
126
+ });
118
127
  app.post('/api/threads/:id/rename', async (request) => {
119
128
  const { id } = z.object({ id: z.uuid() }).parse(request.params);
120
129
  const input = z.object({ machineId: opaqueId, title: z.string().trim().min(1).max(90).refine((title) => !/[\x00-\x1f\x7f]/.test(title)) }).strict().parse(request.body);
@@ -160,6 +169,6 @@ export async function createHost(manager: RuntimeManager, origin: string, ssr?:
160
169
  ssr(request.raw, reply.raw, () => { if (!reply.raw.headersSent) { reply.raw.statusCode = 404; reply.raw.end('Not found'); } }, { runtime: manager, accountId: localOwner });
161
170
  });
162
171
  }
163
- app.addHook('onClose', async () => manager.close());
172
+ app.addHook('onClose', async () => { transcriber?.close(); return manager.close(); });
164
173
  return app;
165
174
  }
@@ -3,7 +3,7 @@ import type { TargetAdapter, TerminalDirection } from './target';
3
3
  import type { Thread } from '../../lib/models';
4
4
  import { threadAgent, threadShell } from '../../lib/terminal-split';
5
5
  import { TargetSupervisor } from './supervisor';
6
- import { reconcile } from './reconcile';
6
+ import { projectNameKey, reconcile } from './reconcile';
7
7
  import type { MetadataDatabase } from '../storage/database';
8
8
  import type { Bootstrap, Projection } from '../../shared/runtime';
9
9
  import type { Machine } from '../../lib/machines';
@@ -109,6 +109,12 @@ export class RuntimeManager extends EventEmitter {
109
109
  this.database.saveThread({ ...row.metadata, archivedAt: archived ? new Date().toISOString() : undefined }, row.anchors, row.alias);
110
110
  this.publish(supervisor);
111
111
  }
112
+ renameProject(machineId: string, logicalId: string, name: string) {
113
+ const supervisor = this.supervisors.get(machineId);
114
+ if (!supervisor) throw new Error('machine_disconnected');
115
+ this.database.setSetting(projectNameKey(machineId, logicalId), name);
116
+ this.publish(supervisor);
117
+ }
112
118
  async renameThread(machineId: string, threadId: string, title: string) {
113
119
  const supervisor = this.supervisors.get(machineId);
114
120
  const thread = this.projections.get(machineId)?.threads.find((thread) => thread.id === threadId);
@@ -4,9 +4,11 @@ import type { NativeSnapshot } from '../protocol/native';
4
4
  import type { Project, Thread } from '../../lib/models';
5
5
  import type { TargetAdapter } from './target';
6
6
 
7
+ export const projectNameKey = (machineId: string, logicalId: string) => `project-name:${machineId}:${logicalId}`;
7
8
  export const projectIdentity = (machineId: string, repository: string) => createHash('sha256').update(machineId + '\0' + repository).digest('hex').slice(0, 32);
8
9
  const sameBinding = (thread: Thread, target: TargetAdapter) => thread.session === target.session && thread.bindingFingerprint === target.fingerprint && thread.bindingConfigVersion === target.configVersion;
9
10
  const paneStatus = (panes: NativeSnapshot['panes']) => { const agents = panes.filter((pane) => pane.agent); return ['blocked', 'working', 'done', 'idle', 'unknown'].find((status) => (agents.length ? agents : panes).some((pane) => pane.agent_status === status)) as Thread['status'] | undefined; };
11
+ const readingAwareStatus = (status: string) => status === 'done' ? 'idle' : status;
10
12
  const threadTitle = (label: string | null | undefined, panes: NativeSnapshot['panes'], fallback = 'Terminal') => {
11
13
  const namedTab = label?.trim();
12
14
  if (namedTab && !/^\d+$/.test(namedTab)) return namedTab;
@@ -22,7 +24,8 @@ export function reconcile(database: MetadataDatabase, target: TargetAdapter, sna
22
24
  const cwd = workspace.worktree?.repo_root ?? snapshot.panes.find((pane) => pane.workspace_id === workspace.workspace_id)?.cwd ?? '';
23
25
  const location = target.locations.find((location) => location.workspaceId === workspace.workspace_id || location.path === cwd);
24
26
  const id = location?.projectId ?? projectIdentity(target.id, workspace.worktree?.repo_key ?? (cwd || workspace.workspace_id));
25
- if (!projects.some((project) => project.id === id)) projects.push({ id, machineId: target.id, localId: location?.localId, logicalId: location?.logicalId ?? workspace.worktree?.repo_key ?? id, name: workspace.worktree?.repo_name ?? workspace.label, path: location?.path ?? cwd, color: '#9b83df', initial: (workspace.label[0] ?? 'P').toUpperCase(), iconUrl: target.icon && location ? `/api/projects/${encodeURIComponent(target.id)}/${encodeURIComponent(id)}/icon` : undefined });
27
+ const logicalId = location?.logicalId ?? workspace.worktree?.repo_key ?? id;
28
+ if (!projects.some((project) => project.id === id)) projects.push({ id, machineId: target.id, localId: location?.localId, logicalId, name: database.getSetting(projectNameKey(target.id, logicalId)) ?? workspace.worktree?.repo_name ?? workspace.label, path: location?.path ?? cwd, color: '#9b83df', initial: (workspace.label[0] ?? 'P').toUpperCase(), iconUrl: target.icon && location ? `/api/projects/${encodeURIComponent(target.id)}/${encodeURIComponent(id)}/icon` : undefined });
26
29
  for (const tab of snapshot.tabs.filter((tab) => tab.workspace_id === workspace.workspace_id)) tabProjects.set(tab.tab_id, id);
27
30
  }
28
31
  const launching = database.operations().some((operation) => operation.input.machineId === target.id && ['pending', 'running'].includes(operation.state));
@@ -40,7 +43,7 @@ export function reconcile(database: MetadataDatabase, target: TargetAdapter, sna
40
43
  const members = tab ? snapshot.panes.filter((pane) => pane.tab_id === tab.tab_id && pane.workspace_id === tab.workspace_id) : [];
41
44
  const attached = sameBinding(row.metadata, target) && members.length > 0;
42
45
  const panes = attached ? members : [];
43
- const signature = JSON.stringify([tab?.label, panes.map((pane) => [pane.terminal_id, pane.pane_id, pane.agent, pane.agent_status, pane.title, pane.terminal_title_stripped, pane.focused, snapshot.agents.find((agent) => agent.terminal_id === pane.terminal_id)?.state_change_seq])]);
46
+ const signature = JSON.stringify([tab?.label, panes.map((pane) => [pane.terminal_id, pane.pane_id, pane.agent, readingAwareStatus(pane.agent_status), pane.title, pane.terminal_title_stripped])]);
44
47
  const meaningfulChange = attached && row.metadata.semanticSignature !== signature;
45
48
  const operation = operations.find((operation) => operation.threadId === row.id);
46
49
  const thread: Thread = { ...row.metadata, title: attached ? threadTitle(tab?.label, panes, row.metadata.title) : row.metadata.title, projectId: attached ? tabProjects.get(tab!.tab_id) ?? row.metadata.projectId : row.metadata.projectId, bindingState: attached ? 'attached' : operation && !row.anchors.length ? 'pending' : 'detached', status: attached ? paneStatus(panes) ?? 'unknown' : 'unknown', agent: attached ? panes.find((pane) => pane.agent)?.agent ?? '' : row.metadata.agent, semanticSignature: attached ? signature : row.metadata.semanticSignature, updatedAt: meaningfulChange ? new Date().toISOString() : row.metadata.updatedAt, panes: panes.map((pane) => ({ id: pane.pane_id, terminalId: pane.terminal_id, title: pane.title ?? pane.terminal_title_stripped ?? pane.agent ?? 'Shell', kind: pane.agent ? 'agent' : 'shell' })), operation: operation ? { id: operation.id, state: operation.state, step: operation.step } : undefined };
@@ -0,0 +1,42 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
4
+ const run = promisify(execFile);
5
+ const TAILSCALE_BINARIES = ['tailscale', '/Applications/Tailscale.app/Contents/MacOS/Tailscale'];
6
+ const HTTPS_PORTS = [443, 8443, 10000];
7
+
8
+ type ServeStatus = { TCP?: Record<string, unknown>; Web?: Record<string, { Handlers?: Record<string, { Proxy?: string }> }> };
9
+ type TailscaleStatus = { Self?: { DNSName?: string }; CertDomains?: string[] };
10
+
11
+ const withoutTrailingDot = (name: string) => name.replace(/\.$/, '');
12
+ const portOfProxy = (proxy: string | undefined) => { try { return Number(new URL(proxy ?? '').port); } catch { return undefined; } };
13
+ const proxiesTo = (status: ServeStatus, hostPort: string, port: number) => Object.values(status.Web?.[hostPort]?.Handlers ?? {}).some((handler) => portOfProxy(handler.Proxy) === port);
14
+ const isFree = (status: ServeStatus, name: string, httpsPort: number) => !(String(httpsPort) in (status.TCP ?? {})) && !(`${name}:${httpsPort}` in (status.Web ?? {}));
15
+ const urlFor = (name: string, httpsPort: number) => `https://${name}${httpsPort === 443 ? '' : `:${httpsPort}`}`;
16
+
17
+ export function choosePort(status: ServeStatus, name: string, webrPort: number) {
18
+ const reusable = HTTPS_PORTS.find((httpsPort) => proxiesTo(status, `${name}:${httpsPort}`, webrPort));
19
+ return reusable !== undefined ? { port: reusable, exists: true } : { port: HTTPS_PORTS.find((httpsPort) => isFree(status, name, httpsPort)), exists: false };
20
+ }
21
+
22
+ async function tailscale(args: string[]) {
23
+ for (const binary of TAILSCALE_BINARIES) {
24
+ try { return (await run(binary, args, { timeout: 10_000 })).stdout; } catch { /* try the next binary */ }
25
+ }
26
+ throw new Error('tailscale unavailable');
27
+ }
28
+
29
+ export type TailscaleHttps = { status: 'ready'; url: string } | { status: 'https-disabled' | 'unavailable' };
30
+
31
+ export async function ensureTailscaleHttps(webrPort: number): Promise<TailscaleHttps> {
32
+ try {
33
+ const { Self, CertDomains } = JSON.parse(await tailscale(['status', '--json'])) as TailscaleStatus;
34
+ const name = Self?.DNSName && withoutTrailingDot(Self.DNSName);
35
+ if (!name) return { status: 'unavailable' };
36
+ if (!CertDomains?.includes(name)) return { status: 'https-disabled' };
37
+ const { port, exists } = choosePort(JSON.parse(await tailscale(['serve', 'status', '--json'])) as ServeStatus, name, webrPort);
38
+ if (port === undefined) return { status: 'unavailable' };
39
+ if (!exists) await tailscale(['serve', '--bg', `--https=${port}`, `http://127.0.0.1:${webrPort}`]);
40
+ return { status: 'ready', url: urlFor(name, port) };
41
+ } catch { return { status: 'unavailable' }; }
42
+ }
@@ -2,7 +2,7 @@ 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
4
  const contentTypes: Record<string, string> = { '.ico': 'image/x-icon', '.png': 'image/png', '.svg': 'image/svg+xml', '.webp': 'image/webp' };
5
- const directories = ['', 'public', 'static', 'assets', 'web/public', 'app/public', 'src/assets', 'src/app', 'frontend/public'];
5
+ const directories = ['', 'public', 'static', 'assets', 'web', 'web/public', 'app/public', 'src/assets', 'src/app', 'frontend/public'];
6
6
  export type Icon = { bytes: Buffer; contentType: string };
7
7
 
8
8
  function withinRoot(root: string, path: string) {
@@ -0,0 +1,35 @@
1
+ import { createWriteStream } from 'node:fs';
2
+ import { mkdir, rename, rm, stat } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { Readable } from 'node:stream';
5
+ import { pipeline } from 'node:stream/promises';
6
+
7
+ const REPOSITORY = 'https://huggingface.co/csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8/resolve/main';
8
+ const MODEL_FILES = { encoder: 'encoder.int8.onnx', decoder: 'decoder.int8.onnx', joiner: 'joiner.int8.onnx', tokens: 'tokens.txt' } as const;
9
+
10
+ export type ModelFiles = Record<keyof typeof MODEL_FILES, string>;
11
+
12
+ export const modelDirectory = (home: string) => join(home, 'models', 'parakeet-tdt-0.6b-v3-int8');
13
+ export const modelFiles = (directory: string) => Object.fromEntries(Object.entries(MODEL_FILES).map(([key, name]) => [key, join(directory, name)])) as ModelFiles;
14
+
15
+ const exists = (path: string) => stat(path).then((entry) => entry.size > 0, () => false);
16
+
17
+ export async function isModelDownloaded(directory: string) {
18
+ return (await Promise.all(Object.values(modelFiles(directory)).map(exists))).every(Boolean);
19
+ }
20
+
21
+ async function downloadFile(url: string, destination: string) {
22
+ if (await exists(destination)) return;
23
+ const response = await fetch(url);
24
+ if (!response.ok || !response.body) throw new Error(`Could not download ${url}: HTTP ${response.status}`);
25
+ const partial = `${destination}.part`;
26
+ try {
27
+ await pipeline(Readable.fromWeb(response.body as never), createWriteStream(partial));
28
+ await rename(partial, destination);
29
+ } catch (error) { await rm(partial, { force: true }); throw error; }
30
+ }
31
+
32
+ export async function downloadModel(directory: string, repository = REPOSITORY) {
33
+ await mkdir(directory, { recursive: true });
34
+ for (const name of Object.values(MODEL_FILES)) await downloadFile(`${repository}/${name}`, join(directory, name));
35
+ }
@@ -0,0 +1,29 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import { z } from 'zod';
3
+ import type { Transcriber } from './transcriber';
4
+
5
+ const PCM_CONTENT_TYPE = 'audio/l16';
6
+ const MAX_SECONDS = 600;
7
+ const MAX_SAMPLE_RATE = 48_000;
8
+ export const MAX_VOICE_BYTES = MAX_SECONDS * MAX_SAMPLE_RATE * 2;
9
+
10
+ const int16ToFloat32 = (pcm: Buffer) => {
11
+ const samples = new Float32Array(Math.floor(pcm.length / 2));
12
+ for (let index = 0; index < samples.length; index++) samples[index] = pcm.readInt16LE(index * 2) / 32768;
13
+ return samples;
14
+ };
15
+
16
+ export function registerVoiceRoutes(app: FastifyInstance, transcriber: Transcriber) {
17
+ app.addContentTypeParser(PCM_CONTENT_TYPE, { parseAs: 'buffer', bodyLimit: MAX_VOICE_BYTES }, (_request, body, done) => done(null, body));
18
+ app.get('/api/voice', async () => ({ state: await transcriber.state() }));
19
+ app.post('/api/voice/warm', async () => {
20
+ void transcriber.warm().catch(() => undefined);
21
+ return { state: await transcriber.state() };
22
+ });
23
+ app.post('/api/voice/transcribe', { bodyLimit: MAX_VOICE_BYTES }, async (request, reply) => {
24
+ const { sampleRate } = z.object({ sampleRate: z.coerce.number().int().min(8000).max(MAX_SAMPLE_RATE) }).strict().parse(request.query);
25
+ if (!Buffer.isBuffer(request.body) || !request.body.length) return reply.code(400).send({ error: 'invalid_request' });
26
+ if (await transcriber.state() === 'unavailable') return reply.code(409).send({ error: 'voice_unavailable' });
27
+ return { text: await transcriber.transcribe(int16ToFloat32(request.body), sampleRate) };
28
+ });
29
+ }
@@ -0,0 +1,80 @@
1
+ import { fork, type ChildProcess } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { downloadModel, isModelDownloaded, modelFiles } from './model';
5
+
6
+ export type VoiceState = 'unavailable' | 'needs-download' | 'downloading' | 'ready';
7
+ type Reply = { id: number; text?: string; error?: string };
8
+ type Pending = { resolve: (text: string) => void; reject: (error: Error) => void };
9
+
10
+ const IDLE_UNLOAD_MS = 5 * 60 * 1000;
11
+ const WORKER_PATH = fileURLToPath(new URL('./worker.mjs', import.meta.url));
12
+
13
+ const recognizerInstalled = () => { try { createRequire(import.meta.url).resolve('sherpa-onnx-node'); return true; } catch { return false; } };
14
+
15
+ export class Transcriber {
16
+ private download?: Promise<void>;
17
+ private worker?: Promise<ChildProcess>;
18
+ private pending = new Map<number, Pending>();
19
+ private nextId = 0;
20
+ private idleTimer?: ReturnType<typeof setTimeout>;
21
+ private readonly installed: boolean;
22
+
23
+ constructor(private readonly directory: string, private readonly idleUnloadMs = IDLE_UNLOAD_MS, installed = recognizerInstalled()) {
24
+ this.installed = installed;
25
+ }
26
+
27
+ async state(): Promise<VoiceState> {
28
+ if (!this.installed) return 'unavailable';
29
+ if (this.download) return 'downloading';
30
+ return await isModelDownloaded(this.directory) ? 'ready' : 'needs-download';
31
+ }
32
+
33
+ warm() {
34
+ if (!this.installed) return Promise.reject(new Error('voice_unavailable'));
35
+ this.keepAliveWhileIdle();
36
+ return this.worker ??= this.ensureDownloaded().then(() => this.startWorker()).catch((error) => { this.worker = undefined; throw error; });
37
+ }
38
+
39
+ async transcribe(samples: Float32Array, sampleRate: number) {
40
+ const worker = await this.warm();
41
+ const id = this.nextId++;
42
+ const text = new Promise<string>((resolve, reject) => this.pending.set(id, { resolve, reject }));
43
+ worker.send({ id, samples, sampleRate });
44
+ try { return await text; } finally { this.keepAliveWhileIdle(); }
45
+ }
46
+
47
+ close() { clearTimeout(this.idleTimer); void this.worker?.then((worker) => worker.kill(), () => undefined); this.worker = undefined; }
48
+
49
+ private ensureDownloaded() {
50
+ return this.download ??= isModelDownloaded(this.directory)
51
+ .then((ready) => ready ? undefined : downloadModel(this.directory))
52
+ .finally(() => { this.download = undefined; });
53
+ }
54
+
55
+ private startWorker() {
56
+ return new Promise<ChildProcess>((resolve, reject) => {
57
+ const worker = fork(WORKER_PATH, [], { serialization: 'advanced', stdio: ['ignore', 'inherit', 'inherit', 'ipc'], env: { ...process.env, WEBR_VOICE_MODEL: JSON.stringify(modelFiles(this.directory)) } });
58
+ worker.on('message', (message: Reply & { ready?: boolean }) => { if (message.ready) resolve(worker); else this.settle(message); });
59
+ worker.once('exit', (code) => { this.forgetWorker(worker, new Error(`voice_worker_exited_${code}`)); reject(new Error('voice_worker_failed')); });
60
+ });
61
+ }
62
+
63
+ private settle({ id, text, error }: Reply) {
64
+ const request = this.pending.get(id);
65
+ this.pending.delete(id);
66
+ if (error !== undefined) request?.reject(new Error(error)); else request?.resolve(text ?? '');
67
+ }
68
+
69
+ private forgetWorker(worker: ChildProcess, reason: Error) {
70
+ void this.worker?.then((current) => { if (current === worker) this.worker = undefined; }, () => undefined);
71
+ for (const request of this.pending.values()) request.reject(reason);
72
+ this.pending.clear();
73
+ }
74
+
75
+ private keepAliveWhileIdle() {
76
+ clearTimeout(this.idleTimer);
77
+ this.idleTimer = setTimeout(() => { if (!this.pending.size) this.close(); }, this.idleUnloadMs);
78
+ this.idleTimer.unref();
79
+ }
80
+ }