@madarco/agentbox 0.1.0 → 0.3.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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createBox
4
- } from "./chunk-OOOKFFR5.js";
4
+ } from "./chunk-3NCUES35.js";
5
5
  import {
6
6
  AmbiguousBoxError,
7
7
  BoxNotFoundError,
@@ -15,7 +15,7 @@ import {
15
15
  startBox,
16
16
  stopBox,
17
17
  unpauseBox
18
- } from "./chunk-RWJE6AER.js";
18
+ } from "./chunk-7NQFIBQG.js";
19
19
  import {
20
20
  ClaudeSessionError,
21
21
  SHARED_CLAUDE_VOLUME,
@@ -37,7 +37,7 @@ import {
37
37
  resolveClaudeVolume,
38
38
  startClaudeSession,
39
39
  stopRelay
40
- } from "./chunk-O5HS3QHW.js";
40
+ } from "./chunk-MOC54XL6.js";
41
41
  import {
42
42
  STATE_DIR,
43
43
  readState,
@@ -1600,6 +1600,7 @@ var InputParser = class {
1600
1600
  if (b === LEADER) {
1601
1601
  this.flush();
1602
1602
  this.state = "leader";
1603
+ this.onEvent({ type: "leader", active: true });
1603
1604
  this.arm(this.leaderMs, "leader");
1604
1605
  } else if (b === ESC) {
1605
1606
  this.flush();
@@ -1622,14 +1623,18 @@ var InputParser = class {
1622
1623
  if (c === "v") this.onEvent({ type: "action", name: "vnc" });
1623
1624
  else if (c === "w") this.onEvent({ type: "action", name: "web" });
1624
1625
  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" });
1626
+ else if (c === "s") this.onEvent({ type: "action", name: "stop" });
1627
+ else if (c === "p") this.onEvent({ type: "action", name: "pause" });
1628
+ else if (c === "d") this.onEvent({ type: "action", name: "destroy" });
1629
+ else if (c === "q") this.onEvent({ type: "quit" });
1630
+ else if (c === "k") this.onEvent({ type: "switch", dir: "prev" });
1627
1631
  else if (c === "j" || c === "n" || c === "N") this.onEvent({ type: "switch", dir: "next" });
1628
1632
  else {
1629
1633
  this.fwd.push(b);
1630
1634
  this.flush();
1631
1635
  }
1632
1636
  }
1637
+ this.onEvent({ type: "leader", active: false });
1633
1638
  this.state = "normal";
1634
1639
  i++;
1635
1640
  continue;
@@ -1737,6 +1742,7 @@ var InputParser = class {
1737
1742
  if (kind === "leader" && this.state === "leader") {
1738
1743
  this.fwd.push(LEADER);
1739
1744
  this.flush();
1745
+ this.onEvent({ type: "leader", active: false });
1740
1746
  this.state = "normal";
1741
1747
  } else if (kind === "esc" && (this.state === "esc" || this.state === "mouseX10")) {
1742
1748
  this.forwardVerbatim(this.esc);
@@ -1861,6 +1867,18 @@ var PtySession = class {
1861
1867
  };
1862
1868
 
1863
1869
  // src/dashboard/sidebar.ts
1870
+ function ellipsize(s, max) {
1871
+ if (max <= 0) return "";
1872
+ if (s.length <= max) return s;
1873
+ if (max === 1) return "\u2026";
1874
+ return s.slice(0, max - 1) + "\u2026";
1875
+ }
1876
+ function ellipsizeHead(s, max) {
1877
+ if (max <= 0) return "";
1878
+ if (s.length <= max) return s;
1879
+ if (max === 1) return "\u2026";
1880
+ return "\u2026" + s.slice(s.length - (max - 1));
1881
+ }
1864
1882
  function activityCell(b) {
1865
1883
  if (b.state !== "running") return `[${b.state}]`;
1866
1884
  switch (b.claudeActivity) {
@@ -1876,8 +1894,12 @@ function activityCell(b) {
1876
1894
  }
1877
1895
  var NEW_BOX_ID = "__agentbox_new__";
1878
1896
  var NEW_BOX_LABEL = "+ New box";
1879
- var SIDEBAR_HEADER = "\u2550 AgentBox \u2550";
1880
- var SIDEBAR_HEADER_LINES = 2;
1897
+ var SIDEBAR_HEADER = "AgentBox";
1898
+ function topBorder(label, w) {
1899
+ const lead = `\u256D\u2500\u2500\u2500 ${label} `;
1900
+ if (lead.length >= w) return lead.slice(0, w);
1901
+ return lead + "\u2500".repeat(w - lead.length);
1902
+ }
1881
1903
  function fit(s, w) {
1882
1904
  if (s.length === w) return s;
1883
1905
  if (s.length > w) return s.slice(0, w);
@@ -1889,17 +1911,55 @@ function center(s, w) {
1889
1911
  const leftPad = Math.floor(pad / 2);
1890
1912
  return " ".repeat(leftPad) + s + " ".repeat(pad - leftPad);
1891
1913
  }
1914
+ function projectLabel(project) {
1915
+ if (!project) return "(no project)";
1916
+ const parts = project.split("/").filter(Boolean);
1917
+ return parts[parts.length - 1] ?? project;
1918
+ }
1919
+ function stripTitleGlyph(s) {
1920
+ const t = s.replace(/^[\s\p{S}*·]+/u, "");
1921
+ return t.length > 0 ? t : s.trim();
1922
+ }
1923
+ function boxRow(b, marker, w) {
1924
+ const numStr = b.index != null ? `${b.index} ` : "";
1925
+ const status = activityCell(b);
1926
+ const left = `${marker}${numStr}`;
1927
+ const room = w - left.length - status.length - 1;
1928
+ if (room <= 0) return fit(`${left}${status}`, w);
1929
+ const middle = b.state === "running" && b.sessionTitle ? ellipsize(stripTitleGlyph(b.sessionTitle), room) : ellipsizeHead(b.name, room);
1930
+ return fit(`${left}${middle}`, w - status.length) + status;
1931
+ }
1892
1932
  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));
1933
+ const lines = [topBorder(SIDEBAR_HEADER, w), fit("", w)];
1934
+ const rowOwner = [null, null];
1935
+ const headerRows = [true, false];
1936
+ const push = (line, owner, header) => {
1937
+ lines.push(fit(line, w));
1938
+ rowOwner.push(owner);
1939
+ headerRows.push(header);
1940
+ };
1941
+ let prevProject;
1942
+ let seenGroup = false;
1895
1943
  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));
1944
+ const marker = b.id === selectedId ? "\u25B8" : " ";
1945
+ if (b.id === NEW_BOX_ID) {
1946
+ push(`${marker}${NEW_BOX_LABEL}`, b.id, false);
1947
+ continue;
1948
+ }
1949
+ if (!seenGroup || b.project !== prevProject) {
1950
+ push(center(` \u2500\u2500 ${projectLabel(b.project)} \u2500\u2500 `, w), null, true);
1951
+ prevProject = b.project;
1952
+ seenGroup = true;
1953
+ }
1954
+ push(boxRow(b, marker, w), b.id, false);
1899
1955
  }
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);
1956
+ if (boxes.length === 0) push(" (no boxes)", null, false);
1957
+ while (lines.length < h) push("", null, false);
1958
+ return {
1959
+ lines: lines.slice(0, h),
1960
+ rowOwner: rowOwner.slice(0, h),
1961
+ headerRows: headerRows.slice(0, h)
1962
+ };
1903
1963
  }
1904
1964
  function menuLines(boxName, w, h) {
1905
1965
  const body = [
@@ -1916,6 +1976,28 @@ function menuLines(boxName, w, h) {
1916
1976
  for (let i = 0; i < h; i++) out.push(fit(body[i - top] ?? "", w));
1917
1977
  return out;
1918
1978
  }
1979
+ function lifecycleMenuLines(boxName, state, confirmDestroy, w, h) {
1980
+ const body = confirmDestroy ? [
1981
+ "",
1982
+ ` Destroy ${boxName}?`,
1983
+ " This removes the container and its volumes.",
1984
+ "",
1985
+ " [y] Yes, destroy",
1986
+ " [any other key] Cancel"
1987
+ ] : [
1988
+ "",
1989
+ ` Box ${boxName} is ${state}.`,
1990
+ "",
1991
+ state === "paused" ? " [u] Unpause" : " [s] Start",
1992
+ " [d] Destroy",
1993
+ "",
1994
+ " Ctrl+Option+\u2191/\u2193 switch \xB7 Ctrl-a then q quit"
1995
+ ];
1996
+ const top = Math.max(0, Math.floor((h - body.length) / 2));
1997
+ const out = [];
1998
+ for (let i = 0; i < h; i++) out.push(fit(body[i - top] ?? "", w));
1999
+ return out;
2000
+ }
1919
2001
  function createMenuLines(where, w, h) {
1920
2002
  const body = [
1921
2003
  "",
@@ -1941,29 +2023,58 @@ var BRAND_NOBOLD = "\x1B[22m";
1941
2023
  var HINT_KEY = "\x1B[38;5;255m";
1942
2024
  var HINT_TXT = "\x1B[38;5;245m";
1943
2025
  var BAR_RESET = "\x1B[0m";
2026
+ var SWITCH_HINT = ["Control+Option+\u2191/\u2193", "switch"];
1944
2027
  var HINT_GROUPS = [
1945
- ["Control+Option+Up/Down", "switch"],
2028
+ SWITCH_HINT,
1946
2029
  ["Control+a c", "code"],
1947
2030
  ["Control+a v", "vnc"],
1948
2031
  ["Control+a w", "web"],
1949
2032
  ["Control+a q", "quit"]
1950
2033
  ];
1951
- function statusLine(box, w, stateLabel) {
2034
+ var COLLAPSED_HINT_GROUPS = [
2035
+ SWITCH_HINT,
2036
+ ["Control+a", "more"]
2037
+ ];
2038
+ var ADVANCED_HINT_GROUPS = [
2039
+ ["c", "code"],
2040
+ ["v", "vnc"],
2041
+ ["w", "web"],
2042
+ ["s", "stop"],
2043
+ ["p", "pause"],
2044
+ ["d", "destroy"],
2045
+ ["q", "quit"]
2046
+ ];
2047
+ function statusLine(box, w, stateLabel, groups = HINT_GROUPS) {
1952
2048
  const state = stateLabel ?? (box ? box.state === "running" ? box.claudeActivity ?? "unknown" : box.state : "");
1953
2049
  const brandPrefix = box ? " agentbox \u25B8 " : " agentbox ";
1954
- const brandMain = box ? `${box.name} (${state}) ` : "";
1955
- const left = brandPrefix + brandMain;
1956
- const leftStyled = BAR_BRAND + brandPrefix + BRAND_BOLD + brandMain + BRAND_NOBOLD;
2050
+ const base = box ? `${box.name} (${state})` : "";
2051
+ const coreMain = box ? `${base} ` : "";
2052
+ const corePlain = brandPrefix + coreMain;
1957
2053
  const SEP = " \u2502 ";
1958
- const rightPlain = HINT_GROUPS.map(([k, l]) => `${k}: ${l}`).join(SEP) + " ";
1959
- const rightStyled = HINT_GROUPS.map(([k, l]) => `${HINT_KEY}${k}${HINT_TXT}: ${l}`).join(
1960
- `${HINT_TXT}${SEP}`
1961
- ) + " ";
1962
- if (left.length + rightPlain.length + 1 > w) {
1963
- return BAR_BASE + BAR_BRAND + fit(left, w) + BAR_RESET;
1964
- }
1965
- const gap = w - left.length - rightPlain.length;
1966
- return BAR_BASE + leftStyled + BAR_BASE + " ".repeat(gap) + rightStyled + BAR_RESET;
2054
+ const renderHints = (g) => ({
2055
+ plain: g.map(([k, l]) => `${k}: ${l}`).join(SEP) + " ",
2056
+ styled: g.map(([k, l]) => `${HINT_KEY}${k}${HINT_TXT}: ${l}`).join(`${HINT_TXT}${SEP}`) + " "
2057
+ });
2058
+ let hints = null;
2059
+ for (const g of [groups, COLLAPSED_HINT_GROUPS]) {
2060
+ const h = renderHints(g);
2061
+ if (corePlain.length + h.plain.length + 1 <= w) {
2062
+ hints = h;
2063
+ break;
2064
+ }
2065
+ }
2066
+ if (!hints) {
2067
+ return BAR_BASE + BAR_BRAND + fit(corePlain, w) + BAR_RESET;
2068
+ }
2069
+ const room = w - corePlain.length - hints.plain.length - 1;
2070
+ let titleSeg = "";
2071
+ if (box?.sessionTitle && room >= 7) {
2072
+ titleSeg = ` \u2014 ${ellipsize(box.sessionTitle, Math.min(40, room - 3))}`;
2073
+ }
2074
+ const leftPlain = brandPrefix + base + titleSeg + (box ? " " : "");
2075
+ const leftStyled = BAR_BRAND + brandPrefix + BRAND_BOLD + base + titleSeg + (box ? " " : "") + BRAND_NOBOLD;
2076
+ const gap = w - leftPlain.length - hints.plain.length;
2077
+ return BAR_BASE + leftStyled + BAR_BASE + " ".repeat(gap) + hints.styled + BAR_RESET;
1967
2078
  }
1968
2079
 
1969
2080
  // src/dashboard/compositor.ts
@@ -1974,6 +2085,7 @@ var SGR_RESET = "\x1B[0m";
1974
2085
  var POLL_MS = 1e3;
1975
2086
  var FRAME_MS = 16;
1976
2087
  var RESIZE_DEBOUNCE_MS = 120;
2088
+ var LEADER_LINGER_MS = 1500;
1977
2089
  var SYNC_BEGIN = "\x1B[?2026h";
1978
2090
  var SYNC_END = "\x1B[?2026l";
1979
2091
  function cursorTo2(x, y) {
@@ -1986,10 +2098,41 @@ var Compositor = class {
1986
2098
  this.layout = computeLayout(this.out.columns ?? 100, this.out.rows ?? 30);
1987
2099
  this.parser = new InputParser({
1988
2100
  onEvent: (e) => {
2101
+ if (e.type === "leader") {
2102
+ if (this.leaderLingerTimer) {
2103
+ clearTimeout(this.leaderLingerTimer);
2104
+ this.leaderLingerTimer = null;
2105
+ }
2106
+ if (e.active) {
2107
+ this.leaderActive = true;
2108
+ } else {
2109
+ this.leaderLingerTimer = setTimeout(() => {
2110
+ this.leaderLingerTimer = null;
2111
+ this.leaderActive = false;
2112
+ this.drawChrome();
2113
+ }, LEADER_LINGER_MS);
2114
+ }
2115
+ this.drawChrome();
2116
+ return;
2117
+ }
2118
+ if (this.pendingConfirm) {
2119
+ if (e.type === "forward") {
2120
+ this.handleConfirmKey(e.bytes);
2121
+ return;
2122
+ }
2123
+ this.pendingConfirm = null;
2124
+ this.drawChrome();
2125
+ }
1989
2126
  if (e.type === "quit") this.onSig();
1990
2127
  else if (e.type === "switch") this.switchBox(e.dir);
1991
- else if (e.type === "action") void this.doAction(e.name);
1992
- else if (this.createMenu) this.handleCreateMenuKey(e.bytes);
2128
+ else if (e.type === "action") {
2129
+ if (e.name === "pause" || e.name === "stop" || e.name === "destroy") {
2130
+ void this.doLifecycle(e.name);
2131
+ } else {
2132
+ void this.doAction(e.name);
2133
+ }
2134
+ } else if (this.createMenu) this.handleCreateMenuKey(e.bytes);
2135
+ else if (this.lifecycleMenu) this.handleLifecycleMenuKey(e.bytes);
1993
2136
  else if (this.menu) this.handleMenuKey(e.bytes);
1994
2137
  else this.session?.write(e.bytes);
1995
2138
  },
@@ -2013,7 +2156,16 @@ var Compositor = class {
2013
2156
  session = null;
2014
2157
  placeholder = null;
2015
2158
  menu = null;
2159
+ lifecycleMenu = null;
2016
2160
  createMenu = null;
2161
+ /** True while the Ctrl-a leader is pending — swaps the footer to the
2162
+ * expanded chord menu (chrome only; never touches the right pane). */
2163
+ leaderActive = false;
2164
+ /** Holds the expanded footer for LEADER_LINGER_MS after the leader resolves
2165
+ * (so the chord menu doesn't flash by). */
2166
+ leaderLingerTimer = null;
2167
+ /** Set while a destroy confirm is pending in the status bar. */
2168
+ pendingConfirm = null;
2017
2169
  activeMode = "claude";
2018
2170
  flashMsg = null;
2019
2171
  flashTimer = null;
@@ -2073,7 +2225,9 @@ var Compositor = class {
2073
2225
  return this.boxes.find((b) => b.id === this.selectedId);
2074
2226
  }
2075
2227
  async poll() {
2076
- const before = JSON.stringify(this.boxes.map((b) => [b.id, b.state, b.claudeActivity]));
2228
+ const before = JSON.stringify(
2229
+ this.boxes.map((b) => [b.id, b.state, b.claudeActivity, b.sessionTitle])
2230
+ );
2077
2231
  await this.refreshBoxes();
2078
2232
  if (this.busy) {
2079
2233
  } else if (!this.boxes.some((b) => b.id === this.selectedId) && this.boxes[0]) {
@@ -2082,10 +2236,12 @@ var Compositor = class {
2082
2236
  } else {
2083
2237
  const box = this.selectedBox();
2084
2238
  const running = box?.state === "running";
2085
- const reresolve = this.session && !running || this.placeholder && running || this.menu && !running;
2239
+ const reresolve = this.session && !running || this.placeholder && running || this.menu && !running || this.lifecycleMenu != null && box?.state !== this.lifecycleMenu.state;
2086
2240
  if (reresolve) await this.spawnActive();
2087
2241
  }
2088
- if (JSON.stringify(this.boxes.map((b) => [b.id, b.state, b.claudeActivity])) !== before) {
2242
+ if (JSON.stringify(
2243
+ this.boxes.map((b) => [b.id, b.state, b.claudeActivity, b.sessionTitle])
2244
+ ) !== before) {
2089
2245
  this.drawChrome();
2090
2246
  }
2091
2247
  }
@@ -2098,7 +2254,9 @@ var Compositor = class {
2098
2254
  this.disposeSession();
2099
2255
  this.placeholder = null;
2100
2256
  this.menu = null;
2257
+ this.lifecycleMenu = null;
2101
2258
  this.createMenu = null;
2259
+ this.pendingConfirm = null;
2102
2260
  this.clearRightPane();
2103
2261
  const id = this.selectedId;
2104
2262
  const target = await this.deps.resolveTarget(id);
@@ -2110,7 +2268,9 @@ var Compositor = class {
2110
2268
  this.disposeSession();
2111
2269
  this.placeholder = null;
2112
2270
  this.menu = null;
2271
+ this.lifecycleMenu = null;
2113
2272
  this.createMenu = null;
2273
+ this.pendingConfirm = null;
2114
2274
  if (target.kind === "attach") {
2115
2275
  this.activeMode = target.mode ?? "claude";
2116
2276
  this.session = new PtySession(
@@ -2124,6 +2284,12 @@ var Compositor = class {
2124
2284
  );
2125
2285
  } else if (target.kind === "menu") {
2126
2286
  this.menu = { boxName: this.selectedBox()?.name ?? this.selectedId };
2287
+ } else if (target.kind === "lifecycle-menu") {
2288
+ this.lifecycleMenu = {
2289
+ boxName: this.selectedBox()?.name ?? this.selectedId,
2290
+ state: target.state,
2291
+ confirmDestroy: false
2292
+ };
2127
2293
  } else if (target.kind === "create-menu") {
2128
2294
  this.createMenu = { where: target.where };
2129
2295
  } else {
@@ -2176,6 +2342,163 @@ var Compositor = class {
2176
2342
  this.busy = false;
2177
2343
  }
2178
2344
  }
2345
+ handleLifecycleMenuKey(bytes) {
2346
+ const m = this.lifecycleMenu;
2347
+ if (!m) return;
2348
+ for (const b of bytes) {
2349
+ if (m.confirmDestroy) {
2350
+ if (b === 121 || b === 13 || b === 10) {
2351
+ void this.runDestroy(this.selectedId, this.selectedBox()?.name ?? this.selectedId);
2352
+ } else {
2353
+ m.confirmDestroy = false;
2354
+ this.drawChrome();
2355
+ this.scheduleRender();
2356
+ }
2357
+ return;
2358
+ }
2359
+ const resumeKey = m.state === "paused" ? 117 : 115;
2360
+ if (b === resumeKey) {
2361
+ void this.resumeSelected();
2362
+ return;
2363
+ }
2364
+ if (b === 100) {
2365
+ m.confirmDestroy = true;
2366
+ this.drawChrome();
2367
+ this.scheduleRender();
2368
+ return;
2369
+ }
2370
+ }
2371
+ }
2372
+ async resumeSelected() {
2373
+ if (this.busy) return;
2374
+ const id = this.selectedId;
2375
+ const name = this.selectedBox()?.name ?? id;
2376
+ const verb = this.lifecycleMenu?.state === "stopped" ? "start" : "unpause";
2377
+ this.busy = true;
2378
+ this.menu = null;
2379
+ this.lifecycleMenu = null;
2380
+ this.createMenu = null;
2381
+ this.placeholder = ["", " Resuming\u2026"];
2382
+ this.prevRows = null;
2383
+ this.drawChrome();
2384
+ this.scheduleRender();
2385
+ try {
2386
+ await this.deps.resumeBox(id);
2387
+ if (this.selectedId !== id || this.tornDown) return;
2388
+ await this.refreshBoxes();
2389
+ await this.spawnActive();
2390
+ } catch (err) {
2391
+ if (this.selectedId !== id || this.tornDown) return;
2392
+ const msg = err instanceof Error ? err.message : String(err);
2393
+ this.placeholder = [
2394
+ "",
2395
+ ` Failed to ${verb} ${name}:`,
2396
+ ` ${msg}`,
2397
+ "",
2398
+ ` Try from a shell: agentbox ${verb} ${name}`
2399
+ ];
2400
+ this.prevRows = null;
2401
+ this.scheduleRender();
2402
+ } finally {
2403
+ this.busy = false;
2404
+ }
2405
+ }
2406
+ /** Destroy `id` and recover the selection. Shared by the lifecycle-menu
2407
+ * confirm and the running-box `Ctrl-a d` status-bar confirm. */
2408
+ async runDestroy(id, name) {
2409
+ if (this.busy) return;
2410
+ this.busy = true;
2411
+ this.menu = null;
2412
+ this.lifecycleMenu = null;
2413
+ this.createMenu = null;
2414
+ this.placeholder = ["", " Destroying\u2026"];
2415
+ this.prevRows = null;
2416
+ this.drawChrome();
2417
+ this.scheduleRender();
2418
+ try {
2419
+ await this.deps.destroyBox(id);
2420
+ if (this.tornDown) return;
2421
+ await this.refreshBoxes();
2422
+ if (this.boxes[0]) this.selectedId = this.boxes[0].id;
2423
+ await this.spawnActive();
2424
+ this.flash(`destroyed ${name}`);
2425
+ } catch (err) {
2426
+ if (this.tornDown) return;
2427
+ const msg = err instanceof Error ? err.message : String(err);
2428
+ this.placeholder = [
2429
+ "",
2430
+ ` Failed to destroy ${name}:`,
2431
+ ` ${msg}`,
2432
+ "",
2433
+ ` Try from a shell: agentbox destroy ${name}`
2434
+ ];
2435
+ this.prevRows = null;
2436
+ this.scheduleRender();
2437
+ } finally {
2438
+ this.busy = false;
2439
+ }
2440
+ }
2441
+ /** Ctrl-a p/s/d on the selected box. pause/stop transition state (the pane
2442
+ * re-resolves to the lifecycle menu); destroy asks to confirm first. */
2443
+ async doLifecycle(name) {
2444
+ if (this.selectedId === NEW_BOX_ID) {
2445
+ this.flash("select a box first");
2446
+ return;
2447
+ }
2448
+ const id = this.selectedId;
2449
+ const boxName = this.selectedBox()?.name ?? id;
2450
+ if (name === "destroy") {
2451
+ this.pendingConfirm = { boxId: id, name: boxName };
2452
+ this.drawChrome();
2453
+ return;
2454
+ }
2455
+ if (this.selectedBox()?.state !== "running") {
2456
+ this.flash(`${boxName} is not running`);
2457
+ return;
2458
+ }
2459
+ if (this.busy) return;
2460
+ this.busy = true;
2461
+ this.menu = null;
2462
+ this.lifecycleMenu = null;
2463
+ this.createMenu = null;
2464
+ this.placeholder = ["", name === "pause" ? " Pausing\u2026" : " Stopping\u2026"];
2465
+ this.prevRows = null;
2466
+ this.drawChrome();
2467
+ this.scheduleRender();
2468
+ try {
2469
+ if (name === "pause") await this.deps.pauseBox(id);
2470
+ else await this.deps.stopBox(id);
2471
+ if (this.selectedId !== id || this.tornDown) return;
2472
+ await this.refreshBoxes();
2473
+ await this.spawnActive();
2474
+ this.flash(`${name === "pause" ? "paused" : "stopped"} ${boxName}`);
2475
+ } catch (err) {
2476
+ if (this.selectedId !== id || this.tornDown) return;
2477
+ const msg = err instanceof Error ? err.message : String(err);
2478
+ this.placeholder = [
2479
+ "",
2480
+ ` Failed to ${name} ${boxName}:`,
2481
+ ` ${msg}`,
2482
+ "",
2483
+ ` Try from a shell: agentbox ${name} ${boxName}`
2484
+ ];
2485
+ this.prevRows = null;
2486
+ this.scheduleRender();
2487
+ } finally {
2488
+ this.busy = false;
2489
+ }
2490
+ }
2491
+ handleConfirmKey(bytes) {
2492
+ const c = this.pendingConfirm;
2493
+ if (!c) return;
2494
+ const b = bytes[0];
2495
+ this.pendingConfirm = null;
2496
+ if (b === 121 || b === 13 || b === 10) {
2497
+ void this.runDestroy(c.boxId, c.name);
2498
+ } else {
2499
+ this.drawChrome();
2500
+ }
2501
+ }
2179
2502
  handleCreateMenuKey(bytes) {
2180
2503
  for (const b of bytes) {
2181
2504
  if (b === 99 || b === 13 || b === 10) {
@@ -2257,6 +2580,7 @@ var Compositor = class {
2257
2580
  }
2258
2581
  switchBox(dir) {
2259
2582
  if (this.boxes.length === 0) return;
2583
+ this.pendingConfirm = null;
2260
2584
  const i = Math.max(
2261
2585
  0,
2262
2586
  this.boxes.findIndex((b) => b.id === this.selectedId)
@@ -2300,6 +2624,17 @@ var Compositor = class {
2300
2624
  let s = SYNC_BEGIN + "\x1B[?25l";
2301
2625
  for (let i = 0; i < r.h; i++) s += cursorTo2(r.x, r.y + i) + "\x1B[0m" + (lines[i] ?? "");
2302
2626
  this.out.write(s + SYNC_END);
2627
+ } else if (this.lifecycleMenu) {
2628
+ const lines = lifecycleMenuLines(
2629
+ this.lifecycleMenu.boxName,
2630
+ this.lifecycleMenu.state,
2631
+ this.lifecycleMenu.confirmDestroy,
2632
+ r.w,
2633
+ r.h
2634
+ );
2635
+ let s = SYNC_BEGIN + "\x1B[?25l";
2636
+ for (let i = 0; i < r.h; i++) s += cursorTo2(r.x, r.y + i) + "\x1B[0m" + (lines[i] ?? "");
2637
+ this.out.write(s + SYNC_END);
2303
2638
  } else if (this.createMenu) {
2304
2639
  const lines = createMenuLines(this.createMenu.where, r.w, r.h);
2305
2640
  let s = SYNC_BEGIN + "\x1B[?25l";
@@ -2317,23 +2652,36 @@ var Compositor = class {
2317
2652
  drawChrome() {
2318
2653
  if (this.tornDown || this.layout.tooSmall) return;
2319
2654
  const { sidebar, sepX, statusY } = this.layout;
2320
- const lines = sidebarLines(this.boxes, this.selectedId, sidebar.w, sidebar.h);
2321
- const selIdx = this.boxes.findIndex((b) => b.id === this.selectedId);
2322
- const selRow = selIdx >= 0 ? SIDEBAR_HEADER_LINES + selIdx : -1;
2655
+ const { lines, rowOwner, headerRows } = sidebarLines(
2656
+ this.boxes,
2657
+ this.selectedId,
2658
+ sidebar.w,
2659
+ sidebar.h
2660
+ );
2323
2661
  let s = SYNC_BEGIN + "\x1B[0m";
2324
2662
  for (let i = 0; i < lines.length; i++) {
2325
- const style = i === 0 ? SB_HEADER : i === selRow ? SB_SELECTED : SB_BODY;
2663
+ const style = headerRows[i] ? SB_HEADER : rowOwner[i] === this.selectedId ? SB_SELECTED : SB_BODY;
2326
2664
  s += cursorTo2(0, i) + style + lines[i] + SGR_RESET;
2327
2665
  }
2328
- for (let y = 0; y < sidebar.h; y++) s += cursorTo2(sepX, y) + "\u2502";
2666
+ for (let y = 0; y < sidebar.h; y++)
2667
+ s += cursorTo2(sepX, y) + SB_HEADER + (y === 0 ? "\u256E" : "\u2502") + SGR_RESET;
2329
2668
  let status;
2330
- if (this.flashMsg) {
2669
+ if (this.pendingConfirm) {
2670
+ const w = this.layout.cols;
2671
+ const txt = ` Destroy ${this.pendingConfirm.name}? y = confirm \xB7 any other key = cancel `.slice(0, w).padEnd(w);
2672
+ status = `\x1B[7m${txt}\x1B[0m`;
2673
+ } else if (this.flashMsg) {
2331
2674
  const w = this.layout.cols;
2332
2675
  const txt = ` ${this.flashMsg} `.slice(0, w).padEnd(w);
2333
2676
  status = `\x1B[7m${txt}\x1B[0m`;
2334
2677
  } else {
2335
2678
  const stateLabel = this.selectedId === NEW_BOX_ID ? "create" : this.menu ? "menu" : this.session && this.activeMode === "shell" ? "shell" : void 0;
2336
- status = statusLine(this.selectedBox(), this.layout.cols, stateLabel);
2679
+ status = statusLine(
2680
+ this.selectedBox(),
2681
+ this.layout.cols,
2682
+ stateLabel,
2683
+ this.leaderActive ? ADVANCED_HINT_GROUPS : void 0
2684
+ );
2337
2685
  }
2338
2686
  s += cursorTo2(0, statusY) + status;
2339
2687
  this.out.write(s + SYNC_END);
@@ -2360,6 +2708,7 @@ var Compositor = class {
2360
2708
  if (this.pollTimer) clearInterval(this.pollTimer);
2361
2709
  if (this.resizeTimer) clearTimeout(this.resizeTimer);
2362
2710
  if (this.flashTimer) clearTimeout(this.flashTimer);
2711
+ if (this.leaderLingerTimer) clearTimeout(this.leaderLingerTimer);
2363
2712
  this.parser.dispose();
2364
2713
  this.disposeSession();
2365
2714
  this.inp.off("data", this.onData);
@@ -2387,7 +2736,15 @@ function scoped(all, projectRoot, boxes) {
2387
2736
  return sortBoxes(all ? boxes : boxes.filter((b) => b.projectRoot === projectRoot));
2388
2737
  }
2389
2738
  function toSidebar(b) {
2390
- return { id: b.id, name: b.name, state: b.state, claudeActivity: b.claudeActivity };
2739
+ return {
2740
+ id: b.id,
2741
+ name: b.name,
2742
+ state: b.state,
2743
+ claudeActivity: b.claudeActivity,
2744
+ sessionTitle: b.claudeSessionTitle,
2745
+ index: b.projectIndex,
2746
+ project: b.projectRoot
2747
+ };
2391
2748
  }
2392
2749
  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) => {
2393
2750
  try {
@@ -2437,6 +2794,9 @@ var dashboardCommand = new Command7("dashboard").description("Box list + the sel
2437
2794
  if (boxId === NEW_BOX_ID) return { kind: "create-menu", where: project.root };
2438
2795
  const box = (await listBoxes()).find((b) => b.id === boxId);
2439
2796
  if (!box) return { kind: "placeholder", lines: ["", " box not found"] };
2797
+ if (box.state === "paused" || box.state === "stopped") {
2798
+ return { kind: "lifecycle-menu", state: box.state };
2799
+ }
2440
2800
  if (box.state !== "running") {
2441
2801
  return {
2442
2802
  kind: "placeholder",
@@ -2552,6 +2912,21 @@ var dashboardCommand = new Command7("dashboard").description("Box list + the sel
2552
2912
  detach(process.execPath, [process.argv[1], "code", box.name, "--no-wait"]);
2553
2913
  return "Launching VS Code / Cursor\u2026";
2554
2914
  };
2915
+ const resumeBox = async (boxId) => {
2916
+ const box = (await listBoxes()).find((b) => b.id === boxId);
2917
+ if (!box) throw new Error("box not found");
2918
+ if (box.state === "paused") await unpauseBox(box.id);
2919
+ else await startBox(box.id);
2920
+ };
2921
+ const pauseBoxAction = async (boxId) => {
2922
+ await pauseBox(boxId);
2923
+ };
2924
+ const stopBoxAction = async (boxId) => {
2925
+ await stopBox(boxId);
2926
+ };
2927
+ const destroyBoxAction = async (boxId) => {
2928
+ await destroyBox(boxId);
2929
+ };
2555
2930
  const compositor = new Compositor(
2556
2931
  {
2557
2932
  ptySpawn,
@@ -2561,6 +2936,10 @@ var dashboardCommand = new Command7("dashboard").description("Box list + the sel
2561
2936
  startClaude,
2562
2937
  openShell,
2563
2938
  createNewBox,
2939
+ resumeBox,
2940
+ pauseBox: pauseBoxAction,
2941
+ stopBox: stopBoxAction,
2942
+ destroyBox: destroyBoxAction,
2564
2943
  openVnc,
2565
2944
  openCode,
2566
2945
  openWeb
@@ -2745,28 +3124,40 @@ function renderTable(boxes, stream) {
2745
3124
  (row2) => row2.map((cell, i) => padCell(cell ?? plain(""), i)).join(" ").trimEnd()
2746
3125
  ).join("\n");
2747
3126
  }
2748
- async function buildListText() {
3127
+ async function scopedBoxes(all) {
2749
3128
  const boxes = await listBoxes();
2750
- if (boxes.length === 0) return "no boxes \u2014 run `agentbox create` to make one";
3129
+ if (all) return { boxes, projectRoot: "", scoped: false };
3130
+ const { root } = await findProjectRoot(process.cwd());
3131
+ return { boxes: boxes.filter((b) => b.projectRoot === root), projectRoot: root, scoped: true };
3132
+ }
3133
+ async function buildListText(all) {
3134
+ const { boxes, projectRoot, scoped: scoped2 } = await scopedBoxes(all);
3135
+ if (boxes.length === 0) {
3136
+ if (scoped2) {
3137
+ return `no boxes in this project (${projectRoot}) \u2014 run \`agentbox create\`, or \`agentbox list --all\` to see all`;
3138
+ }
3139
+ return "no boxes \u2014 run `agentbox create` to make one";
3140
+ }
2751
3141
  return renderTable(boxes, process.stdout);
2752
3142
  }
2753
3143
  var listCommand2 = withWatchOptions(
2754
- new Command9("list").alias("ls").description("List all known agent boxes").option("-j, --json", "machine-readable JSON output")
3144
+ new Command9("list").alias("ls").description("List agent boxes in the current project (-a for all)").option("-j, --json", "machine-readable JSON output").option("-a, --all", "include boxes from all projects")
2755
3145
  ).action(async (opts) => {
2756
3146
  if (opts.json && opts.watch) {
2757
3147
  log11.error("cannot combine --json with --watch");
2758
3148
  process.exit(2);
2759
3149
  }
3150
+ const all = opts.all ?? false;
2760
3151
  if (opts.watch) {
2761
- await watchRender(buildListText, opts.interval);
3152
+ await watchRender(() => buildListText(all), opts.interval);
2762
3153
  return;
2763
3154
  }
2764
3155
  if (opts.json) {
2765
- const boxes = await listBoxes();
3156
+ const { boxes } = await scopedBoxes(all);
2766
3157
  process.stdout.write(JSON.stringify(boxes, null, 2) + "\n");
2767
3158
  return;
2768
3159
  }
2769
- process.stdout.write(await buildListText() + "\n");
3160
+ process.stdout.write(await buildListText(all) + "\n");
2770
3161
  });
2771
3162
 
2772
3163
  // src/commands/logs.ts