agent-yes 1.98.0 → 1.100.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/lab/ui/index.html CHANGED
@@ -52,14 +52,34 @@
52
52
  flex children actually shrink — without it overflow:auto never engages). */
53
53
  .app {
54
54
  display: grid;
55
- grid-template-columns: minmax(360px, 42%) 1fr;
55
+ grid-template-columns: var(--leftw, 42%) 1px 1fr;
56
56
  grid-template-rows: minmax(0, 1fr);
57
57
  height: 100vh;
58
58
  }
59
59
 
60
+ /* VSCode-style splitter: the visible divider stays 1px, but a ::before
61
+ overlay widens the pointer hit area to ~11px so it's easy to grab. */
62
+ .splitter {
63
+ position: relative;
64
+ background: var(--line);
65
+ cursor: col-resize;
66
+ z-index: 5;
67
+ }
68
+ .splitter::before {
69
+ content: "";
70
+ position: absolute;
71
+ top: 0;
72
+ bottom: 0;
73
+ left: -5px;
74
+ right: -5px;
75
+ }
76
+ .splitter:hover,
77
+ .splitter.drag {
78
+ background: var(--accent);
79
+ }
80
+
60
81
  /* ---- left: list ---- */
61
82
  .left {
62
- border-right: 1px solid var(--line);
63
83
  display: flex;
64
84
  flex-direction: column;
65
85
  min-width: 0;
@@ -316,6 +336,24 @@
316
336
  .newbtn:hover {
317
337
  filter: brightness(1.12);
318
338
  }
339
+ .viewbtn {
340
+ background: transparent;
341
+ border: 1px solid var(--line);
342
+ border-radius: 7px;
343
+ color: var(--muted);
344
+ font-size: 11.5px;
345
+ padding: 3px 9px;
346
+ cursor: pointer;
347
+ line-height: 1.6;
348
+ }
349
+ .viewbtn:hover {
350
+ color: var(--fg);
351
+ border-color: var(--accent);
352
+ }
353
+ .viewbtn.on {
354
+ color: var(--accent);
355
+ border-color: var(--accent);
356
+ }
319
357
  .lcard .nfield {
320
358
  margin-top: 12px;
321
359
  }
@@ -428,6 +466,39 @@
428
466
  text-overflow: ellipsis;
429
467
  white-space: nowrap;
430
468
  }
469
+ /* compact view: one line per agent — dot + cli + live title (or prompt), age */
470
+ .row.crow {
471
+ display: flex;
472
+ align-items: center;
473
+ gap: 8px;
474
+ padding: 6px 18px;
475
+ }
476
+ .crow .cident {
477
+ font-family: var(--mono);
478
+ font-size: 11.5px;
479
+ color: var(--green);
480
+ flex: none;
481
+ }
482
+ .crow .cname {
483
+ font-weight: 600;
484
+ color: var(--purple);
485
+ flex: none;
486
+ }
487
+ .crow .ctitle {
488
+ flex: 1;
489
+ min-width: 0;
490
+ font-size: 12.5px;
491
+ overflow: hidden;
492
+ text-overflow: ellipsis;
493
+ white-space: nowrap;
494
+ }
495
+ .crow .ctitle.dim {
496
+ color: var(--muted);
497
+ }
498
+ .crow .age {
499
+ margin-left: 0;
500
+ flex: none;
501
+ }
431
502
  .rowtags {
432
503
  display: flex;
433
504
  flex-wrap: wrap;
@@ -509,50 +580,6 @@
509
580
  color: var(--muted);
510
581
  font-size: 14px;
511
582
  }
