@pablopunk/webr 0.2.1 → 0.3.2
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/README.md +4 -4
- package/dist/client/_astro/{App.1VFY1ClT.css → App.96TkmFyF.css} +1 -1
- package/dist/client/_astro/App.DcDWCCM1.js +4 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/icon-192.png +0 -0
- package/dist/client/icon-512.png +0 -0
- package/dist/server/chunks/{App_C2vmjHiY.mjs → App_DlKdj0Cm.mjs} +207 -214
- package/dist/server/chunks/{_id__BimjoFCd.mjs → _id__yn5rOSmn.mjs} +1 -1
- package/dist/server/chunks/{_tab__WmsbMlfC.mjs → _tab__C9ScWvmu.mjs} +1 -1
- package/dist/server/chunks/{new_bTXSW7C3.mjs → new_DkzcgJw-.mjs} +1 -1
- package/dist/server/chunks/{settings_DwzWmJBe.mjs → settings_DZDiF9cU.mjs} +1 -1
- package/dist/server/entry.mjs +5 -5
- package/package.json +2 -1
- package/server/rebuilt-restart.ts +7 -7
- package/server/run.ts +3 -0
- package/src/lib/models.ts +1 -1
- package/src/lib/pane-strip.ts +19 -0
- package/src/server/host.ts +6 -6
- package/src/server/runtime/herdr-actions.ts +6 -2
- package/src/server/runtime/manager.ts +27 -24
- package/src/server/runtime/reconcile.ts +2 -1
- package/src/server/runtime/target.ts +1 -0
- package/src/server/transport/herdr-target.ts +1 -0
- package/src/server/transport/registry.ts +1 -1
- package/src/server/transport/saved-machines.ts +2 -2
- package/src/server/websockets.ts +5 -9
- package/dist/client/_astro/App.CGyf2WDA.js +0 -4
- package/src/lib/terminal-split.ts +0 -23
|
@@ -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 "./
|
|
4
|
+
import { n as $$Base, t as App } from "./App_DlKdj0Cm.mjs";
|
|
5
5
|
//#region src/pages/sessions/[session]/tabs/[tab].astro
|
|
6
6
|
var _tab__exports = /* @__PURE__ */ __exportAll({
|
|
7
7
|
default: () => $$Tab,
|
|
@@ -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 "./
|
|
4
|
+
import { n as $$Base, t as App } from "./App_DlKdj0Cm.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 "./
|
|
4
|
+
import { n as $$Base, t as App } from "./App_DlKdj0Cm.mjs";
|
|
5
5
|
//#region src/pages/settings.astro
|
|
6
6
|
var settings_exports = /* @__PURE__ */ __exportAll({
|
|
7
7
|
default: () => $$Settings,
|
package/dist/server/entry.mjs
CHANGED
|
@@ -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/
|
|
2431
|
-
var _page2 = () => import("./chunks/
|
|
2432
|
-
var _page3 = () => import("./chunks/
|
|
2433
|
-
var _page4 = () => import("./chunks/
|
|
2430
|
+
var _page1 = () => import("./chunks/new_DkzcgJw-.mjs");
|
|
2431
|
+
var _page2 = () => import("./chunks/_tab__C9ScWvmu.mjs");
|
|
2432
|
+
var _page3 = () => import("./chunks/settings_DZDiF9cU.mjs");
|
|
2433
|
+
var _page4 = () => import("./chunks/_id__yn5rOSmn.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.
|
|
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.96TkmFyF.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.96TkmFyF.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.96TkmFyF.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.96TkmFyF.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__yn5rOSmn.mjs","\u0000virtual:astro:page:src/pages/sessions/[session]/tabs/[tab]@_@astro":"chunks/_tab__C9ScWvmu.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_DkzcgJw-.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_DZDiF9cU.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.DcDWCCM1.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.DcDWCCM1.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.96TkmFyF.css","/_astro/jetbrains-mono.CFJaiWaY.woff2","/_astro/symbols-nerd.CEdkKAK8.woff2"],"buildFormat":"directory","checkOrigin":true,"actionBodySizeLimit":1048576,"serverIslandBodySizeLimit":1048576,"allowedDomains":[],"key":"bKc+VkF8CNFQW0g8nw7BvIEmCg8RzK+QABjVZlYdtWM=","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.2
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Control your Herdr agents from anywhere.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"herdr",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"node": ">=22.16"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
+
"version": "node scripts/sync-plugin-version.mjs && git add plugin/herdr-plugin.toml",
|
|
42
43
|
"dev": "tsx scripts/dev.ts",
|
|
43
44
|
"build": "astro build",
|
|
44
45
|
"start": "tsx server/start.ts",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { statSync,
|
|
2
|
+
import { statSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { distRoot } from '../src/server/dist';
|
|
5
5
|
|
|
@@ -15,12 +15,12 @@ export const recordPid = (stateDir: string | undefined, pidFile: string) => { if
|
|
|
15
15
|
export function restartWhenRebuilt(stop: () => Promise<void>, relaunch = relaunchSelf) {
|
|
16
16
|
const startedAt = builtAt();
|
|
17
17
|
if (startedAt === undefined) return () => {};
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
if (current
|
|
21
|
-
|
|
18
|
+
const timer = setInterval(() => {
|
|
19
|
+
const current = builtAt();
|
|
20
|
+
if (current === undefined || current === startedAt) return;
|
|
21
|
+
clearInterval(timer);
|
|
22
22
|
console.log('Webr was updated on disk. Restarting to serve the new build.');
|
|
23
23
|
void stop().finally(() => { relaunch(); process.exit(0); });
|
|
24
|
-
});
|
|
25
|
-
return () =>
|
|
24
|
+
}, POLL_MS);
|
|
25
|
+
return () => clearInterval(timer);
|
|
26
26
|
}
|
package/server/run.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { MetadataDatabase } from '../src/server/storage/database';
|
|
3
3
|
import { RuntimeManager } from '../src/server/runtime/manager';
|
|
4
4
|
import { loadRegistry } from '../src/server/transport/registry';
|
|
5
|
+
import { discoverSavedMachines } from '../src/server/transport/saved-machines';
|
|
6
|
+
import { boundedProcess } from '../src/server/transport/process';
|
|
5
7
|
import { HerdrTarget } from '../src/server/transport/herdr-target';
|
|
6
8
|
import { createHost } from '../src/server/host';
|
|
7
9
|
import { publicUrls } from '../src/server/public-urls';
|
|
@@ -38,6 +40,7 @@ export async function runServer() {
|
|
|
38
40
|
const app = await createHost(manager, origin, ssr.handler, tls, undefined, { publicUrls: urls, tailscale: tailscale?.status }, { devHmrPort: ssr.hmrPort, transcriber: new Transcriber(modelDirectory(webrHome())) });
|
|
39
41
|
await app.listen({ host, port });
|
|
40
42
|
manager.start();
|
|
43
|
+
if (!process.env.WEBR_TARGETS) void discoverSavedMachines(process.env, boundedProcess, (profile) => manager.addTarget(new HerdrTarget(profile)));
|
|
41
44
|
recordPid(process.env.HERDR_PLUGIN_STATE_DIR, PID_FILE);
|
|
42
45
|
if (process.env.WEBR_DEV !== '1') restartWhenRebuilt(async () => { await Promise.all([app.close(), ssr.stop()]); database.close(); });
|
|
43
46
|
console.log(`Webr listening at ${origin}`);
|
package/src/lib/models.ts
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Pane, Thread } from './models';
|
|
2
|
+
|
|
3
|
+
export const VISIBLE_PANES = 2;
|
|
4
|
+
|
|
5
|
+
const rankIn = (order: string[], id: string) => { const rank = order.indexOf(id); return rank < 0 ? Infinity : rank; };
|
|
6
|
+
export const orderPanes = <T extends { id: string }>(panes: T[], order: string[] = []) => [...panes].sort((a, b) => rankIn(order, a.id) - rankIn(order, b.id));
|
|
7
|
+
export const insertAfter = (ids: string[], afterId: string, id: string) => { const at = ids.indexOf(afterId); return at < 0 ? [...ids, id] : [...ids.slice(0, at + 1), id, ...ids.slice(at + 1)]; };
|
|
8
|
+
export const canClosePane = (thread: Thread, paneId: string) => thread.panes.length > 1 && thread.panes.some((pane) => pane.id === paneId && pane.kind !== 'agent');
|
|
9
|
+
const neighbourPane = (panes: Pane[], at: number) => panes[at - 1] ?? panes[at + 1];
|
|
10
|
+
export function paneToFocusAfterClosing(panes: Pane[], windowStart: number, paneId: string) {
|
|
11
|
+
const at = panes.findIndex((pane) => pane.id === paneId);
|
|
12
|
+
const otherVisible = panes.slice(windowStart, windowStart + VISIBLE_PANES).find((pane) => pane.id !== paneId);
|
|
13
|
+
return otherVisible ?? neighbourPane(panes, at);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function windowStartShowing(count: number, start: number, index: number, size = VISIBLE_PANES) {
|
|
17
|
+
const shown = index < 0 ? start : index < start ? index : index >= start + size ? index - size + 1 : start;
|
|
18
|
+
return Math.max(0, Math.min(shown, count - size));
|
|
19
|
+
}
|
package/src/server/host.ts
CHANGED
|
@@ -129,15 +129,15 @@ export async function createHost(manager: RuntimeManager, origin: string, ssr?:
|
|
|
129
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);
|
|
130
130
|
await manager.renameThread(input.machineId, id, input.title); return { title: input.title };
|
|
131
131
|
});
|
|
132
|
-
app.post('/api/threads/:id/terminal', async (request) => {
|
|
132
|
+
app.post('/api/threads/:id/panes/terminal', async (request) => {
|
|
133
133
|
const { id } = z.object({ id: z.uuid() }).parse(request.params);
|
|
134
|
-
const input = z.object({ machineId: opaqueId,
|
|
135
|
-
return manager.
|
|
134
|
+
const input = z.object({ machineId: opaqueId, afterPaneId: opaqueId }).strict().parse(request.body);
|
|
135
|
+
return manager.openTerminal(input.machineId, id, input.afterPaneId);
|
|
136
136
|
});
|
|
137
|
-
app.post('/api/threads/:id/
|
|
137
|
+
app.post('/api/threads/:id/panes/close', async (request) => {
|
|
138
138
|
const { id } = z.object({ id: z.uuid() }).parse(request.params);
|
|
139
|
-
const input = z.object({ machineId: opaqueId }).strict().parse(request.body);
|
|
140
|
-
|
|
139
|
+
const input = z.object({ machineId: opaqueId, paneId: opaqueId, force: z.boolean() }).strict().parse(request.body);
|
|
140
|
+
return manager.closePane(input.machineId, id, input.paneId, input.force);
|
|
141
141
|
});
|
|
142
142
|
app.post('/api/threads/:id/delete', async (request) => {
|
|
143
143
|
const { id } = z.object({ id: z.uuid() }).parse(request.params);
|
|
@@ -56,11 +56,15 @@ export class HerdrActions {
|
|
|
56
56
|
catch (error) { if (!(error instanceof Error) || error.message !== 'agent_pane_busy' || Date.now() >= end) throw error; await pause(500); }
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
private async shellOwnsForeground(paneId: string) {
|
|
60
|
+
const info = processInfo.safeParse((await this.api.request('pane.process_info', { pane_id: paneId })).process_info);
|
|
61
|
+
return info.success && info.data.foreground_processes.length > 0 && info.data.foreground_processes.every((process) => process.pid === info.data.shell_pid);
|
|
62
|
+
}
|
|
63
|
+
async paneBusy(paneId: string) { return !await this.shellOwnsForeground(paneId); }
|
|
59
64
|
private async untilShellReady(paneId: string) {
|
|
60
65
|
const end = Date.now() + SHELL_READY_TIMEOUT_MS;
|
|
61
66
|
do {
|
|
62
|
-
|
|
63
|
-
if (info.success && info.data.foreground_processes.length && info.data.foreground_processes.every((process) => process.pid === info.data.shell_pid)) return;
|
|
67
|
+
if (await this.shellOwnsForeground(paneId)) return;
|
|
64
68
|
await pause(250);
|
|
65
69
|
} while (Date.now() < end);
|
|
66
70
|
throw new Error('shell_not_ready');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import type { TargetAdapter
|
|
2
|
+
import type { TargetAdapter } from './target';
|
|
3
3
|
import type { Thread } from '../../lib/models';
|
|
4
|
-
import {
|
|
4
|
+
import { canClosePane, insertAfter } from '../../lib/pane-strip';
|
|
5
5
|
import { TargetSupervisor } from './supervisor';
|
|
6
6
|
import { projectNameKey, reconcile } from './reconcile';
|
|
7
7
|
import type { MetadataDatabase } from '../storage/database';
|
|
@@ -17,20 +17,25 @@ export class RuntimeManager extends EventEmitter {
|
|
|
17
17
|
private projections = new Map<string, Projection>();
|
|
18
18
|
private catalogs = new Map<string, { expires: number; value: Promise<Machine> }>();
|
|
19
19
|
private configVersions = new Map<string, number>();
|
|
20
|
+
private started = false;
|
|
21
|
+
private closed = false;
|
|
20
22
|
constructor(readonly database: MetadataDatabase, targets: TargetAdapter[]) {
|
|
21
23
|
super(); this.setMaxListeners(100);
|
|
22
24
|
this.journal = new LaunchJournal(database, () => this.publishAll());
|
|
23
|
-
for (const target of targets)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
for (const target of targets) this.addTarget(target);
|
|
26
|
+
}
|
|
27
|
+
addTarget(target: TargetAdapter) {
|
|
28
|
+
if (this.closed || this.supervisors.has(target.id)) return;
|
|
29
|
+
target.configVersion = this.database.registerProfile(target.id, target.session, JSON.stringify({ fingerprint: target.fingerprint, locations: target.locations }), target.locations);
|
|
30
|
+
this.configVersions.set(target.id, target.configVersion);
|
|
31
|
+
const supervisor = new TargetSupervisor(target, () => this.publish(supervisor));
|
|
32
|
+
if (target.enabled === false) supervisor.error = 'profile_disabled';
|
|
33
|
+
this.supervisors.set(target.id, supervisor);
|
|
34
|
+
this.publish(supervisor);
|
|
35
|
+
if (this.started && target.enabled !== false) void supervisor.start();
|
|
31
36
|
}
|
|
32
|
-
start() { for (const supervisor of this.supervisors.values()) if (supervisor.target.enabled !== false) void supervisor.start(); }
|
|
33
|
-
async close() { for (const supervisor of this.supervisors.values()) supervisor.stop(); await this.journal.stop(); this.removeAllListeners(); }
|
|
37
|
+
start() { this.started = true; for (const supervisor of this.supervisors.values()) if (supervisor.target.enabled !== false) void supervisor.start(); }
|
|
38
|
+
async close() { this.closed = true; for (const supervisor of this.supervisors.values()) supervisor.stop(); await this.journal.stop(); this.removeAllListeners(); }
|
|
34
39
|
bootstrap(): Bootstrap {
|
|
35
40
|
const projections = [...this.projections.values()];
|
|
36
41
|
return { projections, projects: projections.flatMap((projection) => projection.projects), threads: projections.flatMap((projection) => projection.threads), machines: [...this.supervisors.values()].map((supervisor) => ({ id: supervisor.target.id, name: supervisor.target.name, session: supervisor.target.session, connected: supervisor.connected, projectPaths: Object.fromEntries(supervisor.target.locations.map((location) => [location.projectId, location.path])), harnesses: [], error: supervisor.error, writable: supervisor.target.writable, configVersion: this.configVersions.get(supervisor.target.id) })) };
|
|
@@ -128,23 +133,21 @@ export class RuntimeManager extends EventEmitter {
|
|
|
128
133
|
if (thread.bindingState === 'attached') await supervisor.readFresh();
|
|
129
134
|
this.publish(supervisor);
|
|
130
135
|
}
|
|
131
|
-
async
|
|
136
|
+
async openTerminal(machineId: string, threadId: string, afterPaneId: string) {
|
|
132
137
|
const { supervisor, thread } = this.attachedThread(machineId, threadId);
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
if (!agent || !supervisor.target.splitTerminal) throw new Error('thread_not_found');
|
|
137
|
-
const paneId = await supervisor.target.splitTerminal(agent.id, direction);
|
|
138
|
-
this.saveThreadMetadata(machineId, threadId, { terminalHidden: false });
|
|
138
|
+
if (!thread.panes.some((pane) => pane.id === afterPaneId) || !supervisor.target.splitTerminal) throw new Error('thread_not_found');
|
|
139
|
+
const paneId = await supervisor.target.splitTerminal(afterPaneId, 'right');
|
|
140
|
+
this.saveThreadMetadata(machineId, threadId, { paneOrder: insertAfter(thread.panes.map((pane) => pane.id), afterPaneId, paneId) });
|
|
139
141
|
await supervisor.readFresh(); this.publish(supervisor);
|
|
140
|
-
return { paneId
|
|
142
|
+
return { paneId };
|
|
141
143
|
}
|
|
142
|
-
async
|
|
144
|
+
async closePane(machineId: string, threadId: string, paneId: string, force: boolean) {
|
|
143
145
|
const { supervisor, thread } = this.attachedThread(machineId, threadId);
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
146
|
+
if (!canClosePane(thread, paneId)) return { closed: false, busy: false };
|
|
147
|
+
if (!force && await supervisor.target.paneBusy?.(paneId)) return { closed: false, busy: true };
|
|
148
|
+
await supervisor.target.closePane?.(paneId);
|
|
147
149
|
await supervisor.readFresh(); this.publish(supervisor);
|
|
150
|
+
return { closed: true, busy: false };
|
|
148
151
|
}
|
|
149
152
|
private attachedThread(machineId: string, threadId: string) {
|
|
150
153
|
const supervisor = this.supervisors.get(machineId);
|
|
@@ -3,6 +3,7 @@ import type { MetadataDatabase } from '../storage/database';
|
|
|
3
3
|
import type { NativeSnapshot } from '../protocol/native';
|
|
4
4
|
import type { Project, Thread } from '../../lib/models';
|
|
5
5
|
import type { TargetAdapter } from './target';
|
|
6
|
+
import { orderPanes } from '../../lib/pane-strip';
|
|
6
7
|
|
|
7
8
|
export const projectNameKey = (machineId: string, logicalId: string) => `project-name:${machineId}:${logicalId}`;
|
|
8
9
|
export const projectIdentity = (machineId: string, repository: string) => createHash('sha256').update(machineId + '\0' + repository).digest('hex').slice(0, 32);
|
|
@@ -46,7 +47,7 @@ export function reconcile(database: MetadataDatabase, target: TargetAdapter, sna
|
|
|
46
47
|
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])]);
|
|
47
48
|
const meaningfulChange = attached && row.metadata.semanticSignature !== signature;
|
|
48
49
|
const operation = operations.find((operation) => operation.threadId === row.id);
|
|
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 };
|
|
50
|
+
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: orderPanes(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' as const : 'shell' as const })), row.metadata.paneOrder), operation: operation ? { id: operation.id, state: operation.state, step: operation.step } : undefined };
|
|
50
51
|
if (attached && (meaningfulChange || JSON.stringify(row.anchors) !== JSON.stringify(panes.map((pane) => pane.terminal_id)))) database.saveThread({ ...thread, panes: [], operation: undefined }, panes.map((pane) => pane.terminal_id), row.alias);
|
|
51
52
|
return thread;
|
|
52
53
|
});
|
|
@@ -34,5 +34,6 @@ export type TargetAdapter = {
|
|
|
34
34
|
renameTab?(tabId: string, label: string, renameWorkspace: boolean): Promise<void>;
|
|
35
35
|
splitTerminal?(paneId: string, direction: TerminalDirection): Promise<string>;
|
|
36
36
|
closePane?(paneId: string): Promise<void>;
|
|
37
|
+
paneBusy?(paneId: string): Promise<boolean>;
|
|
37
38
|
close(): void;
|
|
38
39
|
};
|
|
@@ -149,6 +149,7 @@ export class HerdrTarget implements TargetAdapter {
|
|
|
149
149
|
async renameTab(tabId: string, label: string, renameWorkspace: boolean) { return this.actions().renameTab(await this.snapshot(), tabId, label, renameWorkspace); }
|
|
150
150
|
async splitTerminal(paneId: string, direction: TerminalDirection) { return this.actions().splitTerminal(await this.snapshot(), paneId, direction); }
|
|
151
151
|
async closePane(paneId: string) { return this.actions().closePane(paneId); }
|
|
152
|
+
async paneBusy(paneId: string) { return this.actions().paneBusy(paneId); }
|
|
152
153
|
async discardTab(tabId: string, removeWorktree: boolean) { return this.actions().discardTab(await this.snapshot(), tabId, removeWorktree); }
|
|
153
154
|
private actions() {
|
|
154
155
|
return new HerdrActions({ request: async (method, params, options) => {
|
|
@@ -13,7 +13,7 @@ export const profileSchema = z.object({
|
|
|
13
13
|
}).strict().superRefine((value, context) => { if (value.transport === 'ssh' && (!value.host || !value.socket)) context.addIssue({ code: 'custom', message: 'SSH requires a host and a verified absolute socket' }); });
|
|
14
14
|
export type TargetProfile = z.infer<typeof profileSchema>;
|
|
15
15
|
export async function loadRegistry(path?: string): Promise<TargetProfile[]> {
|
|
16
|
-
if (!path) return [await (await import('./local-session')).discoverLocalSession()
|
|
16
|
+
if (!path) return [await (await import('./local-session')).discoverLocalSession()];
|
|
17
17
|
const profiles = z.array(profileSchema).max(32).parse(JSON.parse(await readFile(path, 'utf8')));
|
|
18
18
|
if (new Set(profiles.map((profile) => profile.id)).size !== profiles.length) throw new Error('duplicate_target');
|
|
19
19
|
if (profiles.some((profile) => new Set(profile.locations.map((location) => location.projectId)).size !== profile.locations.length)) throw new Error('duplicate_project_location');
|
|
@@ -32,10 +32,10 @@ async function profileOf(machine: z.infer<typeof savedMachines>[number], env: No
|
|
|
32
32
|
} catch { return undefined; }
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export async function discoverSavedMachines(env = process.env, runProcess = boundedProcess): Promise<TargetProfile[]> {
|
|
35
|
+
export async function discoverSavedMachines(env = process.env, runProcess = boundedProcess, onFound: (profile: TargetProfile) => void = () => {}): Promise<TargetProfile[]> {
|
|
36
36
|
try {
|
|
37
37
|
const machines = savedMachines.parse(JSON.parse(await runProcess('herdr', ['machine', 'list', '--json'], env)));
|
|
38
|
-
const profiles = await Promise.all(machines.filter((machine) => machine.enabled).map((machine) => profileOf(machine, env, runProcess)));
|
|
38
|
+
const profiles = await Promise.all(machines.filter((machine) => machine.enabled).map(async (machine) => { const profile = await profileOf(machine, env, runProcess); if (profile) onFound(profile); return profile; }));
|
|
39
39
|
return profiles.filter((profile): profile is TargetProfile => !!profile);
|
|
40
40
|
} catch { return []; }
|
|
41
41
|
}
|
package/src/server/websockets.ts
CHANGED
|
@@ -37,20 +37,16 @@ export function registerWebsockets(app: FastifyInstance, manager: RuntimeManager
|
|
|
37
37
|
if (pair[kind]) { socket.close(1008); return; }
|
|
38
38
|
pair[kind] = socket;
|
|
39
39
|
if (kind === 'terminal') hub.attach(owner, socket);
|
|
40
|
-
|
|
41
|
-
let commands = Promise.resolve();
|
|
40
|
+
const reportFailure = (action: { streamId: number; generation: number }, error: unknown) => { if (socket.readyState === 1) socket.send(JSON.stringify({ type: 'stream.error', streamId: action.streamId, generation: action.generation, reason: error instanceof Error ? error.message : 'action_failed' })); };
|
|
42
41
|
socket.on('message', (data, binary) => {
|
|
43
|
-
if (binary || kind !== 'terminal'
|
|
42
|
+
if (binary || kind !== 'terminal') { pair!.close(); return; }
|
|
44
43
|
let value: unknown;
|
|
45
44
|
try { value = JSON.parse(data.toString()); } catch { pair!.close(); return; }
|
|
46
45
|
const validated = terminalAction.safeParse(value);
|
|
47
46
|
if (!validated.success) { pair!.close(); return; }
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
try { hub.action(owner, validated.data); }
|
|
52
|
-
catch (error) { if (socket.readyState === 1) socket.send(JSON.stringify({ type: 'stream.error', streamId: validated.data.streamId, generation: validated.data.generation, reason: error instanceof Error ? error.message : 'action_failed' })); }
|
|
53
|
-
}).catch(() => pair!.close()).finally(() => { --pending; });
|
|
47
|
+
if (!pairs.has(owner)) return;
|
|
48
|
+
if (!pair?.metadata || !pair.terminal) { pair!.close(); return; }
|
|
49
|
+
try { hub.action(owner, validated.data); } catch (error) { reportFailure(validated.data, error); }
|
|
54
50
|
});
|
|
55
51
|
socket.on('error', () => pair!.close());
|
|
56
52
|
socket.on('close', () => pair!.close());
|