agent-yes 1.206.0 → 1.208.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-C38pi6qs.js} +2 -2
- package/dist/SUPPORTED_CLIS-DTqtsVC5.js +9 -0
- package/dist/{agentShare-k8224qiZ.js → agentShare-sjB7JIAq.js} +2 -2
- package/dist/cli.js +4 -4
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-BbVnNvlo.js → notifyDaemon-CyEnGsPI.js} +2 -2
- package/dist/{rustBinary-BjRvvEs8.js → rustBinary-Cy1JDX9Q.js} +2 -2
- package/dist/{schedule-DJ-v4yVa.js → schedule-Co1vB9EZ.js} +4 -4
- package/dist/{serve-crLypY_c.js → serve-CMUfHfqa.js} +108 -14
- package/dist/{setup-BPNkeSmY.js → setup-aMt_-16G.js} +2 -2
- package/dist/{subcommands-DCGZjr4t.js → subcommands-DVHfue6K.js} +1 -1
- package/dist/{subcommands-DkrMRVNh.js → subcommands-Dvg-RHy6.js} +7 -7
- package/dist/{ts-DnqRreF4.js → ts-_opGnkmz.js} +2 -2
- package/dist/{versionChecker-d5c6_vgS.js → versionChecker-BSmUzVSM.js} +2 -2
- package/dist/{ws-BmHpWaG_.js → ws-CKoSZ9CN.js} +2 -2
- package/lab/ui/console-logic.js +20 -0
- package/lab/ui/index.html +190 -76
- package/package.json +1 -1
- package/ts/serve.ts +109 -6
- package/ts/serveLock.spec.ts +15 -14
- package/ts/sizeNego.spec.ts +56 -0
- package/ts/sizeNego.ts +59 -0
- package/ts/ws.spec.ts +8 -10
- package/dist/SUPPORTED_CLIS-CQ6e94ln.js +0 -9
package/lab/ui/index.html
CHANGED
|
@@ -695,14 +695,14 @@
|
|
|
695
695
|
align-items: center;
|
|
696
696
|
gap: 8px;
|
|
697
697
|
}
|
|
698
|
-
/*
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
698
|
+
/* The single consolidated fleet summary: "N/M agents · R rooms · 👤 P".
|
|
699
|
+
Rooms/peers segments appear only when there's more than one room / any
|
|
700
|
+
peer, so a solo fleet just reads "N/M agents". Turns yellow ("here") when
|
|
701
|
+
another peer is watching the agent you have open. */
|
|
702
|
+
.count {
|
|
703
703
|
cursor: default;
|
|
704
704
|
}
|
|
705
|
-
.
|
|
705
|
+
.count.here {
|
|
706
706
|
color: var(--peer);
|
|
707
707
|
}
|
|
708
708
|
.connbadge {
|
|
@@ -1851,20 +1851,11 @@
|
|
|
1851
1851
|
</div>
|
|
1852
1852
|
<div class="meta">
|
|
1853
1853
|
<span class="metaleft">
|
|
1854
|
-
<span id="count"></span>
|
|
1855
|
-
<span id="peers" class="peers" hidden></span>
|
|
1854
|
+
<span id="count" class="count"></span>
|
|
1856
1855
|
</span>
|
|
1857
1856
|
<span class="metaright">
|
|
1858
1857
|
<button id="foldbtn" class="viewbtn" title="fold subagent trees">⊞ subs</button>
|
|
1859
1858
|
<button id="sortbtn" class="viewbtn" title="cycle sort order">⇅ state</button>
|
|
1860
|
-
<button
|
|
1861
|
-
id="viewbtn"
|
|
1862
|
-
class="viewbtn"
|
|
1863
|
-
title="toggle compact list"
|
|
1864
|
-
aria-label="Toggle compact list view"
|
|
1865
|
-
>
|
|
1866
|
-
☰
|
|
1867
|
-
</button>
|
|
1868
1859
|
<button id="portsbtn" class="viewbtn" title="manage exposed localhost ports">
|
|
1869
1860
|
⇄ ports
|
|
1870
1861
|
</button>
|
|
@@ -2173,7 +2164,7 @@
|
|
|
2173
2164
|
selFromBottom,
|
|
2174
2165
|
parseSel,
|
|
2175
2166
|
selSegments,
|
|
2176
|
-
|
|
2167
|
+
fitTransformCentered,
|
|
2177
2168
|
docTitle,
|
|
2178
2169
|
omniScore,
|
|
2179
2170
|
} from "./console-logic.js";
|
|
@@ -2192,6 +2183,7 @@
|
|
|
2192
2183
|
// row keeps pulsing to. See advanceStdinFlashes/focusSummary (console-logic).
|
|
2193
2184
|
let focusByKey = new Map();
|
|
2194
2185
|
let peerTotal = 0;
|
|
2186
|
+
let shownAgentCount = 0; // agents matching the current filter (for the summary badge)
|
|
2195
2187
|
const stdinSeen = new Map();
|
|
2196
2188
|
const flashUntil = new Map();
|
|
2197
2189
|
const STDIN_FLASH_MS = 1200;
|
|
@@ -2230,7 +2222,15 @@
|
|
|
2230
2222
|
} catch {}
|
|
2231
2223
|
if (term) {
|
|
2232
2224
|
term.options.fontSize = termFontSize;
|
|
2233
|
-
|
|
2225
|
+
// Negotiating host: the grid is canonical (host-owned) — a font change
|
|
2226
|
+
// here only alters our glyph px, so re-letterbox and report the new
|
|
2227
|
+
// capacity; the host reflows the PTY if our readable cols/rows moved.
|
|
2228
|
+
// This is what makes font size a ZOOM control on mobile: bigger font →
|
|
2229
|
+
// fewer readable cells → the PTY itself shrinks → bigger crisp text.
|
|
2230
|
+
if (negoActive) {
|
|
2231
|
+
applyCanvasScale();
|
|
2232
|
+
sendPresence();
|
|
2233
|
+
} else if (fit)
|
|
2234
2234
|
try {
|
|
2235
2235
|
fit.fit();
|
|
2236
2236
|
} catch {}
|
|
@@ -3132,6 +3132,13 @@
|
|
|
3132
3132
|
// short lease after that, the heartbeat won't follow/override — so our own
|
|
3133
3133
|
// push isn't fought while the agent's /api/size is still catching up.
|
|
3134
3134
|
let lastDroveAt = 0;
|
|
3135
|
+
// The selected agent's host negotiates the PTY size from viewer
|
|
3136
|
+
// capacities (its /api/size responses carry nego:true). On such hosts we
|
|
3137
|
+
// never push /api/resize — we report our readable capacity in presence
|
|
3138
|
+
// (sendPresence cap) and follow the negotiated canonical grid, so a phone
|
|
3139
|
+
// and a desktop watching the same agent converge on the smallest
|
|
3140
|
+
// viewer's readable size instead of fighting last-writer-wins.
|
|
3141
|
+
let negoActive = false;
|
|
3135
3142
|
|
|
3136
3143
|
function isDarkColor(c) {
|
|
3137
3144
|
try {
|
|
@@ -3277,6 +3284,18 @@
|
|
|
3277
3284
|
const s = term.getSelectionPosition && term.getSelectionPosition();
|
|
3278
3285
|
selR = selFromBottom(s, term.buffer.active.length);
|
|
3279
3286
|
} catch {}
|
|
3287
|
+
// Our readable capacity: the grid our pane fits at the current font —
|
|
3288
|
+
// measured from the container (transform-independent), NOT term.cols
|
|
3289
|
+
// (which may be a foreign canonical grid we're letterboxing). The host
|
|
3290
|
+
// negotiates the PTY to the min cap across viewers; read-only viewers
|
|
3291
|
+
// report none (they can't influence the PTY). Older hosts ignore it.
|
|
3292
|
+
let cap = null;
|
|
3293
|
+
try {
|
|
3294
|
+
if (!isReadonlyEnt(cur.e) && fit) {
|
|
3295
|
+
const d = fit.proposeDimensions();
|
|
3296
|
+
if (d && d.cols > 0 && d.rows > 0) cap = { cols: d.cols, rows: d.rows };
|
|
3297
|
+
}
|
|
3298
|
+
} catch {}
|
|
3280
3299
|
cur.tx
|
|
3281
3300
|
.post("/api/presence", {
|
|
3282
3301
|
viewer: myViewerId,
|
|
@@ -3284,6 +3303,7 @@
|
|
|
3284
3303
|
cols: term.cols,
|
|
3285
3304
|
rows: term.rows,
|
|
3286
3305
|
sel: selR,
|
|
3306
|
+
cap,
|
|
3287
3307
|
})
|
|
3288
3308
|
.catch(() => {});
|
|
3289
3309
|
}
|
|
@@ -3333,29 +3353,34 @@
|
|
|
3333
3353
|
const sum = focusSummary(recs);
|
|
3334
3354
|
focusByKey = sum.byKey;
|
|
3335
3355
|
peerTotal = sum.total;
|
|
3336
|
-
|
|
3356
|
+
updateSummary();
|
|
3337
3357
|
renderList();
|
|
3338
3358
|
}
|
|
3339
|
-
//
|
|
3340
|
-
// agent
|
|
3341
|
-
//
|
|
3342
|
-
|
|
3343
|
-
|
|
3359
|
+
// The ONE consolidated fleet summary in the header, replacing the separate
|
|
3360
|
+
// agent-count and "N peers" badges: "<shown>/<total> agents" plus, only when
|
|
3361
|
+
// relevant, "· R rooms" (multi-room fleets) and "· 👤 P" (other humans here).
|
|
3362
|
+
// Yellow ("here") when a peer shares the agent you have open; the tooltip
|
|
3363
|
+
// breaks the peer count down by agent so you can see who's where. Called both
|
|
3364
|
+
// from renderList (agent list changed) and on presence updates (peers).
|
|
3365
|
+
function updateSummary() {
|
|
3366
|
+
const el = $("count");
|
|
3344
3367
|
if (!el) return;
|
|
3345
|
-
|
|
3346
|
-
|
|
3368
|
+
const rooms = new Set(entries.filter((e) => e._room).map((e) => e._room)).size;
|
|
3369
|
+
let s = `${shownAgentCount} / ${entries.length} agents`;
|
|
3370
|
+
if (rooms > 1) s += ` · ${rooms} rooms`;
|
|
3371
|
+
if (peerTotal > 0) s += ` · 👤 ${peerTotal}`;
|
|
3372
|
+
el.textContent = s;
|
|
3373
|
+
el.classList.toggle("here", !!(sel && focusByKey.get(sel)));
|
|
3374
|
+
if (peerTotal > 0) {
|
|
3375
|
+
const lines = [...focusByKey.entries()].map(([k, n]) => {
|
|
3376
|
+
const e = entries.find((x) => x._key === k);
|
|
3377
|
+
const name = e ? e.title || cliLabel(e) || ident(e) || `pid ${e.pid}` : k;
|
|
3378
|
+
return ` ${n} on ${name}`;
|
|
3379
|
+
});
|
|
3380
|
+
el.title = "peers watching agents in this fleet:\n" + lines.join("\n");
|
|
3381
|
+
} else {
|
|
3347
3382
|
el.removeAttribute("title");
|
|
3348
|
-
return;
|
|
3349
3383
|
}
|
|
3350
|
-
el.hidden = false;
|
|
3351
|
-
el.textContent = `👤 ${peerTotal} peer${peerTotal === 1 ? "" : "s"}`;
|
|
3352
|
-
el.classList.toggle("here", !!(sel && focusByKey.get(sel)));
|
|
3353
|
-
const lines = [...focusByKey.entries()].map(([k, n]) => {
|
|
3354
|
-
const e = entries.find((x) => x._key === k);
|
|
3355
|
-
const name = e ? e.title || cliLabel(e) || ident(e) || `pid ${e.pid}` : k;
|
|
3356
|
-
return ` ${n} on ${name}`;
|
|
3357
|
-
});
|
|
3358
|
-
el.title = "peers watching agents in this fleet:\n" + lines.join("\n");
|
|
3359
3384
|
}
|
|
3360
3385
|
// 3s heartbeat (< the host's 12s TTL): refresh our presence + read others',
|
|
3361
3386
|
// and follow the canonical grid size (resize+scale, never reflow) so all
|
|
@@ -3600,14 +3625,36 @@
|
|
|
3600
3625
|
const cs = getComputedStyle(logEl);
|
|
3601
3626
|
const pw = logEl.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
|
|
3602
3627
|
const ph = logEl.clientHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom);
|
|
3603
|
-
//
|
|
3604
|
-
|
|
3628
|
+
// fitTransformCentered (console-logic.js, unit-tested) decides
|
|
3629
|
+
// none-vs-scale and centers the letterboxed grid in the pane (fit
|
|
3630
|
+
// width → vertically centered, fit height → horizontally). The peer
|
|
3631
|
+
// overlay reads the transformed screen rect back via
|
|
3632
|
+
// getBoundingClientRect, so the translate needs no extra bookkeeping.
|
|
3633
|
+
xt.style.transform = fitTransformCentered(gw, gh, pw, ph).transform;
|
|
3605
3634
|
} catch {}
|
|
3606
3635
|
}
|
|
3636
|
+
// Pane geometry changed (window resize, splitter drag, soft keyboard, tab
|
|
3637
|
+
// refocus): on legacy hosts re-fit the grid to the pane (the size push
|
|
3638
|
+
// rides term.onResize → pushSize); on negotiating hosts the grid is
|
|
3639
|
+
// canonical — never reflow it locally, just re-letterbox and report the
|
|
3640
|
+
// new capacity so the host can move the min if needed.
|
|
3641
|
+
function refitPane() {
|
|
3642
|
+
if (!term) return;
|
|
3643
|
+
if (negoActive) {
|
|
3644
|
+
applyCanvasScale();
|
|
3645
|
+
sendPresence();
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
if (fit)
|
|
3649
|
+
try {
|
|
3650
|
+
fit.fit();
|
|
3651
|
+
} catch {}
|
|
3652
|
+
}
|
|
3607
3653
|
// Follow the canonical grid: resize our xterm to the agent's size (set by the
|
|
3608
3654
|
// active driver / local owner) WITHOUT reflowing to our pane, then scale to
|
|
3609
3655
|
// fit. No-op when we already match (we're the driver, or sizes agree).
|
|
3610
3656
|
function followAgentSize(sz) {
|
|
3657
|
+
if (sz && sz.nego != null) negoActive = !!sz.nego;
|
|
3611
3658
|
if (!term || !sz || !sz.cols || !sz.rows) return;
|
|
3612
3659
|
// While our own recent push is still settling, don't fight it.
|
|
3613
3660
|
if (Date.now() - lastDroveAt < 6000) {
|
|
@@ -4654,8 +4701,9 @@
|
|
|
4654
4701
|
s.streaming = false;
|
|
4655
4702
|
}
|
|
4656
4703
|
|
|
4657
|
-
// Compact list
|
|
4658
|
-
|
|
4704
|
+
// Compact list is now the ONLY list view (one line per agent: dot + cli +
|
|
4705
|
+
// title). The toggle button was removed — compact is always on.
|
|
4706
|
+
const compactList = true;
|
|
4659
4707
|
|
|
4660
4708
|
// Fold subagent trees: hide nested (sub)agent rows and roll each root's
|
|
4661
4709
|
// hidden descendants into a summary chip. Folded BY DEFAULT (only "0" opts
|
|
@@ -4844,11 +4892,10 @@
|
|
|
4844
4892
|
const rows = foldSubs
|
|
4845
4893
|
? fullRows.filter((r) => !(r.kind === "agent" && r.parentEntry))
|
|
4846
4894
|
: fullRows;
|
|
4847
|
-
|
|
4848
|
-
|
|
4895
|
+
shownAgentCount = agentRows.length;
|
|
4896
|
+
updateSummary(); // one consolidated "agents · rooms · peers" badge
|
|
4849
4897
|
$("foldbtn").textContent = foldSubs ? "⊞ subs" : "⊟ subs";
|
|
4850
4898
|
$("foldbtn").classList.toggle("on", !foldSubs);
|
|
4851
|
-
$("viewbtn").classList.toggle("on", compactList);
|
|
4852
4899
|
// Show the active sort mode on the button so it's self-documenting; the
|
|
4853
4900
|
// default ("state") is also highlighted to hint it's the implicit order.
|
|
4854
4901
|
$("sortbtn").textContent = `⇅ ${sortMode}`;
|
|
@@ -4985,25 +5032,22 @@
|
|
|
4985
5032
|
// the new mtime, so a silent `ay send` still trips the flash within ~1s.
|
|
4986
5033
|
const until = Date.now() + STDIN_FLASH_MS;
|
|
4987
5034
|
for (const k of advanceStdinFlashes(entries, stdinSeen)) flashUntil.set(k, until);
|
|
4988
|
-
//
|
|
4989
|
-
//
|
|
4990
|
-
//
|
|
4991
|
-
//
|
|
5035
|
+
// Connection-status badge ONLY (dot + state). The agent/room/peer COUNTS
|
|
5036
|
+
// live in one place now — the #count summary on the left — so this badge no
|
|
5037
|
+
// longer repeats them; it just shows liveness and stays the rooms-manager
|
|
5038
|
+
// click target (see its title). Red only when nothing is reachable.
|
|
4992
5039
|
const roomSrcs = srcs.filter((s) => s.id !== LOCAL);
|
|
4993
|
-
const liveRooms = new Set(roomSrcs.filter((s) => s.live).map((s) => s.room || s.id)).size;
|
|
4994
5040
|
const anyLive = srcs.some((s) => s.live);
|
|
4995
5041
|
const connecting =
|
|
4996
5042
|
roomSrcs.some((s) => !s.tried) || [...chRooms.values()].some((r) => !r.tried);
|
|
4997
5043
|
const roomCount = roomSrcs.length || chRooms.size;
|
|
4998
|
-
const n = entries.length;
|
|
4999
5044
|
if (!srcs.length && !chRooms.size) {
|
|
5000
5045
|
setConn("● no fleet", "var(--muted)");
|
|
5001
5046
|
} else if (!anyLive) {
|
|
5002
5047
|
if (connecting) setConn("● connecting…", "var(--amber)");
|
|
5003
5048
|
else setConn(roomCount ? "● rooms offline" : "● ay serve down", "var(--red)");
|
|
5004
5049
|
} else {
|
|
5005
|
-
|
|
5006
|
-
setConn(`● ${n} agent${n === 1 ? "" : "s"}${roomBit}`, "var(--green)");
|
|
5050
|
+
setConn("● live", "var(--green)");
|
|
5007
5051
|
}
|
|
5008
5052
|
renderRoomsIfOpen();
|
|
5009
5053
|
renderList();
|
|
@@ -5187,6 +5231,14 @@
|
|
|
5187
5231
|
applyCanvasScale();
|
|
5188
5232
|
return;
|
|
5189
5233
|
}
|
|
5234
|
+
// Negotiating host: the PTY size is the host's min over viewer
|
|
5235
|
+
// capacities, not ours to push. Re-letterbox and refresh our cap
|
|
5236
|
+
// (the pane may have changed) — the host resizes if the min moved.
|
|
5237
|
+
if (negoActive) {
|
|
5238
|
+
applyCanvasScale();
|
|
5239
|
+
sendPresence();
|
|
5240
|
+
return;
|
|
5241
|
+
}
|
|
5190
5242
|
lastDroveAt = Date.now(); // we're driving the size now (lease the grid)
|
|
5191
5243
|
tx.post("/api/resize/" + encodeURIComponent(pid), {
|
|
5192
5244
|
cols: term.cols,
|
|
@@ -5227,6 +5279,7 @@
|
|
|
5227
5279
|
const selKey = e._key;
|
|
5228
5280
|
const fitAndSync = (sz) => {
|
|
5229
5281
|
if (sel !== selKey || !term) return;
|
|
5282
|
+
negoActive = !!(sz && sz.nego);
|
|
5230
5283
|
if (sz && sz.cols && sz.rows) agentSize = sz; // remember for the badge tooltip
|
|
5231
5284
|
// Read-only viewer: don't reflow the agent to our pane (we can't resize
|
|
5232
5285
|
// it, and shouldn't). Adopt the AGENT's own grid and CSS-scale it to fit,
|
|
@@ -5238,6 +5291,22 @@
|
|
|
5238
5291
|
suppressPush = false;
|
|
5239
5292
|
return;
|
|
5240
5293
|
}
|
|
5294
|
+
// Negotiating host: adopt the canonical (negotiated) grid like a
|
|
5295
|
+
// watcher and report our capacity via presence — the host takes the
|
|
5296
|
+
// min over all viewers and resizes the PTY itself. No direct push, so
|
|
5297
|
+
// two viewers can't fight over the size.
|
|
5298
|
+
if (negoActive) {
|
|
5299
|
+
if (sz.cols && sz.rows) followAgentSize(sz);
|
|
5300
|
+
else {
|
|
5301
|
+
// agent has no size on record yet — seed from our own fit
|
|
5302
|
+
try {
|
|
5303
|
+
fit.fit();
|
|
5304
|
+
} catch {}
|
|
5305
|
+
}
|
|
5306
|
+
suppressPush = false;
|
|
5307
|
+
sendPresence(); // cap → host negotiates → size event brings the grid
|
|
5308
|
+
return;
|
|
5309
|
+
}
|
|
5241
5310
|
try {
|
|
5242
5311
|
fit.fit();
|
|
5243
5312
|
} catch {}
|
|
@@ -5381,11 +5450,6 @@
|
|
|
5381
5450
|
} catch {}
|
|
5382
5451
|
renderList();
|
|
5383
5452
|
});
|
|
5384
|
-
$("viewbtn").addEventListener("click", () => {
|
|
5385
|
-
compactList = !compactList;
|
|
5386
|
-
localStorage.setItem("ay.compactList", compactList ? "1" : "0");
|
|
5387
|
-
renderList();
|
|
5388
|
-
});
|
|
5389
5453
|
// Fold / unfold every subagent tree (global toggle, persisted per device).
|
|
5390
5454
|
$("foldbtn").addEventListener("click", () => {
|
|
5391
5455
|
foldSubs = !foldSubs;
|
|
@@ -5413,13 +5477,9 @@
|
|
|
5413
5477
|
renderList();
|
|
5414
5478
|
});
|
|
5415
5479
|
window.addEventListener("resize", () => {
|
|
5416
|
-
// Resizing our window is strong intent → re-fit
|
|
5417
|
-
//
|
|
5418
|
-
|
|
5419
|
-
if (fit)
|
|
5420
|
-
try {
|
|
5421
|
-
fit.fit();
|
|
5422
|
-
} catch {}
|
|
5480
|
+
// Resizing our window is strong intent → re-fit / re-report capacity
|
|
5481
|
+
// (refitPane picks the legacy-push vs negotiate path).
|
|
5482
|
+
refitPane();
|
|
5423
5483
|
applyCanvasScale();
|
|
5424
5484
|
renderPeerSelections(); // cell size changed → reposition peer-selection overlays
|
|
5425
5485
|
});
|
|
@@ -5434,15 +5494,76 @@
|
|
|
5434
5494
|
const onVV = () => {
|
|
5435
5495
|
const kb = Math.max(0, Math.round(window.innerHeight - vv.height - vv.offsetTop));
|
|
5436
5496
|
document.documentElement.style.setProperty("--kb", kb + "px");
|
|
5437
|
-
|
|
5438
|
-
try {
|
|
5439
|
-
fit.fit();
|
|
5440
|
-
} catch {}
|
|
5497
|
+
refitPane();
|
|
5441
5498
|
};
|
|
5442
5499
|
vv.addEventListener("resize", onVV);
|
|
5443
5500
|
vv.addEventListener("scroll", onVV);
|
|
5444
5501
|
onVV();
|
|
5445
5502
|
}
|
|
5503
|
+
// ── mobile pinch on the terminal = terminal zoom, not page zoom ─────────
|
|
5504
|
+
// Two fingers over the log pane adjust the terminal FONT SIZE. On a
|
|
5505
|
+
// negotiating host that reflows the PTY itself (bigger font → fewer
|
|
5506
|
+
// readable cells → the host shrinks the grid → bigger crisp text; smaller
|
|
5507
|
+
// font → more rows/cols), which is what a phone user actually means by
|
|
5508
|
+
// pinch: "show me more / make it readable", with the surrounding UI
|
|
5509
|
+
// staying put. Only 2-touch gestures over .log are intercepted — 1-finger
|
|
5510
|
+
// scroll/selection passes to xterm, and the page keeps its native
|
|
5511
|
+
// pinch-zoom everywhere else (an a11y requirement, WCAG 1.4.4).
|
|
5512
|
+
(function () {
|
|
5513
|
+
const logEl = $("log");
|
|
5514
|
+
if (!logEl) return;
|
|
5515
|
+
let startDist = 0; // 0 = no pinch in flight
|
|
5516
|
+
let startFont = 0;
|
|
5517
|
+
let lastRatio = 1;
|
|
5518
|
+
let baseTransform = "";
|
|
5519
|
+
const dist = (t) => Math.hypot(t[0].clientX - t[1].clientX, t[0].clientY - t[1].clientY);
|
|
5520
|
+
logEl.addEventListener(
|
|
5521
|
+
"touchstart",
|
|
5522
|
+
(ev) => {
|
|
5523
|
+
if (ev.touches.length !== 2) return;
|
|
5524
|
+
ev.preventDefault(); // this gesture is ours — no page zoom
|
|
5525
|
+
startDist = dist(ev.touches);
|
|
5526
|
+
startFont = termFontSize;
|
|
5527
|
+
lastRatio = 1;
|
|
5528
|
+
const xt = logEl.querySelector(".xterm");
|
|
5529
|
+
baseTransform = xt ? xt.style.transform || "none" : "none";
|
|
5530
|
+
},
|
|
5531
|
+
{ passive: false },
|
|
5532
|
+
);
|
|
5533
|
+
logEl.addEventListener(
|
|
5534
|
+
"touchmove",
|
|
5535
|
+
(ev) => {
|
|
5536
|
+
if (ev.touches.length !== 2 || !startDist) return;
|
|
5537
|
+
ev.preventDefault();
|
|
5538
|
+
lastRatio = dist(ev.touches) / startDist;
|
|
5539
|
+
// Live preview: compose the pinch onto the fitted transform. The
|
|
5540
|
+
// grid truly reflows only when the negotiated size lands after the
|
|
5541
|
+
// gesture commits — a brief settle, traded for zero SIGWINCH storm
|
|
5542
|
+
// while the fingers are still moving.
|
|
5543
|
+
const xt = logEl.querySelector(".xterm");
|
|
5544
|
+
if (xt)
|
|
5545
|
+
xt.style.transform =
|
|
5546
|
+
`scale(${lastRatio.toFixed(3)})` +
|
|
5547
|
+
(baseTransform === "none" ? "" : " " + baseTransform);
|
|
5548
|
+
},
|
|
5549
|
+
{ passive: false },
|
|
5550
|
+
);
|
|
5551
|
+
const endPinch = () => {
|
|
5552
|
+
if (!startDist) return;
|
|
5553
|
+
const target = Math.round(startFont * lastRatio);
|
|
5554
|
+
startDist = 0;
|
|
5555
|
+
if (target !== termFontSize) setTermFontSize(target); // nego: cap → host reflows
|
|
5556
|
+
applyCanvasScale(); // clear the preview; re-letterbox at the real grid
|
|
5557
|
+
};
|
|
5558
|
+
logEl.addEventListener("touchend", (ev) => {
|
|
5559
|
+
if (ev.touches.length < 2) endPinch();
|
|
5560
|
+
});
|
|
5561
|
+
logEl.addEventListener("touchcancel", () => endPinch());
|
|
5562
|
+
// Safari's proprietary gesture events fire for pinch regardless of
|
|
5563
|
+
// touch-action; swallow them over the terminal so the page never zooms.
|
|
5564
|
+
for (const t of ["gesturestart", "gesturechange", "gestureend"])
|
|
5565
|
+
logEl.addEventListener(t, (ev) => ev.preventDefault());
|
|
5566
|
+
})();
|
|
5446
5567
|
|
|
5447
5568
|
// Step the selection up/down the (filtered) list — same order the left panel
|
|
5448
5569
|
// renders. Clamps at the ends, scrolls the row into view.
|
|
@@ -6983,11 +7104,7 @@
|
|
|
6983
7104
|
// terminal of a different size) that re-push fights the local terminal
|
|
6984
7105
|
// and reflows the stream on every tab switch — the glitch the eager
|
|
6985
7106
|
// per-focus resync introduced. The one-shot adopt still runs on select.
|
|
6986
|
-
|
|
6987
|
-
try {
|
|
6988
|
-
fit.fit();
|
|
6989
|
-
} catch {}
|
|
6990
|
-
}
|
|
7107
|
+
refitPane();
|
|
6991
7108
|
}
|
|
6992
7109
|
});
|
|
6993
7110
|
watchVersion();
|
|
@@ -7039,10 +7156,7 @@
|
|
|
7039
7156
|
if (!dragging) return;
|
|
7040
7157
|
const w = Math.min(Math.max(e.clientX, 280), window.innerWidth - 360);
|
|
7041
7158
|
app.style.setProperty("--leftw", w + "px");
|
|
7042
|
-
|
|
7043
|
-
try {
|
|
7044
|
-
fit.fit();
|
|
7045
|
-
} catch {}
|
|
7159
|
+
refitPane();
|
|
7046
7160
|
});
|
|
7047
7161
|
const end = (e) => {
|
|
7048
7162
|
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,91 @@ 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(
|
|
1889
|
+
process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes"),
|
|
1890
|
+
"winsize",
|
|
1891
|
+
String(pid),
|
|
1892
|
+
);
|
|
1893
|
+
const liveCapsFor = (pid: number): SizeCap[] => {
|
|
1894
|
+
const now = Date.now();
|
|
1895
|
+
const caps: SizeCap[] = [];
|
|
1896
|
+
for (const v of presence.values())
|
|
1897
|
+
if (now - v.ts <= PRESENCE_TTL_MS && String(v.agent) === String(pid) && v.cap)
|
|
1898
|
+
caps.push(v.cap);
|
|
1899
|
+
return caps;
|
|
1900
|
+
};
|
|
1901
|
+
const applyNego = async (pid: number) => {
|
|
1902
|
+
negoTimers.delete(pid);
|
|
1903
|
+
const eff = negotiateSize(liveCapsFor(pid));
|
|
1904
|
+
const file = winsizePathFor(pid);
|
|
1905
|
+
const prev = negoApplied.get(pid);
|
|
1906
|
+
try {
|
|
1907
|
+
if (eff) {
|
|
1908
|
+
if (prev && prev.cols === eff.cols && prev.rows === eff.rows) return;
|
|
1909
|
+
const content = `${eff.cols} ${eff.rows} ${Date.now()}\n`;
|
|
1910
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
1911
|
+
await writeFile(file, content);
|
|
1912
|
+
negoApplied.set(pid, { ...eff, content });
|
|
1913
|
+
} else {
|
|
1914
|
+
if (!prev) return;
|
|
1915
|
+
negoApplied.delete(pid);
|
|
1916
|
+
try {
|
|
1917
|
+
// withdraw only what we wrote — a different content means ay attach
|
|
1918
|
+
// (or a direct /api/resize) owns the size now; leave it alone.
|
|
1919
|
+
if ((await readFile(file, "utf-8")) !== prev.content) return;
|
|
1920
|
+
await unlink(file);
|
|
1921
|
+
} catch {
|
|
1922
|
+
return; // already gone — nothing to signal
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
try {
|
|
1926
|
+
process.kill(pid, "SIGWINCH");
|
|
1927
|
+
} catch {
|
|
1928
|
+
/* agent gone */
|
|
1929
|
+
}
|
|
1930
|
+
} catch {
|
|
1931
|
+
/* best-effort: a failed write just leaves the previous size in place */
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
const scheduleNego = (pid: number) => {
|
|
1935
|
+
if (!Number.isFinite(pid) || pid <= 0 || negoTimers.has(pid)) return;
|
|
1936
|
+
negoTimers.set(
|
|
1937
|
+
pid,
|
|
1938
|
+
setTimeout(() => void applyNego(pid), NEGO_DEBOUNCE_MS),
|
|
1939
|
+
);
|
|
1940
|
+
};
|
|
1941
|
+
// TTL grow-back sweep: a viewer that vanished without clearing its presence
|
|
1942
|
+
// (tab killed, phone locked) stops refreshing; once its entry expires the
|
|
1943
|
+
// negotiated size must be recomputed even though no presence POST arrives.
|
|
1944
|
+
setInterval(() => {
|
|
1945
|
+
for (const pid of negoApplied.keys()) scheduleNego(pid);
|
|
1946
|
+
}, 5_000);
|
|
1947
|
+
|
|
1857
1948
|
// The whole API as a plain handler: served over HTTP by Bun.serve (--http)
|
|
1858
1949
|
// and called in-process by the WebRTC bridge (--webrtc) — the latter needs
|
|
1859
1950
|
// no TCP port at all.
|
|
@@ -2330,6 +2421,10 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
2330
2421
|
pid: record.pid,
|
|
2331
2422
|
cols: size?.cols ?? null,
|
|
2332
2423
|
rows: size?.rows ?? null,
|
|
2424
|
+
// This host negotiates the PTY size from viewer capacities (presence
|
|
2425
|
+
// `cap`) — a capable console should report its cap instead of pushing
|
|
2426
|
+
// /api/resize directly, so viewers stop fighting last-writer-wins.
|
|
2427
|
+
nego: true,
|
|
2333
2428
|
});
|
|
2334
2429
|
} catch (e) {
|
|
2335
2430
|
return new Response((e as Error).message, { status: 404 });
|
|
@@ -2735,6 +2830,7 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
2735
2830
|
cols?: number;
|
|
2736
2831
|
rows?: number;
|
|
2737
2832
|
sel?: string;
|
|
2833
|
+
cap?: { cols?: number; rows?: number };
|
|
2738
2834
|
};
|
|
2739
2835
|
try {
|
|
2740
2836
|
b = (await req.json()) as typeof b;
|
|
@@ -2743,6 +2839,9 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
2743
2839
|
}
|
|
2744
2840
|
const viewer = String(b.viewer ?? "").slice(0, 64);
|
|
2745
2841
|
if (!viewer) return new Response("missing viewer", { status: 400 });
|
|
2842
|
+
// The agent this viewer WAS on — if the entry moves (agent switch) or
|
|
2843
|
+
// clears, that agent's negotiated size must be recomputed (grow-back).
|
|
2844
|
+
const prevAgent = presence.get(viewer)?.agent;
|
|
2746
2845
|
if (b.agent == null) presence.delete(viewer);
|
|
2747
2846
|
else
|
|
2748
2847
|
presence.set(viewer, {
|
|
@@ -2751,8 +2850,12 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
2751
2850
|
cols: Math.max(0, Math.floor(Number(b.cols) || 0)),
|
|
2752
2851
|
rows: Math.max(0, Math.floor(Number(b.rows) || 0)),
|
|
2753
2852
|
sel: typeof b.sel === "string" ? b.sel.slice(0, 200) : null,
|
|
2853
|
+
cap: sanitizeCap(b.cap),
|
|
2754
2854
|
ts: Date.now(),
|
|
2755
2855
|
});
|
|
2856
|
+
if (b.agent != null) scheduleNego(Number(b.agent));
|
|
2857
|
+
if (prevAgent != null && String(prevAgent) !== String(b.agent ?? ""))
|
|
2858
|
+
scheduleNego(Number(prevAgent));
|
|
2756
2859
|
return new Response(null, { status: 204 });
|
|
2757
2860
|
}
|
|
2758
2861
|
// GET /api/presence — all live viewers (TTL-pruned), for "who's watching".
|