512
- .composer {
513
- border-top: 1px solid var(--line);
514
- padding: 12px 16px;
515
- display: flex;
516
- gap: 8px;
517
- align-items: flex-end;
518
- }
519
- .composer textarea {
520
- flex: 1;
521
- resize: none;
522
- background: var(--panel);
523
- border: 1px solid var(--line);
524
- border-radius: 9px;
525
- color: var(--fg);
526
- font: 13px var(--mono);
527
- padding: 9px 12px;
528
- outline: 0;
529
- min-height: 40px;
530
- max-height: 160px;
531
- }
532
- .composer textarea:focus {
533
- border-color: var(--accent);
534
- }
535
- .send {
536
- background: var(--accent);
537
- color: var(--bg);
538
- border: 0;
539
- border-radius: 9px;
540
- font-weight: 600;
541
- padding: 10px 18px;
542
- cursor: pointer;
543
- font-size: 13px;
544
- }
545
- .send:disabled {
546
- opacity: 0.45;
547
- cursor: not-allowed;
548
- }
549
- .hint {
550
- color: var(--muted);
551
- font-size: 11px;
552
- font-family: var(--mono);
553
- margin-top: 6px;
554
- padding: 0 16px 10px;
555
- }
556
583
  </style>
557
584
  <link
558
585
  rel="stylesheet"
@@ -589,6 +616,7 @@
589
616
  <div class="meta">
590
617
  <span id="count"></span>
591
618
  <span class="metaright">
619
+ <button id="viewbtn" class="viewbtn" title="toggle compact list">☰</button>
592
620
  <button id="newbtn" class="newbtn" title="spawn a new agent on this fleet">
593
621
  + New agent
594
622
  </button>
@@ -600,6 +628,8 @@
600
628
  <div class="list" id="list"></div>
601
629
  </div>
602
630
 
631
+ <div class="splitter" id="splitter" title="drag to resize"></div>
632
+
603
633
  <div class="right">
604
634
  <div class="rhead" id="rhead" style="display: none">
605
635
  <span class="dot" id="rdot"></span>
@@ -610,18 +640,9 @@
610
640
  >
611
641
  </div>
612
642
  <div class="log" id="log">
613
- <div class="placeholder">← pick an agent to tail its log and send it a message</div>
614
- </div>
615
- <div class="composer" id="composer" style="display: none">
616
- <textarea
617
- id="msg"
618
- rows="1"
619
- placeholder="message to send to the agent… (⌘/Ctrl+Enter)"
620
- ></textarea>
621
- <button class="send" id="send">Send ⏎</button>
622
- </div>
623
- <div class="hint" id="hint" style="display: none">
624
- POST /api/send → writes to the agent's stdin fifo, then Enter.
643
+ <div class="placeholder">
644
+ ← pick an agent to tail its log; type directly into the terminal
645
+ </div>
625
646
  </div>
626
647
  </div>
627
648
  </div>
@@ -963,14 +984,32 @@
963
984
  },
964
985
  };
965
986
 
987
+ // claude is the default CLI — show the cli name only when it differs, so the
988
+ // common case stays uncluttered and the identity (repo/branch) leads instead.
989
+ const cliLabel = (e) => (e.cli && e.cli !== "claude" ? e.cli : "");
990
+ // Parse owner/repo/branch from a cwd like .../ws/<owner>/<repo>/tree/<branch>.
991
+ function repoBranch(e) {
992
+ const m = /\/([^/]+)\/([^/]+)\/tree\/([^/]+)/.exec(e.cwd || "");
993
+ return m ? { owner: m[1], repo: m[2], branch: m[3] } : null;
994
+ }
995
+ // Identity string for the left panel. cap=true → repo/branch each clipped to
996
+ // 3 chars for the compact one-line view (e.g. "age/mai").
997
+ function ident(e, cap) {
998
+ const rb = repoBranch(e);
999
+ if (!rb) return "";
1000
+ const c = (s) => (cap && s.length > 3 ? s.slice(0, 3) : s);
1001
+ return `${c(rb.repo)}/${c(rb.branch)}`;
1002
+ }
1003
+
966
1004
  // Derive codehost-style mnemonic tags from a cwd like .../ws/<owner>/<repo>/tree/<wt>
