agent-yes 1.206.0 → 1.207.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.
- package/dist/{SUPPORTED_CLIS-DQSgURGW.js → SUPPORTED_CLIS-B1CDJwYn.js} +2 -2
- package/dist/SUPPORTED_CLIS-vH8hvcZr.js +9 -0
- package/dist/{agentShare-k8224qiZ.js → agentShare-DdEbHtAn.js} +2 -2
- package/dist/cli.js +4 -4
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-BbVnNvlo.js → notifyDaemon-DivHVzZx.js} +2 -2
- package/dist/{rustBinary-BjRvvEs8.js → rustBinary-CuBIr-zX.js} +2 -2
- package/dist/{schedule-DJ-v4yVa.js → schedule-Bg_CDt2j.js} +4 -4
- package/dist/{serve-crLypY_c.js → serve-B-ZCy67S.js} +108 -14
- package/dist/{setup-BPNkeSmY.js → setup-D6UBzk4-.js} +2 -2
- package/dist/{subcommands-DkrMRVNh.js → subcommands-6AQ8egRn.js} +7 -7
- package/dist/{subcommands-DCGZjr4t.js → subcommands-CeRH9-_h.js} +1 -1
- package/dist/{ts-DnqRreF4.js → ts-DZWZvbLM.js} +2 -2
- package/dist/{versionChecker-d5c6_vgS.js → versionChecker-C2CV9VOX.js} +2 -2
- package/dist/{ws-BmHpWaG_.js → ws-CGNmxCPD.js} +2 -2
- package/lab/ui/console-logic.js +20 -0
- package/lab/ui/index.html +149 -24
- package/package.json +1 -1
- package/ts/serve.ts +105 -6
- package/ts/serveLock.spec.ts +15 -14
- package/ts/sizeNego.spec.ts +61 -0
- package/ts/sizeNego.ts +59 -0
- package/ts/ws.spec.ts +8 -10
- package/dist/SUPPORTED_CLIS-CQ6e94ln.js +0 -9
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { t as CLIS_CONFIG } from "./ts-DZWZvbLM.js";
|
|
2
2
|
|
|
3
3
|
//#region ts/SUPPORTED_CLIS.ts
|
|
4
4
|
const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
|
|
5
5
|
|
|
6
6
|
//#endregion
|
|
7
7
|
export { SUPPORTED_CLIS as t };
|
|
8
|
-
//# sourceMappingURL=SUPPORTED_CLIS-
|
|
8
|
+
//# sourceMappingURL=SUPPORTED_CLIS-B1CDJwYn.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "./ts-DZWZvbLM.js";
|
|
2
|
+
import "./logger-CDIsZ-Pp.js";
|
|
3
|
+
import "./versionChecker-C2CV9VOX.js";
|
|
4
|
+
import "./pidStore-BIvsBQ8X.js";
|
|
5
|
+
import "./globalPidIndex-CoNr7tS8.js";
|
|
6
|
+
import "./messageLog-CxrKJj77.js";
|
|
7
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-B1CDJwYn.js";
|
|
8
|
+
|
|
9
|
+
export { SUPPORTED_CLIS };
|
|
@@ -2,7 +2,7 @@ import "./logger-CDIsZ-Pp.js";
|
|
|
2
2
|
import "./globalPidIndex-CoNr7tS8.js";
|
|
3
3
|
import "./messageLog-CxrKJj77.js";
|
|
4
4
|
import "./configShared-aKTg-sa5.js";
|
|
5
|
-
import { M as resolveOne } from "./subcommands-
|
|
5
|
+
import { M as resolveOne } from "./subcommands-6AQ8egRn.js";
|
|
6
6
|
import "./e2e-jb0Hp43q.js";
|
|
7
7
|
import "./webrtcLink-B7REGtK2.js";
|
|
8
8
|
import "./remotes-Cim0dBU7.js";
|
|
@@ -229,4 +229,4 @@ function transformEvent(rawEvent, agentId, forwarded) {
|
|
|
229
229
|
|
|
230
230
|
//#endregion
|
|
231
231
|
export { createScopedShare, listShares, revokeAllShares, revokeShare };
|
|
232
|
-
//# sourceMappingURL=agentShare-
|
|
232
|
+
//# sourceMappingURL=agentShare-DdEbHtAn.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { t as invokedCliName } from "./invokedCli-uqM2YYA7.js";
|
|
3
3
|
import { n as logger } from "./logger-CDIsZ-Pp.js";
|
|
4
|
-
import { i as versionString, n as displayVersion, t as checkAndAutoUpdate } from "./versionChecker-
|
|
5
|
-
import { n as getRustBinary } from "./rustBinary-
|
|
4
|
+
import { i as versionString, n as displayVersion, t as checkAndAutoUpdate } from "./versionChecker-C2CV9VOX.js";
|
|
5
|
+
import { n as getRustBinary } from "./rustBinary-CuBIr-zX.js";
|
|
6
6
|
import { argv } from "process";
|
|
7
7
|
import { spawn } from "child_process";
|
|
8
8
|
import ms from "ms";
|
|
@@ -285,7 +285,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
|
|
|
285
285
|
const rawArg = process.argv[2];
|
|
286
286
|
const managerCommands = !invokedCliName(process.argv);
|
|
287
287
|
const isHelpFlag = rawArg === "-h" || rawArg === "--help";
|
|
288
|
-
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-
|
|
288
|
+
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-CeRH9-_h.js");
|
|
289
289
|
if (isHelpFlag && process.argv.length === 3) {
|
|
290
290
|
await cmdHelp(managerCommands);
|
|
291
291
|
process.exit(0);
|
|
@@ -327,7 +327,7 @@ if (config.useRust) {
|
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
if (rustBinary) {
|
|
330
|
-
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-
|
|
330
|
+
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-vH8hvcZr.js");
|
|
331
331
|
const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
|
|
332
332
|
if (config.verbose) {
|
|
333
333
|
console.log(`[rust] Using binary: ${rustBinary}`);
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-DZWZvbLM.js";
|
|
2
2
|
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-C2CV9VOX.js";
|
|
4
4
|
import "./pidStore-BIvsBQ8X.js";
|
|
5
5
|
import "./globalPidIndex-CoNr7tS8.js";
|
|
6
6
|
import "./messageLog-CxrKJj77.js";
|
|
@@ -2,7 +2,7 @@ import { n as logger } from "./logger-CDIsZ-Pp.js";
|
|
|
2
2
|
import "./globalPidIndex-CoNr7tS8.js";
|
|
3
3
|
import "./messageLog-CxrKJj77.js";
|
|
4
4
|
import "./configShared-aKTg-sa5.js";
|
|
5
|
-
import { $ as daemonLockOwnerPath, G as gcInboxes, J as listInboxParents, K as hostId, Q as daemonLockDir, S as listRecords, W as appendEvent, X as readInbox, Y as liveWatchers, Z as shouldStealLock, _ as isPidAlive, c as deriveLiveState, et as notifyDir, k as renderLogTailLines } from "./subcommands-
|
|
5
|
+
import { $ as daemonLockOwnerPath, G as gcInboxes, J as listInboxParents, K as hostId, Q as daemonLockDir, S as listRecords, W as appendEvent, X as readInbox, Y as liveWatchers, Z as shouldStealLock, _ as isPidAlive, c as deriveLiveState, et as notifyDir, k as renderLogTailLines } from "./subcommands-6AQ8egRn.js";
|
|
6
6
|
import "./e2e-jb0Hp43q.js";
|
|
7
7
|
import "./webrtcLink-B7REGtK2.js";
|
|
8
8
|
import "./remotes-Cim0dBU7.js";
|
|
@@ -589,4 +589,4 @@ async function ensureDaemon() {
|
|
|
589
589
|
|
|
590
590
|
//#endregion
|
|
591
591
|
export { daemonStatus, ensureDaemon, requestDaemonStop, runDaemon };
|
|
592
|
-
//# sourceMappingURL=notifyDaemon-
|
|
592
|
+
//# sourceMappingURL=notifyDaemon-DivHVzZx.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
1
|
+
import { r as getInstalledPackage } from "./versionChecker-C2CV9VOX.js";
|
|
2
2
|
import { execFileSync } from "child_process";
|
|
3
3
|
import { existsSync, mkdirSync, unlinkSync } from "fs";
|
|
4
4
|
import { chmod, copyFile } from "fs/promises";
|
|
@@ -225,4 +225,4 @@ async function getRustBinary(options = {}) {
|
|
|
225
225
|
|
|
226
226
|
//#endregion
|
|
227
227
|
export { getRustBinary as n, findSpawnHiddenLauncher as t };
|
|
228
|
-
//# sourceMappingURL=rustBinary-
|
|
228
|
+
//# sourceMappingURL=rustBinary-CuBIr-zX.js.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import "./ts-
|
|
1
|
+
import "./ts-DZWZvbLM.js";
|
|
2
2
|
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-C2CV9VOX.js";
|
|
4
4
|
import "./pidStore-BIvsBQ8X.js";
|
|
5
5
|
import "./globalPidIndex-CoNr7tS8.js";
|
|
6
6
|
import "./messageLog-CxrKJj77.js";
|
|
7
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-
|
|
7
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-B1CDJwYn.js";
|
|
8
8
|
import { d as resolveSpawnCwd } from "./workspaceConfig-CjaRvTjf.js";
|
|
9
9
|
import { n as liveEnv, t as ensureNodeRuntime } from "./nodeRuntime-CwNJuwH5.js";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
@@ -151,4 +151,4 @@ async function cmdSchedule(rest) {
|
|
|
151
151
|
|
|
152
152
|
//#endregion
|
|
153
153
|
export { cmdSchedule };
|
|
154
|
-
//# sourceMappingURL=schedule-
|
|
154
|
+
//# sourceMappingURL=schedule-Bg_CDt2j.js.map
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import "./ts-
|
|
1
|
+
import "./ts-DZWZvbLM.js";
|
|
2
2
|
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
4
|
-
import { t as findSpawnHiddenLauncher } from "./rustBinary-
|
|
3
|
+
import { r as getInstalledPackage } from "./versionChecker-C2CV9VOX.js";
|
|
4
|
+
import { t as findSpawnHiddenLauncher } from "./rustBinary-CuBIr-zX.js";
|
|
5
5
|
import { t as agentYesHome$1 } from "./agentYesHome-CtHb5b71.js";
|
|
6
6
|
import "./pidStore-BIvsBQ8X.js";
|
|
7
7
|
import { a as updateGlobalPidStatus } from "./globalPidIndex-CoNr7tS8.js";
|
|
8
8
|
import { r as recordInbox } from "./messageLog-CxrKJj77.js";
|
|
9
9
|
import { t as pgidForWrapper } from "./reaper-BUHCyxdF.js";
|
|
10
10
|
import "./configShared-aKTg-sa5.js";
|
|
11
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-
|
|
12
|
-
import { A as renderRawLog, D as recentMessageEdges, E as readPtysize, L as snapshotStatus, M as resolveOne, O as recentReadEdges, S as listRecords, T as readNotes, U as writeToIpc, b as isUserTyping, f as extractNeedsInput, k as renderLogTailLines, l as deriveLiveStatus, o as controlCodeFromName, p as extractTaskCounts, q as isTransientLockMkdirError, tt as TYPING_BADGE, u as extractBadges } from "./subcommands-
|
|
11
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-B1CDJwYn.js";
|
|
12
|
+
import { A as renderRawLog, D as recentMessageEdges, E as readPtysize, L as snapshotStatus, M as resolveOne, O as recentReadEdges, S as listRecords, T as readNotes, U as writeToIpc, b as isUserTyping, f as extractNeedsInput, k as renderLogTailLines, l as deriveLiveStatus, o as controlCodeFromName, p as extractTaskCounts, q as isTransientLockMkdirError, tt as TYPING_BADGE, u as extractBadges } from "./subcommands-6AQ8egRn.js";
|
|
13
13
|
import "./e2e-jb0Hp43q.js";
|
|
14
14
|
import "./webrtcLink-B7REGtK2.js";
|
|
15
15
|
import "./remotes-Cim0dBU7.js";
|
|
@@ -207,6 +207,48 @@ async function acquireWebrtcHostLock(opts) {
|
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region ts/sizeNego.ts
|
|
212
|
+
/** Reject junk caps (a viewer mid-layout can report 0×0 or absurd numbers). */
|
|
213
|
+
const CAP_MIN_COLS = 20;
|
|
214
|
+
const CAP_MIN_ROWS = 5;
|
|
215
|
+
const CAP_MAX_COLS = 500;
|
|
216
|
+
const CAP_MAX_ROWS = 200;
|
|
217
|
+
/** Never negotiate below this — TUIs (claude, codex) break down when the grid
|
|
218
|
+
* gets absurdly narrow, so a tiny watch-sized viewer can't wedge the agent. */
|
|
219
|
+
const NEGO_FLOOR_COLS = 40;
|
|
220
|
+
const NEGO_FLOOR_ROWS = 10;
|
|
221
|
+
/** Parse a presence-reported cap into a sane SizeCap, or null if unusable. */
|
|
222
|
+
function sanitizeCap(cap) {
|
|
223
|
+
if (typeof cap !== "object" || cap === null) return null;
|
|
224
|
+
const c = Math.floor(Number(cap.cols) || 0);
|
|
225
|
+
const r = Math.floor(Number(cap.rows) || 0);
|
|
226
|
+
if (c < CAP_MIN_COLS || c > CAP_MAX_COLS) return null;
|
|
227
|
+
if (r < CAP_MIN_ROWS || r > CAP_MAX_ROWS) return null;
|
|
228
|
+
return {
|
|
229
|
+
cols: c,
|
|
230
|
+
rows: r
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Elementwise minimum over the live viewers' capacities, clamped to the floor.
|
|
235
|
+
* Returns null when no viewer reports a capacity — meaning "withdraw the
|
|
236
|
+
* negotiated size, let the real tty size rule again".
|
|
237
|
+
*/
|
|
238
|
+
function negotiateSize(caps) {
|
|
239
|
+
let cols = Infinity;
|
|
240
|
+
let rows = Infinity;
|
|
241
|
+
for (const cap of caps) {
|
|
242
|
+
if (cap.cols < cols) cols = cap.cols;
|
|
243
|
+
if (cap.rows < rows) rows = cap.rows;
|
|
244
|
+
}
|
|
245
|
+
if (!Number.isFinite(cols) || !Number.isFinite(rows)) return null;
|
|
246
|
+
return {
|
|
247
|
+
cols: Math.max(NEGO_FLOOR_COLS, cols),
|
|
248
|
+
rows: Math.max(NEGO_FLOOR_ROWS, rows)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
210
252
|
//#endregion
|
|
211
253
|
//#region ts/strayServe.ts
|
|
212
254
|
const MANAGEMENT_COMMAND_RE = /\b(?:install|uninstall|status|logs|restart)\b/;
|
|
@@ -387,7 +429,7 @@ function loginShellEnv() {
|
|
|
387
429
|
const delim = "_AY_SHELL_ENV_DELIM_";
|
|
388
430
|
const out = Bun.spawnSync([
|
|
389
431
|
shell,
|
|
390
|
-
"-
|
|
432
|
+
"-lc",
|
|
391
433
|
`printf %s "${delim}"; env -0; printf %s "${delim}"`
|
|
392
434
|
], {
|
|
393
435
|
stdin: "ignore",
|
|
@@ -1467,6 +1509,53 @@ Options:
|
|
|
1467
1509
|
};
|
|
1468
1510
|
const presence = /* @__PURE__ */ new Map();
|
|
1469
1511
|
const PRESENCE_TTL_MS = 12e3;
|
|
1512
|
+
const negoApplied = /* @__PURE__ */ new Map();
|
|
1513
|
+
const negoTimers = /* @__PURE__ */ new Map();
|
|
1514
|
+
const NEGO_DEBOUNCE_MS = 350;
|
|
1515
|
+
const winsizePathFor = (pid) => path.join(process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes"), "winsize", String(pid));
|
|
1516
|
+
const liveCapsFor = (pid) => {
|
|
1517
|
+
const now = Date.now();
|
|
1518
|
+
const caps = [];
|
|
1519
|
+
for (const v of presence.values()) if (now - v.ts <= PRESENCE_TTL_MS && String(v.agent) === String(pid) && v.cap) caps.push(v.cap);
|
|
1520
|
+
return caps;
|
|
1521
|
+
};
|
|
1522
|
+
const applyNego = async (pid) => {
|
|
1523
|
+
negoTimers.delete(pid);
|
|
1524
|
+
const eff = negotiateSize(liveCapsFor(pid));
|
|
1525
|
+
const file = winsizePathFor(pid);
|
|
1526
|
+
const prev = negoApplied.get(pid);
|
|
1527
|
+
try {
|
|
1528
|
+
if (eff) {
|
|
1529
|
+
if (prev && prev.cols === eff.cols && prev.rows === eff.rows) return;
|
|
1530
|
+
const content = `${eff.cols} ${eff.rows} ${Date.now()}\n`;
|
|
1531
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
1532
|
+
await writeFile(file, content);
|
|
1533
|
+
negoApplied.set(pid, {
|
|
1534
|
+
...eff,
|
|
1535
|
+
content
|
|
1536
|
+
});
|
|
1537
|
+
} else {
|
|
1538
|
+
if (!prev) return;
|
|
1539
|
+
negoApplied.delete(pid);
|
|
1540
|
+
try {
|
|
1541
|
+
if (await readFile(file, "utf-8") !== prev.content) return;
|
|
1542
|
+
await unlink(file);
|
|
1543
|
+
} catch {
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
try {
|
|
1548
|
+
process.kill(pid, "SIGWINCH");
|
|
1549
|
+
} catch {}
|
|
1550
|
+
} catch {}
|
|
1551
|
+
};
|
|
1552
|
+
const scheduleNego = (pid) => {
|
|
1553
|
+
if (!Number.isFinite(pid) || pid <= 0 || negoTimers.has(pid)) return;
|
|
1554
|
+
negoTimers.set(pid, setTimeout(() => void applyNego(pid), NEGO_DEBOUNCE_MS));
|
|
1555
|
+
};
|
|
1556
|
+
setInterval(() => {
|
|
1557
|
+
for (const pid of negoApplied.keys()) scheduleNego(pid);
|
|
1558
|
+
}, 5e3);
|
|
1470
1559
|
const apiFetch = async (req) => {
|
|
1471
1560
|
if (!checkAuth(req, token)) return new Response("Unauthorized", { status: 401 });
|
|
1472
1561
|
const url = new URL(req.url);
|
|
@@ -1779,7 +1868,7 @@ Options:
|
|
|
1779
1868
|
}
|
|
1780
1869
|
});
|
|
1781
1870
|
if (req.method === "GET" && p === "/api/ws") {
|
|
1782
|
-
const ws = await import("./ws-
|
|
1871
|
+
const ws = await import("./ws-CGNmxCPD.js");
|
|
1783
1872
|
try {
|
|
1784
1873
|
await ws.loadProvision();
|
|
1785
1874
|
} catch (e) {
|
|
@@ -1799,7 +1888,7 @@ Options:
|
|
|
1799
1888
|
if (req.method === "GET" && p === "/api/ws/status") {
|
|
1800
1889
|
const dirRaw = url.searchParams.get("path");
|
|
1801
1890
|
if (!dirRaw) return new Response("missing ?path=<workspace dir>", { status: 400 });
|
|
1802
|
-
const ws = await import("./ws-
|
|
1891
|
+
const ws = await import("./ws-CGNmxCPD.js");
|
|
1803
1892
|
let prov;
|
|
1804
1893
|
try {
|
|
1805
1894
|
prov = await ws.loadProvision();
|
|
@@ -1864,7 +1953,8 @@ Options:
|
|
|
1864
1953
|
return Response.json({
|
|
1865
1954
|
pid: record.pid,
|
|
1866
1955
|
cols: size?.cols ?? null,
|
|
1867
|
-
rows: size?.rows ?? null
|
|
1956
|
+
rows: size?.rows ?? null,
|
|
1957
|
+
nego: true
|
|
1868
1958
|
});
|
|
1869
1959
|
} catch (e) {
|
|
1870
1960
|
return new Response(e.message, { status: 404 });
|
|
@@ -2145,6 +2235,7 @@ Options:
|
|
|
2145
2235
|
}
|
|
2146
2236
|
const viewer = String(b.viewer ?? "").slice(0, 64);
|
|
2147
2237
|
if (!viewer) return new Response("missing viewer", { status: 400 });
|
|
2238
|
+
const prevAgent = presence.get(viewer)?.agent;
|
|
2148
2239
|
if (b.agent == null) presence.delete(viewer);
|
|
2149
2240
|
else presence.set(viewer, {
|
|
2150
2241
|
viewer,
|
|
@@ -2152,8 +2243,11 @@ Options:
|
|
|
2152
2243
|
cols: Math.max(0, Math.floor(Number(b.cols) || 0)),
|
|
2153
2244
|
rows: Math.max(0, Math.floor(Number(b.rows) || 0)),
|
|
2154
2245
|
sel: typeof b.sel === "string" ? b.sel.slice(0, 200) : null,
|
|
2246
|
+
cap: sanitizeCap(b.cap),
|
|
2155
2247
|
ts: Date.now()
|
|
2156
2248
|
});
|
|
2249
|
+
if (b.agent != null) scheduleNego(Number(b.agent));
|
|
2250
|
+
if (prevAgent != null && String(prevAgent) !== String(b.agent ?? "")) scheduleNego(Number(prevAgent));
|
|
2157
2251
|
return new Response(null, { status: 204 });
|
|
2158
2252
|
}
|
|
2159
2253
|
if (req.method === "GET" && p === "/api/presence") {
|
|
@@ -2355,7 +2449,7 @@ Options:
|
|
|
2355
2449
|
const perm = body.perm ?? "r";
|
|
2356
2450
|
if (perm !== "r" && perm !== "rw") return new Response(`invalid perm ${perm} (want r or rw)`, { status: 400 });
|
|
2357
2451
|
try {
|
|
2358
|
-
const { createScopedShare } = await import("./agentShare-
|
|
2452
|
+
const { createScopedShare } = await import("./agentShare-DdEbHtAn.js");
|
|
2359
2453
|
const share = await createScopedShare({
|
|
2360
2454
|
agent: body.agent,
|
|
2361
2455
|
perm,
|
|
@@ -2370,12 +2464,12 @@ Options:
|
|
|
2370
2464
|
}
|
|
2371
2465
|
}
|
|
2372
2466
|
if (req.method === "GET" && p === "/api/shares") {
|
|
2373
|
-
const { listShares } = await import("./agentShare-
|
|
2467
|
+
const { listShares } = await import("./agentShare-DdEbHtAn.js");
|
|
2374
2468
|
return Response.json(listShares());
|
|
2375
2469
|
}
|
|
2376
2470
|
const revokeM = /^\/api\/share\/([^/]+)$/.exec(p);
|
|
2377
2471
|
if (req.method === "DELETE" && revokeM) {
|
|
2378
|
-
const { revokeShare } = await import("./agentShare-
|
|
2472
|
+
const { revokeShare } = await import("./agentShare-DdEbHtAn.js");
|
|
2379
2473
|
const ok = revokeShare(decodeURIComponent(revokeM[1]));
|
|
2380
2474
|
return new Response(ok ? "revoked" : "no such share", { status: ok ? 200 : 404 });
|
|
2381
2475
|
}
|
|
@@ -2561,7 +2655,7 @@ Options:
|
|
|
2561
2655
|
const shutdown = (resolve) => {
|
|
2562
2656
|
if (heartbeat) clearInterval(heartbeat);
|
|
2563
2657
|
closeShare?.();
|
|
2564
|
-
import("./agentShare-
|
|
2658
|
+
import("./agentShare-DdEbHtAn.js").then((m) => m.revokeAllShares()).catch(() => {});
|
|
2565
2659
|
server?.stop();
|
|
2566
2660
|
Promise.resolve(releaseHostLock?.()).catch(() => {}).then(resolve);
|
|
2567
2661
|
};
|
|
@@ -2574,4 +2668,4 @@ Options:
|
|
|
2574
2668
|
|
|
2575
2669
|
//#endregion
|
|
2576
2670
|
export { cmdServe };
|
|
2577
|
-
//# sourceMappingURL=serve-
|
|
2671
|
+
//# sourceMappingURL=serve-B-ZCy67S.js.map
|
|
@@ -32,7 +32,7 @@ async function cmdSetup(rest) {
|
|
|
32
32
|
if (!existsSync(abs)) process.stderr.write(` note: that directory doesn't exist yet — create it, or agents spawned there will fail\n`);
|
|
33
33
|
if (noShare) return 0;
|
|
34
34
|
process.stdout.write(`\nsharing this machine to agent-yes.com…\n`);
|
|
35
|
-
const { cmdServe } = await import("./serve-
|
|
35
|
+
const { cmdServe } = await import("./serve-B-ZCy67S.js");
|
|
36
36
|
return cmdServe([
|
|
37
37
|
"install",
|
|
38
38
|
"--share",
|
|
@@ -42,4 +42,4 @@ async function cmdSetup(rest) {
|
|
|
42
42
|
|
|
43
43
|
//#endregion
|
|
44
44
|
export { cmdSetup };
|
|
45
|
-
//# sourceMappingURL=setup-
|
|
45
|
+
//# sourceMappingURL=setup-D6UBzk4-.js.map
|
|
@@ -1237,19 +1237,19 @@ async function runSubcommand(argv) {
|
|
|
1237
1237
|
case "restart": return await cmdRestart(rest);
|
|
1238
1238
|
case "note": return await cmdNote(rest);
|
|
1239
1239
|
case "serve": {
|
|
1240
|
-
const { cmdServe } = await import("./serve-
|
|
1240
|
+
const { cmdServe } = await import("./serve-B-ZCy67S.js");
|
|
1241
1241
|
return cmdServe(rest);
|
|
1242
1242
|
}
|
|
1243
1243
|
case "setup": {
|
|
1244
|
-
const { cmdSetup } = await import("./setup-
|
|
1244
|
+
const { cmdSetup } = await import("./setup-D6UBzk4-.js");
|
|
1245
1245
|
return cmdSetup(rest);
|
|
1246
1246
|
}
|
|
1247
1247
|
case "ws": {
|
|
1248
|
-
const { cmdWs } = await import("./ws-
|
|
1248
|
+
const { cmdWs } = await import("./ws-CGNmxCPD.js");
|
|
1249
1249
|
return cmdWs(rest);
|
|
1250
1250
|
}
|
|
1251
1251
|
case "schedule": {
|
|
1252
|
-
const { cmdSchedule } = await import("./schedule-
|
|
1252
|
+
const { cmdSchedule } = await import("./schedule-Bg_CDt2j.js");
|
|
1253
1253
|
return cmdSchedule(rest);
|
|
1254
1254
|
}
|
|
1255
1255
|
case "remote": {
|
|
@@ -4269,7 +4269,7 @@ async function cmdNotify(rest) {
|
|
|
4269
4269
|
}
|
|
4270
4270
|
const ensure = async () => {
|
|
4271
4271
|
if (!argv["ensure-daemon"]) return;
|
|
4272
|
-
const { ensureDaemon } = await import("./notifyDaemon-
|
|
4272
|
+
const { ensureDaemon } = await import("./notifyDaemon-DivHVzZx.js");
|
|
4273
4273
|
await ensureDaemon().catch(() => null);
|
|
4274
4274
|
};
|
|
4275
4275
|
await heartbeatWatcher(parent, selfStartedAt);
|
|
@@ -4343,7 +4343,7 @@ async function cmdNotifyCursor(args) {
|
|
|
4343
4343
|
}
|
|
4344
4344
|
async function cmdNotifyd(rest) {
|
|
4345
4345
|
const sub = rest[0] ?? "status";
|
|
4346
|
-
const daemon = await import("./notifyDaemon-
|
|
4346
|
+
const daemon = await import("./notifyDaemon-DivHVzZx.js");
|
|
4347
4347
|
switch (sub) {
|
|
4348
4348
|
case "run": return daemon.runDaemon();
|
|
4349
4349
|
case "once": return daemon.runDaemon({ once: true });
|
|
@@ -4370,4 +4370,4 @@ async function cmdNotifyd(rest) {
|
|
|
4370
4370
|
|
|
4371
4371
|
//#endregion
|
|
4372
4372
|
export { daemonLockOwnerPath as $, renderRawLog as A, submitAndConfirm as B, matchKeyword as C, recentMessageEdges as D, readPtysize as E, restartHintLines as F, gcInboxes as G, writeKeysPaced as H, runSubcommand as I, listInboxParents as J, hostId as K, snapshotStatus as L, resolveOne as M, resolveReadWindow as N, recentReadEdges as O, resolveResumeArgs as P, daemonLockDir as Q, stdinActivityPath as R, listRecords as S, readNotes as T, writeToIpc as U, waitForLogQuiet as V, appendEvent as W, readInbox as X, liveWatchers as Y, shouldStealLock as Z, isPidAlive as _, cmdHelp as a, isUserTyping as b, deriveLiveState as c, extractMenu as d, notifyDir as et, extractNeedsInput as f, isExitRequest as g, isAgentStuck as h, backoffWhileTyping as i, renderRawLogLines as j, renderLogTailLines as k, deriveLiveStatus as l, finalizedLines as m, READ_PAGE_DEFAULT as n, controlCodeFromName as o, extractTaskCounts as p, isTransientLockMkdirError as q, TYPING_WINDOW_MS as r, cursorAbs as s, GRACEFUL_EXIT_COMMANDS as t, TYPING_BADGE as tt, extractBadges as u, isSlashCommand as v, menuSelectKeys as w, lastStdinAt as x, isSubcommand as y, stopTipForCli as z };
|
|
4373
|
-
//# sourceMappingURL=subcommands-
|
|
4373
|
+
//# sourceMappingURL=subcommands-6AQ8egRn.js.map
|
|
@@ -2,7 +2,7 @@ import "./logger-CDIsZ-Pp.js";
|
|
|
2
2
|
import "./globalPidIndex-CoNr7tS8.js";
|
|
3
3
|
import "./messageLog-CxrKJj77.js";
|
|
4
4
|
import "./configShared-aKTg-sa5.js";
|
|
5
|
-
import { A as renderRawLog, B as submitAndConfirm, C as matchKeyword, D as recentMessageEdges, E as readPtysize, F as restartHintLines, H as writeKeysPaced, I as runSubcommand, L as snapshotStatus, M as resolveOne, N as resolveReadWindow, O as recentReadEdges, P as resolveResumeArgs, R as stdinActivityPath, S as listRecords, T as readNotes, U as writeToIpc, V as waitForLogQuiet, _ as isPidAlive, a as cmdHelp, b as isUserTyping, c as deriveLiveState, d as extractMenu, f as extractNeedsInput, g as isExitRequest, h as isAgentStuck, i as backoffWhileTyping, j as renderRawLogLines, k as renderLogTailLines, l as deriveLiveStatus, m as finalizedLines, n as READ_PAGE_DEFAULT, o as controlCodeFromName, p as extractTaskCounts, r as TYPING_WINDOW_MS, s as cursorAbs, t as GRACEFUL_EXIT_COMMANDS, u as extractBadges, v as isSlashCommand, w as menuSelectKeys, x as lastStdinAt, y as isSubcommand, z as stopTipForCli } from "./subcommands-
|
|
5
|
+
import { A as renderRawLog, B as submitAndConfirm, C as matchKeyword, D as recentMessageEdges, E as readPtysize, F as restartHintLines, H as writeKeysPaced, I as runSubcommand, L as snapshotStatus, M as resolveOne, N as resolveReadWindow, O as recentReadEdges, P as resolveResumeArgs, R as stdinActivityPath, S as listRecords, T as readNotes, U as writeToIpc, V as waitForLogQuiet, _ as isPidAlive, a as cmdHelp, b as isUserTyping, c as deriveLiveState, d as extractMenu, f as extractNeedsInput, g as isExitRequest, h as isAgentStuck, i as backoffWhileTyping, j as renderRawLogLines, k as renderLogTailLines, l as deriveLiveStatus, m as finalizedLines, n as READ_PAGE_DEFAULT, o as controlCodeFromName, p as extractTaskCounts, r as TYPING_WINDOW_MS, s as cursorAbs, t as GRACEFUL_EXIT_COMMANDS, u as extractBadges, v as isSlashCommand, w as menuSelectKeys, x as lastStdinAt, y as isSubcommand, z as stopTipForCli } from "./subcommands-6AQ8egRn.js";
|
|
6
6
|
import "./e2e-jb0Hp43q.js";
|
|
7
7
|
import "./webrtcLink-B7REGtK2.js";
|
|
8
8
|
import "./remotes-Cim0dBU7.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as logger, t as addTransport } from "./logger-CDIsZ-Pp.js";
|
|
2
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
2
|
+
import { r as getInstalledPackage } from "./versionChecker-C2CV9VOX.js";
|
|
3
3
|
import { t as agentYesHome } from "./agentYesHome-CtHb5b71.js";
|
|
4
4
|
import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-CNMl13dC.js";
|
|
5
5
|
import { t as PidStore } from "./pidStore-BIvsBQ8X.js";
|
|
@@ -1901,4 +1901,4 @@ function sleep(ms) {
|
|
|
1901
1901
|
|
|
1902
1902
|
//#endregion
|
|
1903
1903
|
export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
|
|
1904
|
-
//# sourceMappingURL=ts-
|
|
1904
|
+
//# sourceMappingURL=ts-DZWZvbLM.js.map
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
|
|
8
8
|
//#region package.json
|
|
9
9
|
var name = "agent-yes";
|
|
10
|
-
var version = "1.
|
|
10
|
+
var version = "1.207.0";
|
|
11
11
|
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region ts/versionChecker.ts
|
|
@@ -215,4 +215,4 @@ async function displayVersion() {
|
|
|
215
215
|
|
|
216
216
|
//#endregion
|
|
217
217
|
export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
|
|
218
|
-
//# sourceMappingURL=versionChecker-
|
|
218
|
+
//# sourceMappingURL=versionChecker-C2CV9VOX.js.map
|
|
@@ -2,7 +2,7 @@ import "./logger-CDIsZ-Pp.js";
|
|
|
2
2
|
import "./globalPidIndex-CoNr7tS8.js";
|
|
3
3
|
import "./messageLog-CxrKJj77.js";
|
|
4
4
|
import "./configShared-aKTg-sa5.js";
|
|
5
|
-
import { S as listRecords } from "./subcommands-
|
|
5
|
+
import { S as listRecords } from "./subcommands-6AQ8egRn.js";
|
|
6
6
|
import "./e2e-jb0Hp43q.js";
|
|
7
7
|
import "./webrtcLink-B7REGtK2.js";
|
|
8
8
|
import "./remotes-Cim0dBU7.js";
|
|
@@ -418,4 +418,4 @@ async function cmdWs(args) {
|
|
|
418
418
|
|
|
419
419
|
//#endregion
|
|
420
420
|
export { WS_JSON_SCHEMA, cmdWs, collectWorkspaces, isPathInside, loadProvision, workspaceStatus };
|
|
421
|
-
//# sourceMappingURL=ws-
|
|
421
|
+
//# sourceMappingURL=ws-CGNmxCPD.js.map
|
package/lab/ui/console-logic.js
CHANGED
|
@@ -701,6 +701,26 @@ export function fitTransform(gridW, gridH, paneW, paneH) {
|
|
|
701
701
|
return s > 0.985 && s < 1.04 ? "none" : "scale(" + s.toFixed(4) + ")";
|
|
702
702
|
}
|
|
703
703
|
|
|
704
|
+
// Centered variant: same scale selection as fitTransform, plus a translate that
|
|
705
|
+
// centers the scaled grid in the pane (fit-width → vertically centered,
|
|
706
|
+
// fit-height → horizontally centered). transform-origin stays top-left, so the
|
|
707
|
+
// translate is in pane px and consumers mapping grid→screen coords (peer
|
|
708
|
+
// overlays) can read the offset back off this same object. "none" (the crisp
|
|
709
|
+
// driver path) never gets a translate — grid ≈ pane, nothing to center.
|
|
710
|
+
export function fitTransformCentered(gridW, gridH, paneW, paneH) {
|
|
711
|
+
const t = fitTransform(gridW, gridH, paneW, paneH);
|
|
712
|
+
if (t === "none") return { transform: "none", scale: 1, dx: 0, dy: 0 };
|
|
713
|
+
const s = Math.min(paneW / gridW, paneH / gridH);
|
|
714
|
+
const dx = Math.max(0, (paneW - gridW * s) / 2);
|
|
715
|
+
const dy = Math.max(0, (paneH - gridH * s) / 2);
|
|
716
|
+
return {
|
|
717
|
+
transform: `translate(${dx.toFixed(1)}px, ${dy.toFixed(1)}px) ` + t,
|
|
718
|
+
scale: s,
|
|
719
|
+
dx,
|
|
720
|
+
dy,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
704
724
|
// Browser-tab title: "<glyph> <selected agent title> - agent-yes", or the bare
|
|
705
725
|
// console title when nothing is selected (blank/whitespace name). The leading
|
|
706
726
|
// glyph mirrors the agent's status dot — ⌨ needs_input ("your turn"), ⚠ stuck,
|
package/lab/ui/index.html
CHANGED
|
@@ -2173,7 +2173,7 @@
|
|
|
2173
2173
|
selFromBottom,
|
|
2174
2174
|
parseSel,
|
|
2175
2175
|
selSegments,
|
|
2176
|
-
|
|
2176
|
+
fitTransformCentered,
|
|
2177
2177
|
docTitle,
|
|
2178
2178
|
omniScore,
|
|
2179
2179
|
} from "./console-logic.js";
|
|
@@ -2230,7 +2230,15 @@
|
|
|
2230
2230
|
} catch {}
|
|
2231
2231
|
if (term) {
|
|
2232
2232
|
term.options.fontSize = termFontSize;
|
|
2233
|
-
|
|
2233
|
+
// Negotiating host: the grid is canonical (host-owned) — a font change
|
|
2234
|
+
// here only alters our glyph px, so re-letterbox and report the new
|
|
2235
|
+
// capacity; the host reflows the PTY if our readable cols/rows moved.
|
|
2236
|
+
// This is what makes font size a ZOOM control on mobile: bigger font →
|
|
2237
|
+
// fewer readable cells → the PTY itself shrinks → bigger crisp text.
|
|
2238
|
+
if (negoActive) {
|
|
2239
|
+
applyCanvasScale();
|
|
2240
|
+
sendPresence();
|
|
2241
|
+
} else if (fit)
|
|
2234
2242
|
try {
|
|
2235
2243
|
fit.fit();
|
|
2236
2244
|
} catch {}
|
|
@@ -3132,6 +3140,13 @@
|
|
|
3132
3140
|
// short lease after that, the heartbeat won't follow/override — so our own
|
|
3133
3141
|
// push isn't fought while the agent's /api/size is still catching up.
|
|
3134
3142
|
let lastDroveAt = 0;
|
|
3143
|
+
// The selected agent's host negotiates the PTY size from viewer
|
|
3144
|
+
// capacities (its /api/size responses carry nego:true). On such hosts we
|
|
3145
|
+
// never push /api/resize — we report our readable capacity in presence
|
|
3146
|
+
// (sendPresence cap) and follow the negotiated canonical grid, so a phone
|
|
3147
|
+
// and a desktop watching the same agent converge on the smallest
|
|
3148
|
+
// viewer's readable size instead of fighting last-writer-wins.
|
|
3149
|
+
let negoActive = false;
|
|
3135
3150
|
|
|
3136
3151
|
function isDarkColor(c) {
|
|
3137
3152
|
try {
|
|
@@ -3277,6 +3292,18 @@
|
|
|
3277
3292
|
const s = term.getSelectionPosition && term.getSelectionPosition();
|
|
3278
3293
|
selR = selFromBottom(s, term.buffer.active.length);
|
|
3279
3294
|
} catch {}
|
|
3295
|
+
// Our readable capacity: the grid our pane fits at the current font —
|
|
3296
|
+
// measured from the container (transform-independent), NOT term.cols
|
|
3297
|
+
// (which may be a foreign canonical grid we're letterboxing). The host
|
|
3298
|
+
// negotiates the PTY to the min cap across viewers; read-only viewers
|
|
3299
|
+
// report none (they can't influence the PTY). Older hosts ignore it.
|
|
3300
|
+
let cap = null;
|
|
3301
|
+
try {
|
|
3302
|
+
if (!isReadonlyEnt(cur.e) && fit) {
|
|
3303
|
+
const d = fit.proposeDimensions();
|
|
3304
|
+
if (d && d.cols > 0 && d.rows > 0) cap = { cols: d.cols, rows: d.rows };
|
|
3305
|
+
}
|
|
3306
|
+
} catch {}
|
|
3280
3307
|
cur.tx
|
|
3281
3308
|
.post("/api/presence", {
|
|
3282
3309
|
viewer: myViewerId,
|
|
@@ -3284,6 +3311,7 @@
|
|
|
3284
3311
|
cols: term.cols,
|
|
3285
3312
|
rows: term.rows,
|
|
3286
3313
|
sel: selR,
|
|
3314
|
+
cap,
|
|
3287
3315
|
})
|
|
3288
3316
|
.catch(() => {});
|
|
3289
3317
|
}
|
|
@@ -3600,14 +3628,36 @@
|
|
|
3600
3628
|
const cs = getComputedStyle(logEl);
|
|
3601
3629
|
const pw = logEl.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
|
|
3602
3630
|
const ph = logEl.clientHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom);
|
|
3603
|
-
//
|
|
3604
|
-
|
|
3631
|
+
// fitTransformCentered (console-logic.js, unit-tested) decides
|
|
3632
|
+
// none-vs-scale and centers the letterboxed grid in the pane (fit
|
|
3633
|
+
// width → vertically centered, fit height → horizontally). The peer
|
|
3634
|
+
// overlay reads the transformed screen rect back via
|
|
3635
|
+
// getBoundingClientRect, so the translate needs no extra bookkeeping.
|
|
3636
|
+
xt.style.transform = fitTransformCentered(gw, gh, pw, ph).transform;
|
|
3605
3637
|
} catch {}
|
|
3606
3638
|
}
|
|
3639
|
+
// Pane geometry changed (window resize, splitter drag, soft keyboard, tab
|
|
3640
|
+
// refocus): on legacy hosts re-fit the grid to the pane (the size push
|
|
3641
|
+
// rides term.onResize → pushSize); on negotiating hosts the grid is
|
|
3642
|
+
// canonical — never reflow it locally, just re-letterbox and report the
|
|
3643
|
+
// new capacity so the host can move the min if needed.
|
|
3644
|
+
function refitPane() {
|
|
3645
|
+
if (!term) return;
|
|
3646
|
+
if (negoActive) {
|
|
3647
|
+
applyCanvasScale();
|
|
3648
|
+
sendPresence();
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
if (fit)
|
|
3652
|
+
try {
|
|
3653
|
+
fit.fit();
|
|
3654
|
+
} catch {}
|
|
3655
|
+
}
|
|
3607
3656
|
// Follow the canonical grid: resize our xterm to the agent's size (set by the
|
|
3608
3657
|
// active driver / local owner) WITHOUT reflowing to our pane, then scale to
|
|
3609
3658
|
// fit. No-op when we already match (we're the driver, or sizes agree).
|
|
3610
3659
|
function followAgentSize(sz) {
|
|
3660
|
+
if (sz && sz.nego != null) negoActive = !!sz.nego;
|
|
3611
3661
|
if (!term || !sz || !sz.cols || !sz.rows) return;
|
|
3612
3662
|
// While our own recent push is still settling, don't fight it.
|
|
3613
3663
|
if (Date.now() - lastDroveAt < 6000) {
|
|
@@ -5187,6 +5237,14 @@
|
|
|
5187
5237
|
applyCanvasScale();
|
|
5188
5238
|
return;
|
|
5189
5239
|
}
|
|
5240
|
+
// Negotiating host: the PTY size is the host's min over viewer
|
|
5241
|
+
// capacities, not ours to push. Re-letterbox and refresh our cap
|
|
5242
|
+
// (the pane may have changed) — the host resizes if the min moved.
|
|
5243
|
+
if (negoActive) {
|
|
5244
|
+
applyCanvasScale();
|
|
5245
|
+
sendPresence();
|
|
5246
|
+
return;
|
|
5247
|
+
}
|
|
5190
5248
|
lastDroveAt = Date.now(); // we're driving the size now (lease the grid)
|
|
5191
5249
|
tx.post("/api/resize/" + encodeURIComponent(pid), {
|
|
5192
5250
|
cols: term.cols,
|
|
@@ -5227,6 +5285,7 @@
|
|
|
5227
5285
|
const selKey = e._key;
|
|
5228
5286
|
const fitAndSync = (sz) => {
|
|
5229
5287
|
if (sel !== selKey || !term) return;
|
|
5288
|
+
negoActive = !!(sz && sz.nego);
|
|
5230
5289
|
if (sz && sz.cols && sz.rows) agentSize = sz; // remember for the badge tooltip
|
|
5231
5290
|
// Read-only viewer: don't reflow the agent to our pane (we can't resize
|
|
5232
5291
|
// it, and shouldn't). Adopt the AGENT's own grid and CSS-scale it to fit,
|
|
@@ -5238,6 +5297,22 @@
|
|
|
5238
5297
|
suppressPush = false;
|
|
5239
5298
|
return;
|
|
5240
5299
|
}
|
|
5300
|
+
// Negotiating host: adopt the canonical (negotiated) grid like a
|
|
5301
|
+
// watcher and report our capacity via presence — the host takes the
|
|
5302
|
+
// min over all viewers and resizes the PTY itself. No direct push, so
|
|
5303
|
+
// two viewers can't fight over the size.
|
|
5304
|
+
if (negoActive) {
|
|
5305
|
+
if (sz.cols && sz.rows) followAgentSize(sz);
|
|
5306
|
+
else {
|
|
5307
|
+
// agent has no size on record yet — seed from our own fit
|
|
5308
|
+
try {
|
|
5309
|
+
fit.fit();
|
|
5310
|
+
} catch {}
|
|
5311
|
+
}
|
|
5312
|
+
suppressPush = false;
|
|
5313
|
+
sendPresence(); // cap → host negotiates → size event brings the grid
|
|
5314
|
+
return;
|
|
5315
|
+
}
|
|
5241
5316
|
try {
|
|
5242
5317
|
fit.fit();
|
|
5243
5318
|
} catch {}
|
|
@@ -5413,13 +5488,9 @@
|
|
|
5413
5488
|
renderList();
|
|
5414
5489
|
});
|
|
5415
5490
|
window.addEventListener("resize", () => {
|
|
5416
|
-
// Resizing our window is strong intent → re-fit
|
|
5417
|
-
//
|
|
5418
|
-
|
|
5419
|
-
if (fit)
|
|
5420
|
-
try {
|
|
5421
|
-
fit.fit();
|
|
5422
|
-
} catch {}
|
|
5491
|
+
// Resizing our window is strong intent → re-fit / re-report capacity
|
|
5492
|
+
// (refitPane picks the legacy-push vs negotiate path).
|
|
5493
|
+
refitPane();
|
|
5423
5494
|
applyCanvasScale();
|
|
5424
5495
|
renderPeerSelections(); // cell size changed → reposition peer-selection overlays
|
|
5425
5496
|
});
|
|
@@ -5434,15 +5505,76 @@
|
|
|
5434
5505
|
const onVV = () => {
|
|
5435
5506
|
const kb = Math.max(0, Math.round(window.innerHeight - vv.height - vv.offsetTop));
|
|
5436
5507
|
document.documentElement.style.setProperty("--kb", kb + "px");
|
|
5437
|
-
|
|
5438
|
-
try {
|
|
5439
|
-
fit.fit();
|
|
5440
|
-
} catch {}
|
|
5508
|
+
refitPane();
|
|
5441
5509
|
};
|
|
5442
5510
|
vv.addEventListener("resize", onVV);
|
|
5443
5511
|
vv.addEventListener("scroll", onVV);
|
|
5444
5512
|
onVV();
|
|
5445
5513
|
}
|
|
5514
|
+
// ── mobile pinch on the terminal = terminal zoom, not page zoom ─────────
|
|
5515
|
+
// Two fingers over the log pane adjust the terminal FONT SIZE. On a
|
|
5516
|
+
// negotiating host that reflows the PTY itself (bigger font → fewer
|
|
5517
|
+
// readable cells → the host shrinks the grid → bigger crisp text; smaller
|
|
5518
|
+
// font → more rows/cols), which is what a phone user actually means by
|
|
5519
|
+
// pinch: "show me more / make it readable", with the surrounding UI
|
|
5520
|
+
// staying put. Only 2-touch gestures over .log are intercepted — 1-finger
|
|
5521
|
+
// scroll/selection passes to xterm, and the page keeps its native
|
|
5522
|
+
// pinch-zoom everywhere else (an a11y requirement, WCAG 1.4.4).
|
|
5523
|
+
(function () {
|
|
5524
|
+
const logEl = $("log");
|
|
5525
|
+
if (!logEl) return;
|
|
5526
|
+
let startDist = 0; // 0 = no pinch in flight
|
|
5527
|
+
let startFont = 0;
|
|
5528
|
+
let lastRatio = 1;
|
|
5529
|
+
let baseTransform = "";
|
|
5530
|
+
const dist = (t) => Math.hypot(t[0].clientX - t[1].clientX, t[0].clientY - t[1].clientY);
|
|
5531
|
+
logEl.addEventListener(
|
|
5532
|
+
"touchstart",
|
|
5533
|
+
(ev) => {
|
|
5534
|
+
if (ev.touches.length !== 2) return;
|
|
5535
|
+
ev.preventDefault(); // this gesture is ours — no page zoom
|
|
5536
|
+
startDist = dist(ev.touches);
|
|
5537
|
+
startFont = termFontSize;
|
|
5538
|
+
lastRatio = 1;
|
|
5539
|
+
const xt = logEl.querySelector(".xterm");
|
|
5540
|
+
baseTransform = xt ? xt.style.transform || "none" : "none";
|
|
5541
|
+
},
|
|
5542
|
+
{ passive: false },
|
|
5543
|
+
);
|
|
5544
|
+
logEl.addEventListener(
|
|
5545
|
+
"touchmove",
|
|
5546
|
+
(ev) => {
|
|
5547
|
+
if (ev.touches.length !== 2 || !startDist) return;
|
|
5548
|
+
ev.preventDefault();
|
|
5549
|
+
lastRatio = dist(ev.touches) / startDist;
|
|
5550
|
+
// Live preview: compose the pinch onto the fitted transform. The
|
|
5551
|
+
// grid truly reflows only when the negotiated size lands after the
|
|
5552
|
+
// gesture commits — a brief settle, traded for zero SIGWINCH storm
|
|
5553
|
+
// while the fingers are still moving.
|
|
5554
|
+
const xt = logEl.querySelector(".xterm");
|
|
5555
|
+
if (xt)
|
|
5556
|
+
xt.style.transform =
|
|
5557
|
+
`scale(${lastRatio.toFixed(3)})` +
|
|
5558
|
+
(baseTransform === "none" ? "" : " " + baseTransform);
|
|
5559
|
+
},
|
|
5560
|
+
{ passive: false },
|
|
5561
|
+
);
|
|
5562
|
+
const endPinch = () => {
|
|
5563
|
+
if (!startDist) return;
|
|
5564
|
+
const target = Math.round(startFont * lastRatio);
|
|
5565
|
+
startDist = 0;
|
|
5566
|
+
if (target !== termFontSize) setTermFontSize(target); // nego: cap → host reflows
|
|
5567
|
+
applyCanvasScale(); // clear the preview; re-letterbox at the real grid
|
|
5568
|
+
};
|
|
5569
|
+
logEl.addEventListener("touchend", (ev) => {
|
|
5570
|
+
if (ev.touches.length < 2) endPinch();
|
|
5571
|
+
});
|
|
5572
|
+
logEl.addEventListener("touchcancel", () => endPinch());
|
|
5573
|
+
// Safari's proprietary gesture events fire for pinch regardless of
|
|
5574
|
+
// touch-action; swallow them over the terminal so the page never zooms.
|
|
5575
|
+
for (const t of ["gesturestart", "gesturechange", "gestureend"])
|
|
5576
|
+
logEl.addEventListener(t, (ev) => ev.preventDefault());
|
|
5577
|
+
})();
|
|
5446
5578
|
|
|
5447
5579
|
// Step the selection up/down the (filtered) list — same order the left panel
|
|
5448
5580
|
// renders. Clamps at the ends, scrolls the row into view.
|
|
@@ -6983,11 +7115,7 @@
|
|
|
6983
7115
|
// terminal of a different size) that re-push fights the local terminal
|
|
6984
7116
|
// and reflows the stream on every tab switch — the glitch the eager
|
|
6985
7117
|
// per-focus resync introduced. The one-shot adopt still runs on select.
|
|
6986
|
-
|
|
6987
|
-
try {
|
|
6988
|
-
fit.fit();
|
|
6989
|
-
} catch {}
|
|
6990
|
-
}
|
|
7118
|
+
refitPane();
|
|
6991
7119
|
}
|
|
6992
7120
|
});
|
|
6993
7121
|
watchVersion();
|
|
@@ -7039,10 +7167,7 @@
|
|
|
7039
7167
|
if (!dragging) return;
|
|
7040
7168
|
const w = Math.min(Math.max(e.clientX, 280), window.innerWidth - 360);
|
|
7041
7169
|
app.style.setProperty("--leftw", w + "px");
|
|
7042
|
-
|
|
7043
|
-
try {
|
|
7044
|
-
fit.fit();
|
|
7045
|
-
} catch {}
|
|
7170
|
+
refitPane();
|
|
7046
7171
|
});
|
|
7047
7172
|
const end = (e) => {
|
|
7048
7173
|
if (!dragging) return;
|
package/package.json
CHANGED
package/ts/serve.ts
CHANGED
|
@@ -48,6 +48,7 @@ import { findSpawnHiddenLauncher } from "./rustBinary.ts";
|
|
|
48
48
|
import { pgidForWrapper } from "./reaper.ts";
|
|
49
49
|
import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
|
|
50
50
|
import { getInstalledPackage } from "./versionChecker.ts";
|
|
51
|
+
import { negotiateSize, sanitizeCap, type SizeCap } from "./sizeNego.ts";
|
|
51
52
|
import { listStrayServeProcesses } from "./strayServe.ts";
|
|
52
53
|
import {
|
|
53
54
|
getProvisionHook,
|
|
@@ -235,10 +236,19 @@ const SESSION_PIN_ENV = new Set([
|
|
|
235
236
|
// ~/.bun/bin, ~/.cargo/bin, Homebrew, nvm shims, etc. A console-spawned agent
|
|
236
237
|
// that inherits that env can't find `bun`/`bunx`/etc. (the user reported "bunx:
|
|
237
238
|
// command not found" when spawning a new agent from the web UI). We recover the
|
|
238
|
-
// real environment by running the user's
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
239
|
+
// real environment by running the user's LOGIN shell and dumping its env.
|
|
240
|
+
//
|
|
241
|
+
// IMPORTANT: NON-interactive (`-lc`, not `-ilc`). An interactive shell (`-i`)
|
|
242
|
+
// HANGS indefinitely when the daemon has no controlling TTY — exactly the case
|
|
243
|
+
// under any process manager (oxmgr/systemd/launchd/pm2) — and Bun's `spawnSync`
|
|
244
|
+
// `timeout` does NOT kill the hung interactive shell. Because this runs
|
|
245
|
+
// synchronously on the event loop from the /api/spawn handler, the interactive
|
|
246
|
+
// variant PERMANENTLY WEDGED the daemon on the first console spawn (heartbeat
|
|
247
|
+
// froze → healthcheck restart → cache lost → next spawn wedged again), making
|
|
248
|
+
// console spawns impossible on a headless daemon. `-l` still sources the profile
|
|
249
|
+
// chain (`~/.profile`/`~/.bash_profile` → `~/.bashrc`), so the tool PATHs are
|
|
250
|
+
// still recovered. Returns null on Windows (no rc-file model — the process env is
|
|
251
|
+
// already the right one) or on any failure, so callers fall back to process.env.
|
|
242
252
|
let loginShellEnvCache: Record<string, string> | null | undefined;
|
|
243
253
|
function loginShellEnv(): Record<string, string> | null {
|
|
244
254
|
if (loginShellEnvCache !== undefined) return loginShellEnvCache;
|
|
@@ -250,7 +260,7 @@ function loginShellEnv(): Record<string, string> | null {
|
|
|
250
260
|
// print to stdout; `env -0` is NUL-separated so values with newlines survive.
|
|
251
261
|
const delim = "_AY_SHELL_ENV_DELIM_";
|
|
252
262
|
const res = Bun.spawnSync(
|
|
253
|
-
[shell, "-
|
|
263
|
+
[shell, "-lc", `printf %s "${delim}"; env -0; printf %s "${delim}"`],
|
|
254
264
|
{
|
|
255
265
|
stdin: "ignore",
|
|
256
266
|
stderr: "ignore",
|
|
@@ -1850,10 +1860,87 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1850
1860
|
// never a security surface — viewers self-report. Pruned by TTL on read.
|
|
1851
1861
|
const presence = new Map<
|
|
1852
1862
|
string,
|
|
1853
|
-
{
|
|
1863
|
+
{
|
|
1864
|
+
viewer: string;
|
|
1865
|
+
agent: string;
|
|
1866
|
+
cols: number;
|
|
1867
|
+
rows: number;
|
|
1868
|
+
sel: string | null;
|
|
1869
|
+
cap: SizeCap | null;
|
|
1870
|
+
ts: number;
|
|
1871
|
+
}
|
|
1854
1872
|
>();
|
|
1855
1873
|
const PRESENCE_TTL_MS = 12_000;
|
|
1856
1874
|
|
|
1875
|
+
// ── PTY size negotiation — tmux's "smallest client wins" ──────────────────
|
|
1876
|
+
// Writable viewers report their readable capacity (`cap`) alongside their
|
|
1877
|
+
// presence; we resize the agent's PTY to the elementwise min across live caps
|
|
1878
|
+
// (see sizeNego.ts), so a phone gets a grid it can read while wider viewers
|
|
1879
|
+
// letterbox the shared canvas. Uses the same winsize-file + SIGWINCH path as
|
|
1880
|
+
// POST /api/resize. When the last cap leaves (viewer switched agent, tab
|
|
1881
|
+
// died → TTL), the size is withdrawn: the file is deleted ONLY if its content
|
|
1882
|
+
// is still our own last write (so `ay attach`'s pushed size is never
|
|
1883
|
+
// clobbered) and the wrapper falls back to the real tty size.
|
|
1884
|
+
const negoApplied = new Map<number, { cols: number; rows: number; content: string }>();
|
|
1885
|
+
const negoTimers = new Map<number, ReturnType<typeof setTimeout>>();
|
|
1886
|
+
const NEGO_DEBOUNCE_MS = 350;
|
|
1887
|
+
const winsizePathFor = (pid: number) =>
|
|
1888
|
+
path.join(process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes"), "winsize", String(pid));
|
|
1889
|
+
const liveCapsFor = (pid: number): SizeCap[] => {
|
|
1890
|
+
const now = Date.now();
|
|
1891
|
+
const caps: SizeCap[] = [];
|
|
1892
|
+
for (const v of presence.values())
|
|
1893
|
+
if (now - v.ts <= PRESENCE_TTL_MS && String(v.agent) === String(pid) && v.cap)
|
|
1894
|
+
caps.push(v.cap);
|
|
1895
|
+
return caps;
|
|
1896
|
+
};
|
|
1897
|
+
const applyNego = async (pid: number) => {
|
|
1898
|
+
negoTimers.delete(pid);
|
|
1899
|
+
const eff = negotiateSize(liveCapsFor(pid));
|
|
1900
|
+
const file = winsizePathFor(pid);
|
|
1901
|
+
const prev = negoApplied.get(pid);
|
|
1902
|
+
try {
|
|
1903
|
+
if (eff) {
|
|
1904
|
+
if (prev && prev.cols === eff.cols && prev.rows === eff.rows) return;
|
|
1905
|
+
const content = `${eff.cols} ${eff.rows} ${Date.now()}\n`;
|
|
1906
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
1907
|
+
await writeFile(file, content);
|
|
1908
|
+
negoApplied.set(pid, { ...eff, content });
|
|
1909
|
+
} else {
|
|
1910
|
+
if (!prev) return;
|
|
1911
|
+
negoApplied.delete(pid);
|
|
1912
|
+
try {
|
|
1913
|
+
// withdraw only what we wrote — a different content means ay attach
|
|
1914
|
+
// (or a direct /api/resize) owns the size now; leave it alone.
|
|
1915
|
+
if ((await readFile(file, "utf-8")) !== prev.content) return;
|
|
1916
|
+
await unlink(file);
|
|
1917
|
+
} catch {
|
|
1918
|
+
return; // already gone — nothing to signal
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
try {
|
|
1922
|
+
process.kill(pid, "SIGWINCH");
|
|
1923
|
+
} catch {
|
|
1924
|
+
/* agent gone */
|
|
1925
|
+
}
|
|
1926
|
+
} catch {
|
|
1927
|
+
/* best-effort: a failed write just leaves the previous size in place */
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
const scheduleNego = (pid: number) => {
|
|
1931
|
+
if (!Number.isFinite(pid) || pid <= 0 || negoTimers.has(pid)) return;
|
|
1932
|
+
negoTimers.set(
|
|
1933
|
+
pid,
|
|
1934
|
+
setTimeout(() => void applyNego(pid), NEGO_DEBOUNCE_MS),
|
|
1935
|
+
);
|
|
1936
|
+
};
|
|
1937
|
+
// TTL grow-back sweep: a viewer that vanished without clearing its presence
|
|
1938
|
+
// (tab killed, phone locked) stops refreshing; once its entry expires the
|
|
1939
|
+
// negotiated size must be recomputed even though no presence POST arrives.
|
|
1940
|
+
setInterval(() => {
|
|
1941
|
+
for (const pid of negoApplied.keys()) scheduleNego(pid);
|
|
1942
|
+
}, 5_000);
|
|
1943
|
+
|
|
1857
1944
|
// The whole API as a plain handler: served over HTTP by Bun.serve (--http)
|
|
1858
1945
|
// and called in-process by the WebRTC bridge (--webrtc) — the latter needs
|
|
1859
1946
|
// no TCP port at all.
|
|
@@ -2330,6 +2417,10 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
2330
2417
|
pid: record.pid,
|
|
2331
2418
|
cols: size?.cols ?? null,
|
|
2332
2419
|
rows: size?.rows ?? null,
|
|
2420
|
+
// This host negotiates the PTY size from viewer capacities (presence
|
|
2421
|
+
// `cap`) — a capable console should report its cap instead of pushing
|
|
2422
|
+
// /api/resize directly, so viewers stop fighting last-writer-wins.
|
|
2423
|
+
nego: true,
|
|
2333
2424
|
});
|
|
2334
2425
|
} catch (e) {
|
|
2335
2426
|
return new Response((e as Error).message, { status: 404 });
|
|
@@ -2735,6 +2826,7 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
2735
2826
|
cols?: number;
|
|
2736
2827
|
rows?: number;
|
|
2737
2828
|
sel?: string;
|
|
2829
|
+
cap?: { cols?: number; rows?: number };
|
|
2738
2830
|
};
|
|
2739
2831
|
try {
|
|
2740
2832
|
b = (await req.json()) as typeof b;
|
|
@@ -2743,6 +2835,9 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
2743
2835
|
}
|
|
2744
2836
|
const viewer = String(b.viewer ?? "").slice(0, 64);
|
|
2745
2837
|
if (!viewer) return new Response("missing viewer", { status: 400 });
|
|
2838
|
+
// The agent this viewer WAS on — if the entry moves (agent switch) or
|
|
2839
|
+
// clears, that agent's negotiated size must be recomputed (grow-back).
|
|
2840
|
+
const prevAgent = presence.get(viewer)?.agent;
|
|
2746
2841
|
if (b.agent == null) presence.delete(viewer);
|
|
2747
2842
|
else
|
|
2748
2843
|
presence.set(viewer, {
|
|
@@ -2751,8 +2846,12 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
2751
2846
|
cols: Math.max(0, Math.floor(Number(b.cols) || 0)),
|
|
2752
2847
|
rows: Math.max(0, Math.floor(Number(b.rows) || 0)),
|
|
2753
2848
|
sel: typeof b.sel === "string" ? b.sel.slice(0, 200) : null,
|
|
2849
|
+
cap: sanitizeCap(b.cap),
|
|
2754
2850
|
ts: Date.now(),
|
|
2755
2851
|
});
|
|
2852
|
+
if (b.agent != null) scheduleNego(Number(b.agent));
|
|
2853
|
+
if (prevAgent != null && String(prevAgent) !== String(b.agent ?? ""))
|
|
2854
|
+
scheduleNego(Number(prevAgent));
|
|
2756
2855
|
return new Response(null, { status: 204 });
|
|
2757
2856
|
}
|
|
2758
2857
|
// GET /api/presence — all live viewers (TTL-pruned), for "who's watching".
|
package/ts/serveLock.spec.ts
CHANGED
|
@@ -140,7 +140,8 @@ describe("acquireWebrtcHostLock — held-lock lifecycle", () => {
|
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
const ownerFile = () => path.join(home, "webrtc-host.lock", "owner.json");
|
|
143
|
-
const readOwnerFile = async () =>
|
|
143
|
+
const readOwnerFile = async () =>
|
|
144
|
+
JSON.parse(await readFile(ownerFile(), "utf-8")) as ServeLockOwner;
|
|
144
145
|
|
|
145
146
|
it("heartbeats refresh beat_at while held", async () => {
|
|
146
147
|
const got = await acquireWebrtcHostLock({ graceMs: 0, beatMs: 40 });
|
|
@@ -190,19 +191,19 @@ describe("acquireWebrtcHostLock — held-lock lifecycle", () => {
|
|
|
190
191
|
it.skipIf(process.platform === "win32")(
|
|
191
192
|
"escalates to SIGKILL when the owner ignores SIGTERM",
|
|
192
193
|
async () => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
194
|
+
const { spawn } = await import("node:child_process");
|
|
195
|
+
const stubborn = spawn("/bin/sh", ["-c", "trap '' TERM; sleep 30"], { stdio: "ignore" });
|
|
196
|
+
const pid = stubborn.pid!;
|
|
197
|
+
await new Promise((r) => setTimeout(r, 100)); // let the trap install
|
|
198
|
+
const { mkdir: mkd, writeFile: wf } = await import("fs/promises");
|
|
199
|
+
await mkd(path.join(home, "webrtc-host.lock"), { recursive: true });
|
|
200
|
+
await wf(ownerFile(), JSON.stringify({ pid, started_at: Date.now(), beat_at: Date.now() }));
|
|
201
|
+
|
|
202
|
+
const got = await acquireWebrtcHostLock({ takeover: true, graceMs: 0, takeoverWaitMs: 200 });
|
|
203
|
+
expect(got.ok).toBe(true);
|
|
204
|
+
if (got.ok) releases.push(got.release);
|
|
205
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
206
|
+
expect(stubborn.signalCode).toBe("SIGKILL");
|
|
206
207
|
},
|
|
207
208
|
);
|
|
208
209
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
NEGO_FLOOR_COLS,
|
|
4
|
+
NEGO_FLOOR_ROWS,
|
|
5
|
+
negotiateSize,
|
|
6
|
+
sanitizeCap,
|
|
7
|
+
} from "./sizeNego.ts";
|
|
8
|
+
|
|
9
|
+
describe("sanitizeCap", () => {
|
|
10
|
+
it("accepts a normal viewer capacity", () => {
|
|
11
|
+
expect(sanitizeCap({ cols: 80, rows: 24 })).toEqual({ cols: 80, rows: 24 });
|
|
12
|
+
});
|
|
13
|
+
it("floors fractional values", () => {
|
|
14
|
+
expect(sanitizeCap({ cols: 80.9, rows: 24.7 })).toEqual({ cols: 80, rows: 24 });
|
|
15
|
+
});
|
|
16
|
+
it("rejects null / non-object / missing fields", () => {
|
|
17
|
+
expect(sanitizeCap(null)).toBeNull();
|
|
18
|
+
expect(sanitizeCap(undefined)).toBeNull();
|
|
19
|
+
expect(sanitizeCap("80x24")).toBeNull();
|
|
20
|
+
expect(sanitizeCap({})).toBeNull();
|
|
21
|
+
expect(sanitizeCap({ cols: 80 })).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
it("rejects out-of-range junk (mid-layout 0x0, absurd sizes)", () => {
|
|
24
|
+
expect(sanitizeCap({ cols: 0, rows: 0 })).toBeNull();
|
|
25
|
+
expect(sanitizeCap({ cols: 10, rows: 24 })).toBeNull(); // below min cols
|
|
26
|
+
expect(sanitizeCap({ cols: 80, rows: 2 })).toBeNull(); // below min rows
|
|
27
|
+
expect(sanitizeCap({ cols: 9999, rows: 24 })).toBeNull();
|
|
28
|
+
expect(sanitizeCap({ cols: 80, rows: 9999 })).toBeNull();
|
|
29
|
+
expect(sanitizeCap({ cols: NaN, rows: 24 })).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("negotiateSize", () => {
|
|
34
|
+
it("returns null with no caps (withdraw → tty size rules)", () => {
|
|
35
|
+
expect(negotiateSize([])).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
it("single viewer: adopts its capacity", () => {
|
|
38
|
+
expect(negotiateSize([{ cols: 133, rows: 40 }])).toEqual({ cols: 133, rows: 40 });
|
|
39
|
+
});
|
|
40
|
+
it("phone + desktop: elementwise min (the tmux rule)", () => {
|
|
41
|
+
// phone is narrow but tall, desktop is wide but short → min of each axis
|
|
42
|
+
const phone = { cols: 51, rows: 60 };
|
|
43
|
+
const desktop = { cols: 200, rows: 50 };
|
|
44
|
+
expect(negotiateSize([phone, desktop])).toEqual({ cols: 51, rows: 50 });
|
|
45
|
+
});
|
|
46
|
+
it("clamps to the floor so a tiny viewer can't wedge the agent", () => {
|
|
47
|
+
expect(negotiateSize([{ cols: 20, rows: 5 }])).toEqual({
|
|
48
|
+
cols: NEGO_FLOOR_COLS,
|
|
49
|
+
rows: NEGO_FLOOR_ROWS,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
it("three viewers: min wins per axis", () => {
|
|
53
|
+
expect(
|
|
54
|
+
negotiateSize([
|
|
55
|
+
{ cols: 100, rows: 30 },
|
|
56
|
+
{ cols: 80, rows: 50 },
|
|
57
|
+
{ cols: 120, rows: 24 },
|
|
58
|
+
]),
|
|
59
|
+
).toEqual({ cols: 80, rows: 24 });
|
|
60
|
+
});
|
|
61
|
+
});
|
package/ts/sizeNego.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-viewer PTY size negotiation — the tmux "smallest client wins" rule.
|
|
3
|
+
*
|
|
4
|
+
* Every writable web viewer reports its readable capacity (the cols×rows its
|
|
5
|
+
* pane can render at its chosen font size) via /api/presence. The host resizes
|
|
6
|
+
* the agent's PTY to the elementwise minimum across live viewers, so the
|
|
7
|
+
* narrowest screen (a phone) gets a grid it can actually read while wider
|
|
8
|
+
* viewers simply render fewer columns. When the last capacity-reporting viewer
|
|
9
|
+
* leaves, the negotiated size is withdrawn and the PTY falls back to the real
|
|
10
|
+
* terminal's size (the wrapper re-reads the tty once the winsize file is gone).
|
|
11
|
+
*
|
|
12
|
+
* Pure logic only — the serve layer owns presence bookkeeping, the winsize
|
|
13
|
+
* file, and SIGWINCH delivery.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface SizeCap {
|
|
17
|
+
cols: number;
|
|
18
|
+
rows: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Reject junk caps (a viewer mid-layout can report 0×0 or absurd numbers). */
|
|
22
|
+
const CAP_MIN_COLS = 20;
|
|
23
|
+
const CAP_MIN_ROWS = 5;
|
|
24
|
+
const CAP_MAX_COLS = 500;
|
|
25
|
+
const CAP_MAX_ROWS = 200;
|
|
26
|
+
|
|
27
|
+
/** Never negotiate below this — TUIs (claude, codex) break down when the grid
|
|
28
|
+
* gets absurdly narrow, so a tiny watch-sized viewer can't wedge the agent. */
|
|
29
|
+
export const NEGO_FLOOR_COLS = 40;
|
|
30
|
+
export const NEGO_FLOOR_ROWS = 10;
|
|
31
|
+
|
|
32
|
+
/** Parse a presence-reported cap into a sane SizeCap, or null if unusable. */
|
|
33
|
+
export function sanitizeCap(cap: unknown): SizeCap | null {
|
|
34
|
+
if (typeof cap !== "object" || cap === null) return null;
|
|
35
|
+
const c = Math.floor(Number((cap as SizeCap).cols) || 0);
|
|
36
|
+
const r = Math.floor(Number((cap as SizeCap).rows) || 0);
|
|
37
|
+
if (c < CAP_MIN_COLS || c > CAP_MAX_COLS) return null;
|
|
38
|
+
if (r < CAP_MIN_ROWS || r > CAP_MAX_ROWS) return null;
|
|
39
|
+
return { cols: c, rows: r };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Elementwise minimum over the live viewers' capacities, clamped to the floor.
|
|
44
|
+
* Returns null when no viewer reports a capacity — meaning "withdraw the
|
|
45
|
+
* negotiated size, let the real tty size rule again".
|
|
46
|
+
*/
|
|
47
|
+
export function negotiateSize(caps: ReadonlyArray<SizeCap>): SizeCap | null {
|
|
48
|
+
let cols = Infinity;
|
|
49
|
+
let rows = Infinity;
|
|
50
|
+
for (const cap of caps) {
|
|
51
|
+
if (cap.cols < cols) cols = cap.cols;
|
|
52
|
+
if (cap.rows < rows) rows = cap.rows;
|
|
53
|
+
}
|
|
54
|
+
if (!Number.isFinite(cols) || !Number.isFinite(rows)) return null;
|
|
55
|
+
return {
|
|
56
|
+
cols: Math.max(NEGO_FLOOR_COLS, cols),
|
|
57
|
+
rows: Math.max(NEGO_FLOOR_ROWS, rows),
|
|
58
|
+
};
|
|
59
|
+
}
|
package/ts/ws.spec.ts
CHANGED
|
@@ -468,16 +468,14 @@ describe("collectWorkspaces / workspaceStatus (mocked provision)", () => {
|
|
|
468
468
|
beforeEach(() => {
|
|
469
469
|
root = realpathSync(mkdtempSync(path.join(os.tmpdir(), "ay-ws-api-")));
|
|
470
470
|
prov.wsRoot = root;
|
|
471
|
-
prov.readStatus
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
hasUpstream: true,
|
|
480
|
-
});
|
|
471
|
+
prov.readStatus.mockReset().mockResolvedValue({
|
|
472
|
+
branch: "main",
|
|
473
|
+
head: "abc123",
|
|
474
|
+
ahead: 1,
|
|
475
|
+
behind: 0,
|
|
476
|
+
dirty: true,
|
|
477
|
+
hasUpstream: true,
|
|
478
|
+
});
|
|
481
479
|
homeBackup = process.env.AGENT_YES_HOME;
|
|
482
480
|
process.env.AGENT_YES_HOME = path.join(root, ".ay-home");
|
|
483
481
|
});
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import "./ts-DnqRreF4.js";
|
|
2
|
-
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-d5c6_vgS.js";
|
|
4
|
-
import "./pidStore-BIvsBQ8X.js";
|
|
5
|
-
import "./globalPidIndex-CoNr7tS8.js";
|
|
6
|
-
import "./messageLog-CxrKJj77.js";
|
|
7
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DQSgURGW.js";
|
|
8
|
-
|
|
9
|
-
export { SUPPORTED_CLIS };
|