@madarco/agentbox 0.2.0 → 0.4.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.
@@ -3,8 +3,8 @@ import {
3
3
  createBox,
4
4
  defaultBoxName,
5
5
  sanitizeBasename
6
- } from "./chunk-3OPLNXQ5.js";
7
- import "./chunk-3NIZKZPJ.js";
6
+ } from "./chunk-3NCUES35.js";
7
+ import "./chunk-MOC54XL6.js";
8
8
  import "./chunk-IDR4HVIC.js";
9
9
  import "./chunk-SOMIKEN2.js";
10
10
  export {
@@ -12,4 +12,4 @@ export {
12
12
  defaultBoxName,
13
13
  sanitizeBasename
14
14
  };
15
- //# sourceMappingURL=create-SE6H4B5U-ESNDTXAI.js.map
15
+ //# sourceMappingURL=create-SE6H4B5U-IWAZHJHV.js.map
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createBox
4
- } from "./chunk-3OPLNXQ5.js";
4
+ } from "./chunk-3NCUES35.js";
5
5
  import {
6
6
  AmbiguousBoxError,
7
7
  BoxNotFoundError,
8
8
  destroyBox,
9
+ getBoxEndpoints,
9
10
  getBoxHostPaths,
10
11
  inspectBox,
11
12
  listBoxes,
@@ -15,7 +16,7 @@ import {
15
16
  startBox,
16
17
  stopBox,
17
18
  unpauseBox
18
- } from "./chunk-3NU4PS5W.js";
19
+ } from "./chunk-3JKQNOXP.js";
19
20
  import {
20
21
  ClaudeSessionError,
21
22
  SHARED_CLAUDE_VOLUME,
@@ -37,7 +38,7 @@ import {
37
38
  resolveClaudeVolume,
38
39
  startClaudeSession,
39
40
  stopRelay
40
- } from "./chunk-3NIZKZPJ.js";
41
+ } from "./chunk-MOC54XL6.js";
41
42
  import {
42
43
  STATE_DIR,
43
44
  readState,
@@ -66,6 +67,7 @@ import {
66
67
  lookupKey,
67
68
  pruneOrphanProjectConfigs,
68
69
  pullToHost,
70
+ readBoxStatus,
69
71
  refreshExport,
70
72
  removeCheckpoint,
71
73
  removeImage,
@@ -1600,6 +1602,7 @@ var InputParser = class {
1600
1602
  if (b === LEADER) {
1601
1603
  this.flush();
1602
1604
  this.state = "leader";
1605
+ this.onEvent({ type: "leader", active: true });
1603
1606
  this.arm(this.leaderMs, "leader");
1604
1607
  } else if (b === ESC) {
1605
1608
  this.flush();
@@ -1622,14 +1625,18 @@ var InputParser = class {
1622
1625
  if (c === "v") this.onEvent({ type: "action", name: "vnc" });
1623
1626
  else if (c === "w") this.onEvent({ type: "action", name: "web" });
1624
1627
  else if (c === "c") this.onEvent({ type: "action", name: "code" });
1625
- else if (c === "q" || c === "d") this.onEvent({ type: "quit" });
1626
- else if (c === "k" || c === "p" || c === "P") this.onEvent({ type: "switch", dir: "prev" });
1628
+ else if (c === "s") this.onEvent({ type: "action", name: "stop" });
1629
+ else if (c === "p") this.onEvent({ type: "action", name: "pause" });
1630
+ else if (c === "d") this.onEvent({ type: "action", name: "destroy" });
1631
+ else if (c === "q") this.onEvent({ type: "quit" });
1632
+ else if (c === "k") this.onEvent({ type: "switch", dir: "prev" });
1627
1633
  else if (c === "j" || c === "n" || c === "N") this.onEvent({ type: "switch", dir: "next" });
1628
1634
  else {
1629
1635
  this.fwd.push(b);
1630
1636
  this.flush();
1631
1637
  }
1632
1638
  }
1639
+ this.onEvent({ type: "leader", active: false });
1633
1640
  this.state = "normal";
1634
1641
  i++;
1635
1642
  continue;
@@ -1737,6 +1744,7 @@ var InputParser = class {
1737
1744
  if (kind === "leader" && this.state === "leader") {
1738
1745
  this.fwd.push(LEADER);
1739
1746
  this.flush();
1747
+ this.onEvent({ type: "leader", active: false });
1740
1748
  this.state = "normal";
1741
1749
  } else if (kind === "esc" && (this.state === "esc" || this.state === "mouseX10")) {
1742
1750
  this.forwardVerbatim(this.esc);
@@ -1861,6 +1869,18 @@ var PtySession = class {
1861
1869
  };
1862
1870
 
1863
1871
  // src/dashboard/sidebar.ts
1872
+ function ellipsize(s, max) {
1873
+ if (max <= 0) return "";
1874
+ if (s.length <= max) return s;
1875
+ if (max === 1) return "\u2026";
1876
+ return s.slice(0, max - 1) + "\u2026";
1877
+ }
1878
+ function ellipsizeHead(s, max) {
1879
+ if (max <= 0) return "";
1880
+ if (s.length <= max) return s;
1881
+ if (max === 1) return "\u2026";
1882
+ return "\u2026" + s.slice(s.length - (max - 1));
1883
+ }
1864
1884
  function activityCell(b) {
1865
1885
  if (b.state !== "running") return `[${b.state}]`;
1866
1886
  switch (b.claudeActivity) {
@@ -1876,8 +1896,12 @@ function activityCell(b) {
1876
1896
  }
1877
1897
  var NEW_BOX_ID = "__agentbox_new__";
1878
1898
  var NEW_BOX_LABEL = "+ New box";
1879
- var SIDEBAR_HEADER = "\u2550 AgentBox \u2550";
1880
- var SIDEBAR_HEADER_LINES = 2;
1899
+ var SIDEBAR_HEADER = "AgentBox";
1900
+ function topBorder(label, w) {
1901
+ const lead = `\u256D\u2500\u2500\u2500 ${label} `;
1902
+ if (lead.length >= w) return lead.slice(0, w);
1903
+ return lead + "\u2500".repeat(w - lead.length);
1904
+ }
1881
1905
  function fit(s, w) {
1882
1906
  if (s.length === w) return s;
1883
1907
  if (s.length > w) return s.slice(0, w);
@@ -1889,17 +1913,55 @@ function center(s, w) {
1889
1913
  const leftPad = Math.floor(pad / 2);
1890
1914
  return " ".repeat(leftPad) + s + " ".repeat(pad - leftPad);
1891
1915
  }
1916
+ function projectLabel(project) {
1917
+ if (!project) return "(no project)";
1918
+ const parts = project.split("/").filter(Boolean);
1919
+ return parts[parts.length - 1] ?? project;
1920
+ }
1921
+ function stripTitleGlyph(s) {
1922
+ const t = s.replace(/^[\s\p{S}*·]+/u, "");
1923
+ return t.length > 0 ? t : s.trim();
1924
+ }
1925
+ function boxRow(b, marker, w) {
1926
+ const numStr = b.index != null ? `${b.index} ` : "";
1927
+ const status = activityCell(b);
1928
+ const left = `${marker}${numStr}`;
1929
+ const room = w - left.length - status.length - 1;
1930
+ if (room <= 0) return fit(`${left}${status}`, w);
1931
+ const middle = b.state === "running" && b.sessionTitle ? ellipsize(stripTitleGlyph(b.sessionTitle), room) : ellipsizeHead(b.name, room);
1932
+ return fit(`${left}${middle}`, w - status.length) + status;
1933
+ }
1892
1934
  function sidebarLines(boxes, selectedId, w, h) {
1893
- const lines = [center(SIDEBAR_HEADER, w), fit("", w)];
1894
- const nameW = Math.min(16, Math.max(6, ...boxes.map((b) => b.name.length), 6));
1935
+ const lines = [topBorder(SIDEBAR_HEADER, w), fit("", w)];
1936
+ const rowOwner = [null, null];
1937
+ const headerRows = [true, false];
1938
+ const push = (line, owner, header) => {
1939
+ lines.push(fit(line, w));
1940
+ rowOwner.push(owner);
1941
+ headerRows.push(header);
1942
+ };
1943
+ let prevProject;
1944
+ let seenGroup = false;
1895
1945
  for (const b of boxes) {
1896
- const marker = b.id === selectedId ? "\u25B8 " : " ";
1897
- const row2 = b.id === NEW_BOX_ID ? `${marker}${NEW_BOX_LABEL}` : `${marker}${fit(b.name, nameW)} ${activityCell(b)}`;
1898
- lines.push(fit(row2, w));
1946
+ const marker = b.id === selectedId ? "\u25B8" : " ";
1947
+ if (b.id === NEW_BOX_ID) {
1948
+ push(`${marker}${NEW_BOX_LABEL}`, b.id, false);
1949
+ continue;
1950
+ }
1951
+ if (!seenGroup || b.project !== prevProject) {
1952
+ push(center(` \u2500\u2500 ${projectLabel(b.project)} \u2500\u2500 `, w), null, true);
1953
+ prevProject = b.project;
1954
+ seenGroup = true;
1955
+ }
1956
+ push(boxRow(b, marker, w), b.id, false);
1899
1957
  }
1900
- if (boxes.length === 0) lines.push(fit(" (no boxes)", w));
1901
- while (lines.length < h) lines.push(fit("", w));
1902
- return lines.slice(0, h);
1958
+ if (boxes.length === 0) push(" (no boxes)", null, false);
1959
+ while (lines.length < h) push("", null, false);
1960
+ return {
1961
+ lines: lines.slice(0, h),
1962
+ rowOwner: rowOwner.slice(0, h),
1963
+ headerRows: headerRows.slice(0, h)
1964
+ };
1903
1965
  }
1904
1966
  function menuLines(boxName, w, h) {
1905
1967
  const body = [
@@ -1963,29 +2025,58 @@ var BRAND_NOBOLD = "\x1B[22m";
1963
2025
  var HINT_KEY = "\x1B[38;5;255m";
1964
2026
  var HINT_TXT = "\x1B[38;5;245m";
1965
2027
  var BAR_RESET = "\x1B[0m";
2028
+ var SWITCH_HINT = ["Control+Option+\u2191/\u2193", "switch"];
1966
2029
  var HINT_GROUPS = [
1967
- ["Control+Option+Up/Down", "switch"],
2030
+ SWITCH_HINT,
1968
2031
  ["Control+a c", "code"],
1969
2032
  ["Control+a v", "vnc"],
1970
2033
  ["Control+a w", "web"],
1971
2034
  ["Control+a q", "quit"]
1972
2035
  ];
1973
- function statusLine(box, w, stateLabel) {
2036
+ var COLLAPSED_HINT_GROUPS = [
2037
+ SWITCH_HINT,
2038
+ ["Control+a", "more"]
2039
+ ];
2040
+ var ADVANCED_HINT_GROUPS = [
2041
+ ["c", "code"],
2042
+ ["v", "vnc"],
2043
+ ["w", "web"],
2044
+ ["s", "stop"],
2045
+ ["p", "pause"],
2046
+ ["d", "destroy"],
2047
+ ["q", "quit"]
2048
+ ];
2049
+ function statusLine(box, w, stateLabel, groups = HINT_GROUPS) {
1974
2050
  const state = stateLabel ?? (box ? box.state === "running" ? box.claudeActivity ?? "unknown" : box.state : "");
1975
2051
  const brandPrefix = box ? " agentbox \u25B8 " : " agentbox ";
1976
- const brandMain = box ? `${box.name} (${state}) ` : "";
1977
- const left = brandPrefix + brandMain;
1978
- const leftStyled = BAR_BRAND + brandPrefix + BRAND_BOLD + brandMain + BRAND_NOBOLD;
2052
+ const base = box ? `${box.name} (${state})` : "";
2053
+ const coreMain = box ? `${base} ` : "";
2054
+ const corePlain = brandPrefix + coreMain;
1979
2055
  const SEP = " \u2502 ";
1980
- const rightPlain = HINT_GROUPS.map(([k, l]) => `${k}: ${l}`).join(SEP) + " ";
1981
- const rightStyled = HINT_GROUPS.map(([k, l]) => `${HINT_KEY}${k}${HINT_TXT}: ${l}`).join(
1982
- `${HINT_TXT}${SEP}`
1983
- ) + " ";
1984
- if (left.length + rightPlain.length + 1 > w) {
1985
- return BAR_BASE + BAR_BRAND + fit(left, w) + BAR_RESET;
1986
- }
1987
- const gap = w - left.length - rightPlain.length;
1988
- return BAR_BASE + leftStyled + BAR_BASE + " ".repeat(gap) + rightStyled + BAR_RESET;
2056
+ const renderHints = (g) => ({
2057
+ plain: g.map(([k, l]) => `${k}: ${l}`).join(SEP) + " ",
2058
+ styled: g.map(([k, l]) => `${HINT_KEY}${k}${HINT_TXT}: ${l}`).join(`${HINT_TXT}${SEP}`) + " "
2059
+ });
2060
+ let hints = null;
2061
+ for (const g of [groups, COLLAPSED_HINT_GROUPS]) {
2062
+ const h = renderHints(g);
2063
+ if (corePlain.length + h.plain.length + 1 <= w) {
2064
+ hints = h;
2065
+ break;
2066
+ }
2067
+ }
2068
+ if (!hints) {
2069
+ return BAR_BASE + BAR_BRAND + fit(corePlain, w) + BAR_RESET;
2070
+ }
2071
+ const room = w - corePlain.length - hints.plain.length - 1;
2072
+ let titleSeg = "";
2073
+ if (box?.sessionTitle && room >= 7) {
2074
+ titleSeg = ` \u2014 ${ellipsize(box.sessionTitle, Math.min(40, room - 3))}`;
2075
+ }
2076
+ const leftPlain = brandPrefix + base + titleSeg + (box ? " " : "");
2077
+ const leftStyled = BAR_BRAND + brandPrefix + BRAND_BOLD + base + titleSeg + (box ? " " : "") + BRAND_NOBOLD;
2078
+ const gap = w - leftPlain.length - hints.plain.length;
2079
+ return BAR_BASE + leftStyled + BAR_BASE + " ".repeat(gap) + hints.styled + BAR_RESET;
1989
2080
  }
1990
2081
 
1991
2082
  // src/dashboard/compositor.ts
@@ -1996,6 +2087,7 @@ var SGR_RESET = "\x1B[0m";
1996
2087
  var POLL_MS = 1e3;
1997
2088
  var FRAME_MS = 16;
1998
2089
  var RESIZE_DEBOUNCE_MS = 120;
2090
+ var LEADER_LINGER_MS = 1500;
1999
2091
  var SYNC_BEGIN = "\x1B[?2026h";
2000
2092
  var SYNC_END = "\x1B[?2026l";
2001
2093
  function cursorTo2(x, y) {
@@ -2008,10 +2100,40 @@ var Compositor = class {
2008
2100
  this.layout = computeLayout(this.out.columns ?? 100, this.out.rows ?? 30);
2009
2101
  this.parser = new InputParser({
2010
2102
  onEvent: (e) => {
2103
+ if (e.type === "leader") {
2104
+ if (this.leaderLingerTimer) {
2105
+ clearTimeout(this.leaderLingerTimer);
2106
+ this.leaderLingerTimer = null;
2107
+ }
2108
+ if (e.active) {
2109
+ this.leaderActive = true;
2110
+ } else {
2111
+ this.leaderLingerTimer = setTimeout(() => {
2112
+ this.leaderLingerTimer = null;
2113
+ this.leaderActive = false;
2114
+ this.drawChrome();
2115
+ }, LEADER_LINGER_MS);
2116
+ }
2117
+ this.drawChrome();
2118
+ return;
2119
+ }
2120
+ if (this.pendingConfirm) {
2121
+ if (e.type === "forward") {
2122
+ this.handleConfirmKey(e.bytes);
2123
+ return;
2124
+ }
2125
+ this.pendingConfirm = null;
2126
+ this.drawChrome();
2127
+ }
2011
2128
  if (e.type === "quit") this.onSig();
2012
2129
  else if (e.type === "switch") this.switchBox(e.dir);
2013
- else if (e.type === "action") void this.doAction(e.name);
2014
- else if (this.createMenu) this.handleCreateMenuKey(e.bytes);
2130
+ else if (e.type === "action") {
2131
+ if (e.name === "pause" || e.name === "stop" || e.name === "destroy") {
2132
+ void this.doLifecycle(e.name);
2133
+ } else {
2134
+ void this.doAction(e.name);
2135
+ }
2136
+ } else if (this.createMenu) this.handleCreateMenuKey(e.bytes);
2015
2137
  else if (this.lifecycleMenu) this.handleLifecycleMenuKey(e.bytes);
2016
2138
  else if (this.menu) this.handleMenuKey(e.bytes);
2017
2139
  else this.session?.write(e.bytes);
@@ -2038,6 +2160,14 @@ var Compositor = class {
2038
2160
  menu = null;
2039
2161
  lifecycleMenu = null;
2040
2162
  createMenu = null;
2163
+ /** True while the Ctrl-a leader is pending — swaps the footer to the
2164
+ * expanded chord menu (chrome only; never touches the right pane). */
2165
+ leaderActive = false;
2166
+ /** Holds the expanded footer for LEADER_LINGER_MS after the leader resolves
2167
+ * (so the chord menu doesn't flash by). */
2168
+ leaderLingerTimer = null;
2169
+ /** Set while a destroy confirm is pending in the status bar. */
2170
+ pendingConfirm = null;
2041
2171
  activeMode = "claude";
2042
2172
  flashMsg = null;
2043
2173
  flashTimer = null;
@@ -2097,7 +2227,9 @@ var Compositor = class {
2097
2227
  return this.boxes.find((b) => b.id === this.selectedId);
2098
2228
  }
2099
2229
  async poll() {
2100
- const before = JSON.stringify(this.boxes.map((b) => [b.id, b.state, b.claudeActivity]));
2230
+ const before = JSON.stringify(
2231
+ this.boxes.map((b) => [b.id, b.state, b.claudeActivity, b.sessionTitle])
2232
+ );
2101
2233
  await this.refreshBoxes();
2102
2234
  if (this.busy) {
2103
2235
  } else if (!this.boxes.some((b) => b.id === this.selectedId) && this.boxes[0]) {
@@ -2109,7 +2241,9 @@ var Compositor = class {
2109
2241
  const reresolve = this.session && !running || this.placeholder && running || this.menu && !running || this.lifecycleMenu != null && box?.state !== this.lifecycleMenu.state;
2110
2242
  if (reresolve) await this.spawnActive();
2111
2243
  }
2112
- if (JSON.stringify(this.boxes.map((b) => [b.id, b.state, b.claudeActivity])) !== before) {
2244
+ if (JSON.stringify(
2245
+ this.boxes.map((b) => [b.id, b.state, b.claudeActivity, b.sessionTitle])
2246
+ ) !== before) {
2113
2247
  this.drawChrome();
2114
2248
  }
2115
2249
  }
@@ -2124,6 +2258,7 @@ var Compositor = class {
2124
2258
  this.menu = null;
2125
2259
  this.lifecycleMenu = null;
2126
2260
  this.createMenu = null;
2261
+ this.pendingConfirm = null;
2127
2262
  this.clearRightPane();
2128
2263
  const id = this.selectedId;
2129
2264
  const target = await this.deps.resolveTarget(id);
@@ -2137,6 +2272,7 @@ var Compositor = class {
2137
2272
  this.menu = null;
2138
2273
  this.lifecycleMenu = null;
2139
2274
  this.createMenu = null;
2275
+ this.pendingConfirm = null;
2140
2276
  if (target.kind === "attach") {
2141
2277
  this.activeMode = target.mode ?? "claude";
2142
2278
  this.session = new PtySession(
@@ -2214,7 +2350,7 @@ var Compositor = class {
2214
2350
  for (const b of bytes) {
2215
2351
  if (m.confirmDestroy) {
2216
2352
  if (b === 121 || b === 13 || b === 10) {
2217
- void this.choosePaused("destroy");
2353
+ void this.runDestroy(this.selectedId, this.selectedBox()?.name ?? this.selectedId);
2218
2354
  } else {
2219
2355
  m.confirmDestroy = false;
2220
2356
  this.drawChrome();
@@ -2224,7 +2360,7 @@ var Compositor = class {
2224
2360
  }
2225
2361
  const resumeKey = m.state === "paused" ? 117 : 115;
2226
2362
  if (b === resumeKey) {
2227
- void this.choosePaused("resume");
2363
+ void this.resumeSelected();
2228
2364
  return;
2229
2365
  }
2230
2366
  if (b === 100) {
@@ -2235,37 +2371,27 @@ var Compositor = class {
2235
2371
  }
2236
2372
  }
2237
2373
  }
2238
- async choosePaused(which) {
2374
+ async resumeSelected() {
2239
2375
  if (this.busy) return;
2240
2376
  const id = this.selectedId;
2241
2377
  const name = this.selectedBox()?.name ?? id;
2242
- const resumeVerb = this.lifecycleMenu?.state === "stopped" ? "start" : "unpause";
2378
+ const verb = this.lifecycleMenu?.state === "stopped" ? "start" : "unpause";
2243
2379
  this.busy = true;
2244
2380
  this.menu = null;
2245
2381
  this.lifecycleMenu = null;
2246
2382
  this.createMenu = null;
2247
- this.placeholder = ["", which === "resume" ? " Resuming\u2026" : " Destroying\u2026"];
2383
+ this.placeholder = ["", " Resuming\u2026"];
2248
2384
  this.prevRows = null;
2249
2385
  this.drawChrome();
2250
2386
  this.scheduleRender();
2251
2387
  try {
2252
- if (which === "resume") {
2253
- await this.deps.resumeBox(id);
2254
- if (this.selectedId !== id || this.tornDown) return;
2255
- await this.refreshBoxes();
2256
- await this.spawnActive();
2257
- } else {
2258
- await this.deps.destroyBox(id);
2259
- if (this.tornDown) return;
2260
- await this.refreshBoxes();
2261
- if (this.boxes[0]) this.selectedId = this.boxes[0].id;
2262
- await this.spawnActive();
2263
- this.flash(`destroyed ${name}`);
2264
- }
2388
+ await this.deps.resumeBox(id);
2389
+ if (this.selectedId !== id || this.tornDown) return;
2390
+ await this.refreshBoxes();
2391
+ await this.spawnActive();
2265
2392
  } catch (err) {
2266
2393
  if (this.selectedId !== id || this.tornDown) return;
2267
2394
  const msg = err instanceof Error ? err.message : String(err);
2268
- const verb = which === "resume" ? resumeVerb : "destroy";
2269
2395
  this.placeholder = [
2270
2396
  "",
2271
2397
  ` Failed to ${verb} ${name}:`,
@@ -2279,6 +2405,102 @@ var Compositor = class {
2279
2405
  this.busy = false;
2280
2406
  }
2281
2407
  }
2408
+ /** Destroy `id` and recover the selection. Shared by the lifecycle-menu
2409
+ * confirm and the running-box `Ctrl-a d` status-bar confirm. */
2410
+ async runDestroy(id, name) {
2411
+ if (this.busy) return;
2412
+ this.busy = true;
2413
+ this.menu = null;
2414
+ this.lifecycleMenu = null;
2415
+ this.createMenu = null;
2416
+ this.placeholder = ["", " Destroying\u2026"];
2417
+ this.prevRows = null;
2418
+ this.drawChrome();
2419
+ this.scheduleRender();
2420
+ try {
2421
+ await this.deps.destroyBox(id);
2422
+ if (this.tornDown) return;
2423
+ await this.refreshBoxes();
2424
+ if (this.boxes[0]) this.selectedId = this.boxes[0].id;
2425
+ await this.spawnActive();
2426
+ this.flash(`destroyed ${name}`);
2427
+ } catch (err) {
2428
+ if (this.tornDown) return;
2429
+ const msg = err instanceof Error ? err.message : String(err);
2430
+ this.placeholder = [
2431
+ "",
2432
+ ` Failed to destroy ${name}:`,
2433
+ ` ${msg}`,
2434
+ "",
2435
+ ` Try from a shell: agentbox destroy ${name}`
2436
+ ];
2437
+ this.prevRows = null;
2438
+ this.scheduleRender();
2439
+ } finally {
2440
+ this.busy = false;
2441
+ }
2442
+ }
2443
+ /** Ctrl-a p/s/d on the selected box. pause/stop transition state (the pane
2444
+ * re-resolves to the lifecycle menu); destroy asks to confirm first. */
2445
+ async doLifecycle(name) {
2446
+ if (this.selectedId === NEW_BOX_ID) {
2447
+ this.flash("select a box first");
2448
+ return;
2449
+ }
2450
+ const id = this.selectedId;
2451
+ const boxName = this.selectedBox()?.name ?? id;
2452
+ if (name === "destroy") {
2453
+ this.pendingConfirm = { boxId: id, name: boxName };
2454
+ this.drawChrome();
2455
+ return;
2456
+ }
2457
+ if (this.selectedBox()?.state !== "running") {
2458
+ this.flash(`${boxName} is not running`);
2459
+ return;
2460
+ }
2461
+ if (this.busy) return;
2462
+ this.busy = true;
2463
+ this.menu = null;
2464
+ this.lifecycleMenu = null;
2465
+ this.createMenu = null;
2466
+ this.placeholder = ["", name === "pause" ? " Pausing\u2026" : " Stopping\u2026"];
2467
+ this.prevRows = null;
2468
+ this.drawChrome();
2469
+ this.scheduleRender();
2470
+ try {
2471
+ if (name === "pause") await this.deps.pauseBox(id);
2472
+ else await this.deps.stopBox(id);
2473
+ if (this.selectedId !== id || this.tornDown) return;
2474
+ await this.refreshBoxes();
2475
+ await this.spawnActive();
2476
+ this.flash(`${name === "pause" ? "paused" : "stopped"} ${boxName}`);
2477
+ } catch (err) {
2478
+ if (this.selectedId !== id || this.tornDown) return;
2479
+ const msg = err instanceof Error ? err.message : String(err);
2480
+ this.placeholder = [
2481
+ "",
2482
+ ` Failed to ${name} ${boxName}:`,
2483
+ ` ${msg}`,
2484
+ "",
2485
+ ` Try from a shell: agentbox ${name} ${boxName}`
2486
+ ];
2487
+ this.prevRows = null;
2488
+ this.scheduleRender();
2489
+ } finally {
2490
+ this.busy = false;
2491
+ }
2492
+ }
2493
+ handleConfirmKey(bytes) {
2494
+ const c = this.pendingConfirm;
2495
+ if (!c) return;
2496
+ const b = bytes[0];
2497
+ this.pendingConfirm = null;
2498
+ if (b === 121 || b === 13 || b === 10) {
2499
+ void this.runDestroy(c.boxId, c.name);
2500
+ } else {
2501
+ this.drawChrome();
2502
+ }
2503
+ }
2282
2504
  handleCreateMenuKey(bytes) {
2283
2505
  for (const b of bytes) {
2284
2506
  if (b === 99 || b === 13 || b === 10) {
@@ -2360,6 +2582,7 @@ var Compositor = class {
2360
2582
  }
2361
2583
  switchBox(dir) {
2362
2584
  if (this.boxes.length === 0) return;
2585
+ this.pendingConfirm = null;
2363
2586
  const i = Math.max(
2364
2587
  0,
2365
2588
  this.boxes.findIndex((b) => b.id === this.selectedId)
@@ -2431,23 +2654,36 @@ var Compositor = class {
2431
2654
  drawChrome() {
2432
2655
  if (this.tornDown || this.layout.tooSmall) return;
2433
2656
  const { sidebar, sepX, statusY } = this.layout;
2434
- const lines = sidebarLines(this.boxes, this.selectedId, sidebar.w, sidebar.h);
2435
- const selIdx = this.boxes.findIndex((b) => b.id === this.selectedId);
2436
- const selRow = selIdx >= 0 ? SIDEBAR_HEADER_LINES + selIdx : -1;
2657
+ const { lines, rowOwner, headerRows } = sidebarLines(
2658
+ this.boxes,
2659
+ this.selectedId,
2660
+ sidebar.w,
2661
+ sidebar.h
2662
+ );
2437
2663
  let s = SYNC_BEGIN + "\x1B[0m";
2438
2664
  for (let i = 0; i < lines.length; i++) {
2439
- const style = i === 0 ? SB_HEADER : i === selRow ? SB_SELECTED : SB_BODY;
2665
+ const style = headerRows[i] ? SB_HEADER : rowOwner[i] === this.selectedId ? SB_SELECTED : SB_BODY;
2440
2666
  s += cursorTo2(0, i) + style + lines[i] + SGR_RESET;
2441
2667
  }
2442
- for (let y = 0; y < sidebar.h; y++) s += cursorTo2(sepX, y) + "\u2502";
2668
+ for (let y = 0; y < sidebar.h; y++)
2669
+ s += cursorTo2(sepX, y) + SB_HEADER + (y === 0 ? "\u256E" : "\u2502") + SGR_RESET;
2443
2670
  let status;
2444
- if (this.flashMsg) {
2671
+ if (this.pendingConfirm) {
2672
+ const w = this.layout.cols;
2673
+ const txt = ` Destroy ${this.pendingConfirm.name}? y = confirm \xB7 any other key = cancel `.slice(0, w).padEnd(w);
2674
+ status = `\x1B[7m${txt}\x1B[0m`;
2675
+ } else if (this.flashMsg) {
2445
2676
  const w = this.layout.cols;
2446
2677
  const txt = ` ${this.flashMsg} `.slice(0, w).padEnd(w);
2447
2678
  status = `\x1B[7m${txt}\x1B[0m`;
2448
2679
  } else {
2449
2680
  const stateLabel = this.selectedId === NEW_BOX_ID ? "create" : this.menu ? "menu" : this.session && this.activeMode === "shell" ? "shell" : void 0;
2450
- status = statusLine(this.selectedBox(), this.layout.cols, stateLabel);
2681
+ status = statusLine(
2682
+ this.selectedBox(),
2683
+ this.layout.cols,
2684
+ stateLabel,
2685
+ this.leaderActive ? ADVANCED_HINT_GROUPS : void 0
2686
+ );
2451
2687
  }
2452
2688
  s += cursorTo2(0, statusY) + status;
2453
2689
  this.out.write(s + SYNC_END);
@@ -2474,6 +2710,7 @@ var Compositor = class {
2474
2710
  if (this.pollTimer) clearInterval(this.pollTimer);
2475
2711
  if (this.resizeTimer) clearTimeout(this.resizeTimer);
2476
2712
  if (this.flashTimer) clearTimeout(this.flashTimer);
2713
+ if (this.leaderLingerTimer) clearTimeout(this.leaderLingerTimer);
2477
2714
  this.parser.dispose();
2478
2715
  this.disposeSession();
2479
2716
  this.inp.off("data", this.onData);
@@ -2501,7 +2738,15 @@ function scoped(all, projectRoot, boxes) {
2501
2738
  return sortBoxes(all ? boxes : boxes.filter((b) => b.projectRoot === projectRoot));
2502
2739
  }
2503
2740
  function toSidebar(b) {
2504
- return { id: b.id, name: b.name, state: b.state, claudeActivity: b.claudeActivity };
2741
+ return {
2742
+ id: b.id,
2743
+ name: b.name,
2744
+ state: b.state,
2745
+ claudeActivity: b.claudeActivity,
2746
+ sessionTitle: b.claudeSessionTitle,
2747
+ index: b.projectIndex,
2748
+ project: b.projectRoot
2749
+ };
2505
2750
  }
2506
2751
  var dashboardCommand = new Command7("dashboard").description("Box list + the selected box live Agent session").argument("[box]", "initial box (default: first running box; -p restricts to the cwd project)").option("-p, --project", "only this project's boxes (default: all boxes globally)").action(async (idOrName, opts) => {
2507
2752
  try {
@@ -2643,23 +2888,35 @@ var dashboardCommand = new Command7("dashboard").description("Box list + the sel
2643
2888
  const ep = box.endpoints.endpoints.find((e) => e.kind === kind);
2644
2889
  return { name: box.name, url: ep && ep.reachable && ep.url ? ep.url : null };
2645
2890
  };
2891
+ const webTarget = (box) => {
2892
+ const ep = box.endpoints.endpoints.find((e) => e.kind === "web");
2893
+ const exposed = Boolean(ep && ep.reachable && ep.url);
2894
+ const url = box.endpoints.domainIsOrb ? `http://${box.endpoints.domain}` : exposed && ep?.url ? ep.url : `http://${box.endpoints.domain}`;
2895
+ return { url, exposed };
2896
+ };
2646
2897
  const openVnc = async (boxId) => {
2647
2898
  const { url } = await findEndpointUrl(boxId, "vnc");
2648
2899
  if (!url) return "VNC not available for this box";
2900
+ let exposedWebUrl = null;
2649
2901
  try {
2650
2902
  const box = await findBox2(boxId);
2903
+ const web = webTarget(box);
2904
+ if (web.exposed) exposedWebUrl = web.url;
2651
2905
  const br = await ensureBoxBrowser(box.container);
2652
2906
  if (!br.up) return `VNC: in-box browser unavailable (${br.reason ?? "box not running?"})`;
2653
2907
  } catch {
2654
2908
  }
2655
2909
  detach("open", [url]);
2910
+ if (exposedWebUrl) {
2911
+ detach("open", [exposedWebUrl]);
2912
+ return "Opening VNC + web in browser\u2026";
2913
+ }
2656
2914
  return "Opening VNC in browser\u2026";
2657
2915
  };
2658
2916
  const openWeb = async (boxId) => {
2659
2917
  const box = (await listBoxes()).find((b) => b.id === boxId);
2660
2918
  if (!box) return "box not found";
2661
- const ep = box.endpoints.endpoints.find((e) => e.kind === "web");
2662
- const url = ep && ep.reachable && ep.url ? ep.url : `http://${box.endpoints.domain}`;
2919
+ const { url } = webTarget(box);
2663
2920
  detach("open", [url]);
2664
2921
  return `Opening ${url.replace(/^https?:\/\//, "")}\u2026`;
2665
2922
  };
@@ -2675,6 +2932,12 @@ var dashboardCommand = new Command7("dashboard").description("Box list + the sel
2675
2932
  if (box.state === "paused") await unpauseBox(box.id);
2676
2933
  else await startBox(box.id);
2677
2934
  };
2935
+ const pauseBoxAction = async (boxId) => {
2936
+ await pauseBox(boxId);
2937
+ };
2938
+ const stopBoxAction = async (boxId) => {
2939
+ await stopBox(boxId);
2940
+ };
2678
2941
  const destroyBoxAction = async (boxId) => {
2679
2942
  await destroyBox(boxId);
2680
2943
  };
@@ -2688,6 +2951,8 @@ var dashboardCommand = new Command7("dashboard").description("Box list + the sel
2688
2951
  openShell,
2689
2952
  createNewBox,
2690
2953
  resumeBox,
2954
+ pauseBox: pauseBoxAction,
2955
+ stopBox: stopBoxAction,
2691
2956
  destroyBox: destroyBoxAction,
2692
2957
  openVnc,
2693
2958
  openCode,
@@ -3484,6 +3749,21 @@ var screenCommand = new Command18("screen").description("Open a box's VNC (noVNC
3484
3749
  }
3485
3750
  process.stdout.write(`opened ${url}
3486
3751
  `);
3752
+ try {
3753
+ const { record } = await getBoxHostPaths(box.id);
3754
+ const persisted = await readBoxStatus(box.id);
3755
+ const eps = await getBoxEndpoints(record, engine, persisted);
3756
+ const webEp = eps.endpoints.find((e) => e.kind === "web");
3757
+ if (webEp?.reachable && webEp.url) {
3758
+ const webUrl = engine === "orbstack" && !opts.loopback ? `http://${record.container}.orb.local` : webEp.url;
3759
+ const w = spawnSync5("open", [webUrl], { stdio: "inherit" });
3760
+ if (w.status === 0) process.stdout.write(`also opened ${webUrl}
3761
+ `);
3762
+ else log19.warn(`could not open web app (${webUrl})`);
3763
+ }
3764
+ } catch (e) {
3765
+ log19.warn(`could not open web app: ${e instanceof Error ? e.message : String(e)}`);
3766
+ }
3487
3767
  } catch (err) {
3488
3768
  handleLifecycleError(err);
3489
3769
  }