967
1005
  function tagsFor(e) {
968
1006
  const t = [];
969
- const m = /\/([^/]+)\/([^/]+)\/tree\/([^/]+)/.exec(e.cwd || "");
970
- if (m) {
971
- t.push(["repo", `${m[1]}/${m[2]}`], ["wt", m[3]]);
1007
+ const rb = repoBranch(e);
1008
+ if (rb) {
1009
+ t.push(["repo", `${rb.owner}/${rb.repo}`], ["wt", rb.branch]);
972
1010
  }
973
- if (e.cli) t.push(["cli", e.cli]);
1011
+ const cli = cliLabel(e);
1012
+ if (cli) t.push(["cli", cli]);
974
1013
  if (e._host) t.push(["host", e._host]); // codehost rooms: which machine
975
1014
  return t;
976
1015
  }
@@ -1005,10 +1044,31 @@
1005
1044
  });
1006
1045
  }
1007
1046
 
1047
+ // Compact list: one line per agent (dot + cli + title), persisted per device.
1048
+ let compactList = localStorage.getItem("ay.compactList") === "1";
1049
+
1008
1050
  function renderList() {
1009
1051
  const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
1010
1052
  const shown = entries.filter((e) => matches(e, toks));
1011
1053
  $("count").textContent = `${shown.length} / ${entries.length} agents`;
1054
+ $("viewbtn").classList.toggle("on", compactList);
1055
+ if (compactList) {
1056
+ $("list").innerHTML =
1057
+ shown
1058
+ .map((e) => {
1059
+ const t = e.title || e.prompt || "";
1060
+ const id = ident(e, true);
1061
+ const cli = cliLabel(e);
1062
+ return `<div class="row crow ${String(e.pid) === sel ? "sel" : ""}" data-pid="${e.pid}">
1063
+ <span class="dot ${esc(e.status)}"></span>
1064
+ ${id ? `<span class="cident" title="${esc(ident(e))}">${esc(id)}</span>` : ""}
1065
+ ${cli ? `<span class="cname">${esc(cli)}</span>` : ""}
1066
+ <span class="ctitle ${e.title ? "" : "dim"}" title="${esc(t)}">${esc(t)}</span>
1067
+ <span class="age">${age(e)}</span></div>`;
1068
+ })
1069
+ .join("") || `<div class="empty">no match</div>`;
1070
+ return;
1071
+ }
1012
1072
  $("list").innerHTML =
1013
1073
  shown
1014
1074
  .map((e) => {
@@ -1020,7 +1080,7 @@
1020
1080
  .join("");
1021
1081
  return `<div class="row ${String(e.pid) === sel ? "sel" : ""}" data-pid="${e.pid}">
1022
1082
  <div class="r1"><span class="dot ${esc(e.status)}"></span>
1023
- <span class="name">${esc(e.cli)}</span>
1083
+ <span class="name">${esc(cliLabel(e) || ident(e) || "agent")}</span>
1024
1084
  <span class="badge">pid ${e.pid}</span>
1025
1085
  <span class="age">${age(e)}</span></div>
1026
1086
  ${e.title ? `<div class="rowtitle" title="${esc(e.title)}">${esc(e.title)}</div>` : ""}
@@ -1060,12 +1120,9 @@
1060
1120
  if (!e) return;
1061
1121
  renderList();
1062
1122
  $("rhead").style.display = "flex";
1063
- $("composer").style.display = "flex";
1064
- $("hint").style.display = "block";
1065
1123
  $("rdot").className = "dot " + e.status;
1066
- $("rname").textContent = e.title || e.cli;
1124
+ $("rname").textContent = e.title || cliLabel(e) || ident(e) || "agent";
1067
1125
  $("rpid").textContent = "pid " + e.pid;
1068
- $("msg").focus();
1069
1126
 
1070
1127
  // Render the agent's native TUI with xterm.js by feeding it the raw PTY
1071
1128
  // stream (ANSI/cursor control intact) — see /api/tail?raw=1.
@@ -1088,6 +1145,7 @@
1088
1145
  fit = new FitAddon.FitAddon();
1089
1146
  term.loadAddon(fit);
1090
1147
  term.open(logEl);
1148
+ term.focus();
1091
1149
  // An agent can rename itself by emitting an OSC 0/2 title sequence
1092
1150
  // (\x1b]2;my-name\x07); xterm parses it out of the raw PTY stream we already
1093
1151
  // feed it, so we just surface the latest title as the header name. Falls
@@ -1163,42 +1221,22 @@
1163
1221
  es = { close };
1164
1222
  }
1165
1223
 
1166
- async function send() {
1167
- if (!sel) return;
1168
- const msg = $("msg").value;
1169
- if (!msg.trim()) return;
1170
- $("send").disabled = true;
1171
- try {
1172
- const r = await Conn.post("/api/send", { keyword: sel, msg, code: "enter" });
1173
- if (r.ok) {
1174
- $("msg").value = "";
1175
- } else {
1176
- alert("send failed: " + r.text);
1177
- }
1178
- } finally {
1179
- $("send").disabled = false;
1180
- $("msg").focus();
1181
- }
1182
- }
1183
-
1184
1224
  $("list").addEventListener("click", (ev) => {
1185
1225
  const row = ev.target.closest(".row");
1186
1226
  if (row) select(row.dataset.pid);
1187
1227
  });
1188
1228
  $("q").addEventListener("input", renderList);
1229
+ $("viewbtn").addEventListener("click", () => {
1230
+ compactList = !compactList;
1231
+ localStorage.setItem("ay.compactList", compactList ? "1" : "0");
1232
+ renderList();
1233
+ });
1189
1234
  window.addEventListener("resize", () => {
1190
1235
  if (fit)
1191
1236
  try {
1192
1237
  fit.fit();
1193
1238
  } catch {}
1194
1239
  });
1195
- $("send").addEventListener("click", send);
1196
- $("msg").addEventListener("keydown", (ev) => {
1197
- if (ev.key === "Enter" && (ev.metaKey || ev.ctrlKey)) {
1198
- ev.preventDefault();
1199
- send();
1200
- }
1201
- });
1202
1240
 
1203
1241
  // ---- rooms: localStorage cache + a manager you open by clicking the badge ----
1204
1242
  const ROOMS_KEY = "ay.rooms";
@@ -1504,8 +1542,7 @@
1504
1542
  history.replaceState(null, document.title, location.pathname + location.search); // eat launch params
1505
1543
  if (spec) showLaunch(spec);
1506
1544
  setConn("● local", "var(--muted)");
1507
- loadList();
1508
- setInterval(loadList, 3000);
1545
+ startPolling();
1509
1546
  return;
1510
1547
  }
1511
1548
  // #k=<token> — local-mode auth from `ay serve --http`'s printed link.
@@ -1517,8 +1554,7 @@
1517
1554
  // SECURITY: strip the token from the URL immediately.
1518
1555
  history.replaceState(null, document.title, location.pathname + location.search);
1519
1556
  setConn("● local", "var(--muted)");
1520
- loadList();
1521
- setInterval(loadList, 3000);
1557
+ startPolling();
1522
1558
  return;
1523
1559
  }
1524
1560
  const h = decodeURIComponent(raw);
@@ -1567,10 +1603,97 @@
1567
1603
  // Render the UI immediately and refresh on a timer; connect to a room (if
1568
1604
  // any) in the BACKGROUND so a dead/slow cached room never blanks the page.
1569
1605
  if (!pending) setConn("● local", "var(--muted)");
1570
- loadList();
1571
- setInterval(loadList, 3000); // refresh statuses / new agents
1606
+ startPolling();
1572
1607
  if (pending) connectRoom(pending.room, pending.token, pending.host);
1573
1608
  }
1609
+
1610
+ // ---- activity-gated polling + auto-reload on new deploy ----------------
1611
+ // Poll the agent list while the page is actually in use; pause when the tab
1612
+ // is hidden or the user has been idle for IDLE_MS, so an unattended console
1613
+ // stops making requests. On returning to the tab we refresh immediately.
1614
+ let lastActivity = Date.now();
1615
+ const IDLE_MS = 60_000; // no interaction for 1 min → idle → stop polling
1616
+ ["mousemove", "keydown", "pointerdown", "wheel", "touchstart"].forEach((ev) =>
1617
+ window.addEventListener(ev, () => (lastActivity = Date.now()), { passive: true }),
1618
+ );
1619
+ const isActive = () =>
1620
+ document.visibilityState === "visible" && Date.now() - lastActivity < IDLE_MS;
1621
+
1622
+ let polling = false;
1623
+ function startPolling() {
1624
+ if (polling) return;
1625
+ polling = true;
1626
+ loadList();
1627
+ setInterval(() => {
1628
+ if (isActive()) loadList();
1629
+ }, 3000); // refresh statuses / new agents — only while active
1630
+ document.addEventListener("visibilitychange", () => {
1631
+ if (document.visibilityState === "visible") {
1632
+ lastActivity = Date.now();
1633
+ loadList();
1634
+ }
1635
+ });
1636
+ watchVersion();
1637
+ }
1638
+
1639
+ // Auto-reload when the console's own assets change (a new deploy). HEAD the
1640
+ // page once a minute and compare the cache validator; reload on change.
1641
+ // Best-effort — ignores failures (e.g. remote rooms / cross-origin).
1642
+ function watchVersion() {
1643
+ let seen = null;
1644
+ const check = async () => {
1645
+ if (!isActive()) return;
1646
+ try {
1647
+ const r = await fetch(location.pathname, { method: "HEAD", cache: "no-store" });
1648
+ const tag = r.headers.get("etag") || r.headers.get("last-modified");
1649
+ if (!tag) return;
1650
+ if (seen && seen !== tag) return location.reload();
1651
+ seen = tag;
1652
+ } catch {}
1653
+ };
1654
+ check();
1655
+ setInterval(check, 60_000);
1656
+ }
1657
+
1658
+ // ---- draggable middle splitter (VSCode-style) -------------------------
1659
+ // Drag adjusts the left column width (a CSS var on .app), clamped so neither
1660
+ // pane collapses, persisted per device, and refits the terminal as it moves.
1661
+ (function () {
1662
+ const sp = $("splitter");
1663
+ const app = document.querySelector(".app");
1664
+ const saved = localStorage.getItem("ay.leftw");
1665
+ if (saved) app.style.setProperty("--leftw", saved);
1666
+ let dragging = false;
1667
+ sp.addEventListener("pointerdown", (e) => {
1668
+ dragging = true;
1669
+ sp.classList.add("drag");
1670
+ sp.setPointerCapture(e.pointerId);
1671
+ e.preventDefault();
1672
+ });
1673
+ sp.addEventListener("pointermove", (e) => {
1674
+ if (!dragging) return;
1675
+ const w = Math.min(Math.max(e.clientX, 280), window.innerWidth - 360);
1676
+ app.style.setProperty("--leftw", w + "px");
1677
+ if (fit)
1678
+ try {
1679
+ fit.fit();
1680
+ } catch {}
1681
+ });
1682
+ const end = (e) => {
1683
+ if (!dragging) return;
1684
+ dragging = false;
1685
+ sp.classList.remove("drag");
1686
+ try {
1687
+ sp.releasePointerCapture(e.pointerId);
1688
+ } catch {}
1689
+ localStorage.setItem(
1690
+ "ay.leftw",
1691
+ getComputedStyle(app).getPropertyValue("--leftw").trim(),
1692
+ );
1693
+ };
1694
+ sp.addEventListener("pointerup", end);
1695
+ sp.addEventListener("pointercancel", end);
1696
+ })();
1574
1697
  boot();
1575
1698
  </script>
1576
1699
  </body>