@wrongstack/tui 0.1.10 → 0.2.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,4 +1,4 @@
1
- import { render, useApp, Box, useStdout, Static, Text, useInput } from 'ink';
1
+ import { render, useApp, Box, useStdout, Static, Text, useInput, useStdin } from 'ink';
2
2
  import React, { useState, useReducer, useRef, useEffect, useMemo } from 'react';
3
3
  import * as fs from 'fs/promises';
4
4
  import * as path3 from 'path';
@@ -217,6 +217,159 @@ function FilePicker({ query, matches, selected }) {
217
217
  function highlight(path4, query) {
218
218
  return path4;
219
219
  }
220
+ var STATUS_ICON = {
221
+ idle: { icon: "\u25CB", color: "gray" },
222
+ running: { icon: "\u25CF", color: "green" },
223
+ success: { icon: "\u2713", color: "green" },
224
+ failed: { icon: "\u2717", color: "red" },
225
+ timeout: { icon: "\u23F1", color: "yellow" },
226
+ stopped: { icon: "\u2298", color: "yellow" }
227
+ };
228
+ function fmtCost(n) {
229
+ if (n === 0) return "\u2014";
230
+ return `$${n.toFixed(3)}`;
231
+ }
232
+ function fmtCount(n) {
233
+ if (n === 0) return "\u2014";
234
+ return String(n);
235
+ }
236
+ function fmtModel(provider, model) {
237
+ if (!provider && !model) return "";
238
+ const p = provider ?? "";
239
+ const m = model ?? "";
240
+ return p && m ? `${p}/${m}` : p || m;
241
+ }
242
+ function resolveName(entry, roster) {
243
+ const rosterEntry = roster?.[entry.id];
244
+ if (rosterEntry) return rosterEntry.name;
245
+ return entry.name;
246
+ }
247
+ function FleetPanel({ entries, totalCost, roster }) {
248
+ const list = Object.values(entries);
249
+ if (list.length === 0) return null;
250
+ const sorted = [...list].sort((a, b) => {
251
+ const order = { running: 0, success: 1, failed: 2, timeout: 3, stopped: 4, idle: 5 };
252
+ const ao = order[a.status] ?? 9;
253
+ const bo = order[b.status] ?? 9;
254
+ if (ao !== bo) return ao - bo;
255
+ return b.lastEventAt - a.lastEventAt;
256
+ });
257
+ const runningCount = list.filter((e) => e.status === "running").length;
258
+ const totalLabel = totalCost > 0 ? `$${totalCost.toFixed(3)} \xB7 ${runningCount} active` : `${runningCount} active`;
259
+ return /* @__PURE__ */ jsxs(
260
+ Box,
261
+ {
262
+ flexDirection: "column",
263
+ paddingX: 1,
264
+ borderStyle: "single",
265
+ borderTop: false,
266
+ borderBottom: false,
267
+ borderLeft: false,
268
+ borderRight: false,
269
+ children: [
270
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, children: [
271
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Fleet" }),
272
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
273
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
274
+ list.length,
275
+ " agent",
276
+ list.length === 1 ? "" : "s"
277
+ ] }),
278
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
279
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: totalLabel })
280
+ ] }),
281
+ sorted.map((entry) => {
282
+ const si = STATUS_ICON[entry.status];
283
+ const modelTag = fmtModel(entry.provider, entry.model);
284
+ const name = resolveName(entry, roster);
285
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
286
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
287
+ /* @__PURE__ */ jsx(Text, { color: si.color, children: si.icon }),
288
+ /* @__PURE__ */ jsx(Text, { children: name.slice(0, 16).padEnd(16) }),
289
+ modelTag ? /* @__PURE__ */ jsxs(Fragment, { children: [
290
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
291
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: modelTag })
292
+ ] }) : null,
293
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
294
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
295
+ fmtCount(entry.iterations).padStart(3),
296
+ "it"
297
+ ] }),
298
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
299
+ fmtCount(entry.toolCalls).padStart(3),
300
+ "tc"
301
+ ] }),
302
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
303
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: fmtCost(entry.cost) })
304
+ ] }),
305
+ entry.status === "running" && entry.currentTool ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, children: [
306
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
307
+ "\u2192 ",
308
+ entry.currentTool.name
309
+ ] }),
310
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
311
+ " (",
312
+ Math.max(0, Date.now() - entry.currentTool.startedAt),
313
+ "ms)"
314
+ ] })
315
+ ] }) : null,
316
+ entry.status === "running" && entry.streamingText ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
317
+ ">",
318
+ " ",
319
+ entry.streamingText.slice(-80)
320
+ ] }) }) : null,
321
+ entry.transcriptPath ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
322
+ "log: ",
323
+ entry.transcriptPath
324
+ ] }) }) : null
325
+ ] }, entry.id);
326
+ })
327
+ ]
328
+ }
329
+ );
330
+ }
331
+ function fmtElapsed(ms) {
332
+ if (ms < 1e3) return `${ms}ms`;
333
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
334
+ const m = Math.floor(ms / 6e4);
335
+ const s = Math.floor(ms % 6e4 / 1e3);
336
+ return `${m}m${s.toString().padStart(2, "0")}s`;
337
+ }
338
+ function LiveActivityStrip({
339
+ entries,
340
+ nowTick,
341
+ maxRows = 4
342
+ }) {
343
+ const running = Object.values(entries).filter((e) => e.status === "running").sort((a, b) => a.startedAt - b.startedAt).slice(0, maxRows);
344
+ if (running.length === 0) return null;
345
+ const now = Date.now();
346
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
347
+ running.map((e) => {
348
+ const toolElapsed = e.currentTool ? now - e.currentTool.startedAt : 0;
349
+ const taskElapsed = now - e.startedAt;
350
+ const toolSeg = e.currentTool ? `\u2192 ${e.currentTool.name} (${fmtElapsed(toolElapsed)})` : "idle between tools";
351
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
352
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u25CF" }),
353
+ /* @__PURE__ */ jsx(Text, { children: e.name.slice(0, 14).padEnd(14) }),
354
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
355
+ /* @__PURE__ */ jsx(Text, { color: e.currentTool ? "green" : "yellow", children: toolSeg }),
356
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
357
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
358
+ e.iterations,
359
+ "it ",
360
+ e.toolCalls,
361
+ "tc \xB7 ",
362
+ fmtElapsed(taskElapsed)
363
+ ] })
364
+ ] }, e.id);
365
+ }),
366
+ Object.values(entries).filter((e) => e.status === "running").length > maxRows ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
367
+ "\u2026+",
368
+ Object.values(entries).filter((e) => e.status === "running").length - maxRows,
369
+ " more"
370
+ ] }) }) : null
371
+ ] });
372
+ }
220
373
 
221
374
  // src/markdown-table.ts
222
375
  function renderMarkdownTables(text, maxWidth) {
@@ -596,6 +749,29 @@ function Entry({
596
749
  );
597
750
  case "banner":
598
751
  return /* @__PURE__ */ jsx(Banner, { entry });
752
+ case "subagent": {
753
+ const lines = entry.text.split("\n");
754
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
755
+ /* @__PURE__ */ jsxs(Text, { children: [
756
+ /* @__PURE__ */ jsx(Text, { color: entry.agentColor, bold: true, children: `[${entry.agentLabel}]` }),
757
+ /* @__PURE__ */ jsx(Text, { children: " " }),
758
+ /* @__PURE__ */ jsx(Text, { color: entry.agentColor, children: entry.icon }),
759
+ /* @__PURE__ */ jsx(Text, { children: " " }),
760
+ /* @__PURE__ */ jsx(Text, { children: lines[0] ?? "" }),
761
+ entry.detail ? /* @__PURE__ */ jsxs(Fragment, { children: [
762
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
763
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.detail })
764
+ ] }) : null
765
+ ] }),
766
+ lines.slice(1).map((line, i) => (
767
+ // biome-ignore lint/suspicious/noArrayIndexKey: stable line index
768
+ /* @__PURE__ */ jsxs(Text, { children: [
769
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
770
+ /* @__PURE__ */ jsx(Text, { children: line })
771
+ ] }, i)
772
+ ))
773
+ ] });
774
+ }
599
775
  }
600
776
  }
601
777
  function Banner({
@@ -1120,8 +1296,51 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
1120
1296
  const lastLine = lines[lines.length - 1];
1121
1297
  return lastLine ? [head, `"${truncMid(lastLine.trim(), 70)}"`] : [head];
1122
1298
  }
1123
- const firstLine = text.split("\n").find((l) => l.trim()) ?? text;
1124
- return [truncMid(firstLine.replace(/\s+/g, " "), OUT_BUDGET)];
1299
+ if (json && typeof json === "object" && !Array.isArray(json)) {
1300
+ const summary = summarizeJsonObject(json);
1301
+ if (summary) return [summary];
1302
+ }
1303
+ const collapsed = text.replace(/\s+/g, " ").trim();
1304
+ return [truncMid(collapsed, GENERIC_BUDGET)];
1305
+ }
1306
+ var GENERIC_BUDGET = 240;
1307
+ function summarizeJsonObject(obj) {
1308
+ const keys = Object.keys(obj);
1309
+ if (keys.length === 0) return null;
1310
+ const priority = [
1311
+ "ok",
1312
+ "status",
1313
+ "timedOut",
1314
+ "stopReason",
1315
+ "reason",
1316
+ "error",
1317
+ "message",
1318
+ "result",
1319
+ "summary",
1320
+ "iterations",
1321
+ "toolCalls",
1322
+ "durationMs",
1323
+ "subagentId",
1324
+ "taskId"
1325
+ ];
1326
+ const ordered = [
1327
+ ...priority.filter((k) => keys.includes(k)),
1328
+ ...keys.filter((k) => !priority.includes(k))
1329
+ ];
1330
+ const parts = [];
1331
+ let used = 0;
1332
+ for (const key of ordered) {
1333
+ const v = obj[key];
1334
+ if (v === void 0 || v === null) continue;
1335
+ const rendered = typeof v === "string" ? `${key}="${truncMid(v.replace(/\s+/g, " "), 80)}"` : typeof v === "number" || typeof v === "boolean" ? `${key}=${v}` : Array.isArray(v) ? `${key}=[${v.length}]` : `${key}={\u2026}`;
1336
+ if (used + rendered.length > GENERIC_BUDGET) {
1337
+ parts.push("\u2026");
1338
+ break;
1339
+ }
1340
+ parts.push(rendered);
1341
+ used += rendered.length + 3;
1342
+ }
1343
+ return parts.length > 0 ? parts.join(" \xB7 ") : null;
1125
1344
  }
1126
1345
  function firstNonEmpty(text) {
1127
1346
  if (!text) return void 0;
@@ -1239,6 +1458,31 @@ function truncMid(s, max) {
1239
1458
  if (s.length <= max) return s;
1240
1459
  return `${s.slice(0, max - 1)}\u2026`;
1241
1460
  }
1461
+ function isHomeEnd(data) {
1462
+ if (data === "\x1B[H" || data === "\x1B[1~" || data === "\x1BOH" || data === "\x1B[7~")
1463
+ return "home";
1464
+ if (data === "\x1B[F" || data === "\x1B[4~" || data === "\x1BOF" || data === "\x1B[8~")
1465
+ return "end";
1466
+ return null;
1467
+ }
1468
+ var EMPTY_KEY = {
1469
+ upArrow: false,
1470
+ downArrow: false,
1471
+ leftArrow: false,
1472
+ rightArrow: false,
1473
+ return: false,
1474
+ escape: false,
1475
+ ctrl: false,
1476
+ meta: false,
1477
+ shift: false,
1478
+ tab: false,
1479
+ backspace: false,
1480
+ delete: false,
1481
+ pageUp: false,
1482
+ pageDown: false,
1483
+ home: false,
1484
+ end: false
1485
+ };
1242
1486
  function Input({
1243
1487
  prompt = "\u203A ",
1244
1488
  value,
@@ -1252,6 +1496,19 @@ function Input({
1252
1496
  if (disabled) return;
1253
1497
  onKey(input, key);
1254
1498
  });
1499
+ const { stdin } = useStdin();
1500
+ useEffect(() => {
1501
+ if (!stdin || disabled) return;
1502
+ const handleData = (data) => {
1503
+ const kind = isHomeEnd(data.toString());
1504
+ if (kind === "home") onKey("", { ...EMPTY_KEY, home: true });
1505
+ else if (kind === "end") onKey("", { ...EMPTY_KEY, end: true });
1506
+ };
1507
+ stdin.on("data", handleData);
1508
+ return () => {
1509
+ stdin.off("data", handleData);
1510
+ };
1511
+ }, [stdin, disabled, onKey]);
1255
1512
  const before = value.slice(0, cursor);
1256
1513
  const at = value.slice(cursor, cursor + 1) || " ";
1257
1514
  const after = value.slice(cursor + 1);
@@ -1348,6 +1605,9 @@ function StatusBar({
1348
1605
  yolo = false,
1349
1606
  elapsedMs,
1350
1607
  todos,
1608
+ plan,
1609
+ fleet,
1610
+ fleetAgents,
1351
1611
  git,
1352
1612
  subagentCount = 0,
1353
1613
  context,
@@ -1358,7 +1618,9 @@ function StatusBar({
1358
1618
  const cache2 = tokenCounter?.cacheStats();
1359
1619
  const stateColor = state === "idle" ? "cyan" : state === "aborting" ? "yellow" : "green";
1360
1620
  const stateLabel = state === "idle" ? "idle" : state === "aborting" ? "aborting\u2026" : "thinking\u2026";
1361
- const hasSecondLine = yolo || elapsedMs !== void 0 || todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) || git !== null && git !== void 0 || subagentCount > 0 || projectName !== void 0 && projectName.length > 0;
1621
+ const hasSecondLine = yolo || elapsedMs !== void 0 || git !== null && git !== void 0 || projectName !== void 0 && projectName.length > 0;
1622
+ const fleetHasActivity = fleet && (fleet.running > 0 || fleet.idle > 0 || fleet.pending > 0 || fleet.completed > 0) || subagentCount > 0;
1623
+ const hasThirdLine = todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) || plan && (plan.open > 0 || plan.inProgress > 0 || plan.done > 0) || fleetHasActivity;
1362
1624
  return /* @__PURE__ */ jsxs(
1363
1625
  Box,
1364
1626
  {
@@ -1425,7 +1687,7 @@ function StatusBar({
1425
1687
  yolo ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }) : null,
1426
1688
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1427
1689
  "\u23F1 ",
1428
- fmtElapsed(elapsedMs)
1690
+ fmtElapsed2(elapsedMs)
1429
1691
  ] })
1430
1692
  ] }) : null,
1431
1693
  projectName ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1455,36 +1717,93 @@ function StatusBar({
1455
1717
  git.untracked
1456
1718
  ] }) : null
1457
1719
  ] })
1720
+ ] }) : null
1721
+ ] }) : null,
1722
+ hasThirdLine ? /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, children: [
1723
+ todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) ? /* @__PURE__ */ jsxs(Text, { children: [
1724
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "todos " }),
1725
+ todos.inProgress > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1726
+ "\u231B",
1727
+ todos.inProgress
1728
+ ] }) : null,
1729
+ todos.inProgress > 0 && (todos.pending > 0 || todos.completed > 0) ? " " : "",
1730
+ todos.pending > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1731
+ "\u2610",
1732
+ todos.pending
1733
+ ] }) : null,
1734
+ todos.pending > 0 && todos.completed > 0 ? " " : "",
1735
+ todos.completed > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1736
+ "\u2713",
1737
+ todos.completed
1738
+ ] }) : null
1458
1739
  ] }) : null,
1459
- todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) ? /* @__PURE__ */ jsxs(Fragment, { children: [
1460
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1740
+ plan && (plan.open > 0 || plan.inProgress > 0 || plan.done > 0) ? /* @__PURE__ */ jsxs(Fragment, { children: [
1741
+ todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }) : null,
1461
1742
  /* @__PURE__ */ jsxs(Text, { children: [
1462
- todos.inProgress > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1463
- "\u231B ",
1464
- todos.inProgress
1743
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u{1F4CB} " }),
1744
+ plan.inProgress > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1745
+ "\u231B",
1746
+ plan.inProgress
1465
1747
  ] }) : null,
1466
- todos.inProgress > 0 && (todos.pending > 0 || todos.completed > 0) ? " " : "",
1467
- todos.pending > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1468
- "\u2610 ",
1469
- todos.pending
1748
+ plan.inProgress > 0 && (plan.open > 0 || plan.done > 0) ? " " : "",
1749
+ plan.open > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1750
+ "\u2610",
1751
+ plan.open
1470
1752
  ] }) : null,
1471
- todos.pending > 0 && todos.completed > 0 ? " " : "",
1472
- todos.completed > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1473
- "\u2713 ",
1474
- todos.completed
1753
+ plan.open > 0 && plan.done > 0 ? " " : "",
1754
+ plan.done > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1755
+ "\u2713",
1756
+ plan.done
1475
1757
  ] }) : null
1476
1758
  ] })
1477
1759
  ] }) : null,
1478
- subagentCount > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1479
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1480
- /* @__PURE__ */ jsxs(Text, { color: "blue", children: [
1760
+ fleetHasActivity ? /* @__PURE__ */ jsxs(Fragment, { children: [
1761
+ todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) || plan && (plan.open > 0 || plan.inProgress > 0 || plan.done > 0) ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }) : null,
1762
+ fleet ? /* @__PURE__ */ jsxs(Text, { children: [
1763
+ /* @__PURE__ */ jsx(Text, { color: "blue", children: "\u{1F310} " }),
1764
+ fleet.running > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1765
+ "\u25B6",
1766
+ fleet.running
1767
+ ] }) : null,
1768
+ fleet.running > 0 && (fleet.pending > 0 || fleet.idle > 0 || fleet.completed > 0) ? " " : "",
1769
+ fleet.pending > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1770
+ "\u2610",
1771
+ fleet.pending
1772
+ ] }) : null,
1773
+ fleet.pending > 0 && (fleet.idle > 0 || fleet.completed > 0) ? " " : "",
1774
+ fleet.idle > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1775
+ "\xB7",
1776
+ fleet.idle,
1777
+ "idle"
1778
+ ] }) : null,
1779
+ fleet.idle > 0 && fleet.completed > 0 ? " " : "",
1780
+ fleet.completed > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1781
+ "\u2713",
1782
+ fleet.completed
1783
+ ] }) : null
1784
+ ] }) : /* @__PURE__ */ jsxs(Text, { color: "blue", children: [
1481
1785
  "\u{1F310} ",
1482
1786
  subagentCount,
1483
1787
  " agent",
1484
1788
  subagentCount === 1 ? "" : "s"
1485
1789
  ] })
1486
1790
  ] }) : null
1487
- ] }) : null
1791
+ ] }) : null,
1792
+ fleetAgents && fleetAgents.length > 0 ? /* @__PURE__ */ jsx(Box, { flexDirection: "row", gap: 2, children: fleetAgents.map((a, i) => (
1793
+ // biome-ignore lint/suspicious/noArrayIndexKey: agent list is stable per render
1794
+ /* @__PURE__ */ jsxs(Text, { children: [
1795
+ /* @__PURE__ */ jsx(Text, { color: a.color, bold: true, children: a.label }),
1796
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
1797
+ /* @__PURE__ */ jsx(Text, { color: a.running ? "yellow" : void 0, dimColor: !a.running, children: a.running ? "\u25B6" : "\xB7" }),
1798
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
1799
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: fmtElapsed2(a.elapsedMs) }),
1800
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
1801
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1802
+ a.toolCalls,
1803
+ "t"
1804
+ ] })
1805
+ ] }, i)
1806
+ )) }) : null
1488
1807
  ]
1489
1808
  }
1490
1809
  );
@@ -1524,7 +1843,7 @@ function fmtTok2(n) {
1524
1843
  if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
1525
1844
  return `${(n / 1e6).toFixed(1)}M`;
1526
1845
  }
1527
- function fmtElapsed(ms) {
1846
+ function fmtElapsed2(ms) {
1528
1847
  const totalSec = Math.floor(ms / 1e3);
1529
1848
  const h = Math.floor(totalSec / 3600);
1530
1849
  const m = Math.floor(totalSec % 3600 / 60);
@@ -1782,6 +2101,10 @@ function reducer(state, action) {
1782
2101
  return { ...state, status: action.status };
1783
2102
  case "interrupt":
1784
2103
  return { ...state, interrupts: state.interrupts + 1 };
2104
+ case "steerStart":
2105
+ return { ...state, steeringPending: true, steerSnapshot: action.snapshot };
2106
+ case "steerConsume":
2107
+ return { ...state, steeringPending: false, steerSnapshot: null };
1785
2108
  case "resetInterrupts":
1786
2109
  return { ...state, interrupts: 0 };
1787
2110
  case "hint":
@@ -1982,9 +2305,234 @@ function reducer(state, action) {
1982
2305
  return { ...state, confirm: null };
1983
2306
  case "resetContextChip":
1984
2307
  return { ...state, contextChipVersion: state.contextChipVersion + 1 };
2308
+ // --- Fleet ---
2309
+ case "fleetSeed": {
2310
+ const seeded = {};
2311
+ for (const e of action.entries) seeded[e.id] = e;
2312
+ return { ...state, fleet: seeded, fleetCost: action.cost };
2313
+ }
2314
+ case "fleetSpawn": {
2315
+ if (state.fleet[action.id]) return state;
2316
+ const entry = {
2317
+ id: action.id,
2318
+ name: action.name ?? action.id.slice(0, 8),
2319
+ provider: action.provider,
2320
+ model: action.model,
2321
+ status: "idle",
2322
+ streamingText: "",
2323
+ iterations: 0,
2324
+ toolCalls: 0,
2325
+ cost: 0,
2326
+ startedAt: Date.now(),
2327
+ lastEventAt: Date.now(),
2328
+ transcriptPath: action.transcriptPath
2329
+ };
2330
+ return { ...state, fleet: { ...state.fleet, [action.id]: entry } };
2331
+ }
2332
+ case "fleetToolStart": {
2333
+ const cur = state.fleet[action.id];
2334
+ if (!cur) return state;
2335
+ return {
2336
+ ...state,
2337
+ fleet: {
2338
+ ...state.fleet,
2339
+ [action.id]: {
2340
+ ...cur,
2341
+ currentTool: { name: action.name, startedAt: Date.now() },
2342
+ lastEventAt: Date.now()
2343
+ }
2344
+ }
2345
+ };
2346
+ }
2347
+ case "fleetToolEnd": {
2348
+ const cur = state.fleet[action.id];
2349
+ if (!cur) return state;
2350
+ return {
2351
+ ...state,
2352
+ fleet: {
2353
+ ...state.fleet,
2354
+ [action.id]: { ...cur, currentTool: void 0, lastEventAt: Date.now() }
2355
+ }
2356
+ };
2357
+ }
2358
+ case "fleetStart": {
2359
+ const cur = state.fleet[action.id];
2360
+ if (!cur) return state;
2361
+ return {
2362
+ ...state,
2363
+ fleet: {
2364
+ ...state.fleet,
2365
+ [action.id]: {
2366
+ ...cur,
2367
+ status: "running",
2368
+ streamingText: "",
2369
+ startedAt: Date.now()
2370
+ }
2371
+ }
2372
+ };
2373
+ }
2374
+ case "fleetDelta": {
2375
+ const cur = state.fleet[action.id];
2376
+ if (!cur) return state;
2377
+ const appended = (cur.streamingText + action.text).slice(-200);
2378
+ return {
2379
+ ...state,
2380
+ fleet: {
2381
+ ...state.fleet,
2382
+ [action.id]: { ...cur, streamingText: appended, lastEventAt: Date.now() }
2383
+ }
2384
+ };
2385
+ }
2386
+ case "fleetTool": {
2387
+ const cur = state.fleet[action.id];
2388
+ if (!cur) return state;
2389
+ return {
2390
+ ...state,
2391
+ fleet: {
2392
+ ...state.fleet,
2393
+ [action.id]: { ...cur, toolCalls: cur.toolCalls + 1, lastEventAt: Date.now() }
2394
+ }
2395
+ };
2396
+ }
2397
+ case "fleetUsage": {
2398
+ const cur = state.fleet[action.id];
2399
+ if (!cur) return state;
2400
+ const cost = cur.cost;
2401
+ return {
2402
+ ...state,
2403
+ fleet: { ...state.fleet, [action.id]: { ...cur, cost, lastEventAt: Date.now() } }
2404
+ };
2405
+ }
2406
+ case "fleetDone": {
2407
+ const cur = state.fleet[action.id];
2408
+ if (!cur) return state;
2409
+ return {
2410
+ ...state,
2411
+ fleet: {
2412
+ ...state.fleet,
2413
+ [action.id]: {
2414
+ ...cur,
2415
+ status: action.status,
2416
+ iterations: action.iterations,
2417
+ toolCalls: action.toolCalls,
2418
+ streamingText: "",
2419
+ lastEventAt: Date.now()
2420
+ }
2421
+ }
2422
+ };
2423
+ }
2424
+ case "fleetCost": {
2425
+ return { ...state, fleetCost: action.cost };
2426
+ }
2427
+ case "setStreamFleet": {
2428
+ return { ...state, streamFleet: action.enabled };
2429
+ }
1985
2430
  }
1986
2431
  }
1987
2432
  var PASTE_THRESHOLD_CHARS = 200;
2433
+ function buildSteeringPreamble(snapshot, newDirection) {
2434
+ const lines = ["[STEERING \u2014 I pressed Esc to interrupt you mid-task on purpose.", ""];
2435
+ const ctx = [];
2436
+ if (snapshot?.runningTools && snapshot.runningTools.length > 0) {
2437
+ ctx.push(`- in-flight tools (now cancelled): ${snapshot.runningTools.join(", ")}`);
2438
+ }
2439
+ if (snapshot?.subagentsTerminated && snapshot.subagentsTerminated > 0) {
2440
+ const subDetails = snapshot.subagents.map((s) => `${s.label}${s.tool ? ` (was running: ${s.tool})` : ""}`).join(", ");
2441
+ ctx.push(
2442
+ `- subagents (${snapshot.subagentsTerminated} terminated by me, do NOT await them): ${subDetails}`
2443
+ );
2444
+ }
2445
+ if (snapshot?.partialAssistantText && snapshot.partialAssistantText.trim().length > 0) {
2446
+ const tail = snapshot.partialAssistantText.trim().slice(-300);
2447
+ ctx.push(`- your last partial output (truncated, for context only): "${tail}"`);
2448
+ }
2449
+ if (ctx.length > 0) {
2450
+ lines.push("What was happening when I cut you off:");
2451
+ lines.push(...ctx);
2452
+ lines.push("");
2453
+ }
2454
+ lines.push("You have authority to:");
2455
+ lines.push("- Abandon the prior plan entirely if the new direction makes it stale.");
2456
+ lines.push("- Re-spawn fresh subagents (with different roles or tasks) if needed.");
2457
+ lines.push('- Skip a polite "should I continue?" \u2014 just pivot.');
2458
+ lines.push("- Ask me to clarify if the new direction is genuinely ambiguous.");
2459
+ lines.push("");
2460
+ lines.push("New direction:");
2461
+ lines.push("---");
2462
+ lines.push(newDirection);
2463
+ lines.push("---");
2464
+ lines.push("]");
2465
+ return lines.join("\n");
2466
+ }
2467
+ function buildGoalPreamble(goal) {
2468
+ return [
2469
+ "[GOAL \u2014 LOCKED IN. You will work on this until it is verifiably done.",
2470
+ "The user granted you full autonomy. Read these constraints once, then act.",
2471
+ "",
2472
+ "YOUR GOAL:",
2473
+ "---",
2474
+ goal,
2475
+ "---",
2476
+ "",
2477
+ "AUTHORITY YOU HAVE:",
2478
+ "- Spawn as many subagents as the work needs (delegate / spawn_subagent).",
2479
+ " Parallel + recursive fan-out are both fine. There is no spawn budget.",
2480
+ "- Use any provider/model per subagent \u2014 pick the right tool for each",
2481
+ " piece of work. Heavy reasoning model for planning, fast model for",
2482
+ " batch work, specialist model for domain code.",
2483
+ "- Run unlimited tool calls and iterations. There is NO hidden budget.",
2484
+ " The Agent loop auto-extends every 100 iterations forever.",
2485
+ "- Retry failed tools with different inputs, alternative paths, fresh",
2486
+ " subagents. Switch providers mid-run if one is rate-limited.",
2487
+ "- Re-plan freely when an approach hits a dead end. You are not obliged",
2488
+ " to stick with the first plan you proposed.",
2489
+ "",
2490
+ 'WHAT "DONE" MEANS \u2014 non-negotiable:',
2491
+ "- You can name a concrete artifact (a passing test, a written file at",
2492
+ " a specific path, a fixed bug verified by re-running the failing case,",
2493
+ " a clean grep that previously had matches).",
2494
+ "- You can tell the user HOW to verify it themselves in 10 seconds.",
2495
+ '- You have NOT hedged. None of: "looks like it should work", "I',
2496
+ ' believe this fixes it", "the changes appear correct".',
2497
+ "",
2498
+ "WHAT IS NOT DONE \u2014 never report any of these as completion:",
2499
+ "- An error message you didn't recover from.",
2500
+ '- An empty result, a 0-line file, a "no matches found" you accepted',
2501
+ " without questioning the search.",
2502
+ '- "Should I continue?" / "Want me to also...?" / "Let me know if you',
2503
+ ' want X." Those are hedges. The user already told you to finish the',
2504
+ " goal \u2014 just do it.",
2505
+ "- Partial progress dressed up as success. Fixed 3 of 5 bugs = 60%",
2506
+ " done, not done.",
2507
+ "- A subagent's failed/timeout/stopped TaskResult that you didn't",
2508
+ " respond to with a fresh attempt (different role, different model,",
2509
+ " tighter prompt).",
2510
+ "",
2511
+ "PERSISTENCE PROTOCOL:",
2512
+ "- If blocked, try at least 3 different angles before reporting the",
2513
+ " problem to the user. Different tool inputs, different subagent",
2514
+ " roles, different providers, different decomposition of the task.",
2515
+ "- If a tool fails, read its error, alter the input, try again. Do",
2516
+ " not just report the failure back.",
2517
+ "- If a subagent returns useless output, respawn with a tighter prompt",
2518
+ ' or a different role. Do not accept "I could not determine\u2026" as the',
2519
+ " final answer.",
2520
+ "- Use `ask_subagent` for one-shot questions when you don't need a",
2521
+ " full delegated task.",
2522
+ "",
2523
+ "REPORTING:",
2524
+ "- Stream short progress notes between major actions so the user can",
2525
+ " monitor. Do not go silent for 50 tool calls then dump a wall of",
2526
+ " text \u2014 but also do not narrate every tool call.",
2527
+ "- Use the shared scratchpad (if available) to leave breadcrumbs",
2528
+ " subagents can read.",
2529
+ "- Final response must include: (a) what was accomplished, (b) how",
2530
+ " to verify, (c) any caveats (residual TODOs, things the user",
2531
+ " should know about).",
2532
+ "",
2533
+ "BEGIN.]"
2534
+ ].join("\n");
2535
+ }
1988
2536
  function App({
1989
2537
  agent,
1990
2538
  slashRegistry,
@@ -2003,7 +2551,12 @@ function App({
2003
2551
  switchProviderAndModel,
2004
2552
  effectiveMaxContext,
2005
2553
  onExit,
2006
- onClearHistory
2554
+ director,
2555
+ fleetRoster,
2556
+ onClearHistory,
2557
+ fleetStreamController,
2558
+ initialGoal,
2559
+ initialAsk
2007
2560
  }) {
2008
2561
  const { exit } = useApp();
2009
2562
  const [liveModel, setLiveModel] = useState(model);
@@ -2028,6 +2581,8 @@ function App({
2028
2581
  toolStream: null,
2029
2582
  status: "idle",
2030
2583
  interrupts: 0,
2584
+ steeringPending: false,
2585
+ steerSnapshot: null,
2031
2586
  hint: "",
2032
2587
  nextId: 1,
2033
2588
  picker: { open: false, query: "", matches: [], selected: 0 },
@@ -2045,13 +2600,18 @@ function App({
2045
2600
  selected: 0
2046
2601
  },
2047
2602
  confirm: null,
2048
- contextChipVersion: 0
2603
+ contextChipVersion: 0,
2604
+ fleet: {},
2605
+ fleetCost: 0,
2606
+ streamFleet: true
2049
2607
  });
2050
2608
  const builderRef = useRef(null);
2051
2609
  if (builderRef.current === null) {
2052
2610
  builderRef.current = new InputBuilder({ store: attachments });
2053
2611
  }
2054
2612
  const activeCtrlRef = useRef(null);
2613
+ const inputGateRef = useRef(false);
2614
+ const lastEnterAtRef = useRef(0);
2055
2615
  const projectRoot = agent.ctx.projectRoot;
2056
2616
  const projectName = React.useMemo(() => {
2057
2617
  const base = path3.basename(projectRoot);
@@ -2108,6 +2668,111 @@ function App({
2108
2668
  }
2109
2669
  return counts;
2110
2670
  }, [nowTick, agent.ctx.todos]);
2671
+ const fleetCounts = useMemo(() => {
2672
+ const entries = Object.values(state.fleet);
2673
+ if (entries.length === 0) return void 0;
2674
+ let running = 0;
2675
+ let idle = 0;
2676
+ let completed = 0;
2677
+ for (const e of entries) {
2678
+ if (e.status === "running") running += 1;
2679
+ else if (e.status === "idle") idle += 1;
2680
+ else completed += 1;
2681
+ }
2682
+ return { running, idle, pending: 0, completed };
2683
+ }, [state.fleet]);
2684
+ const STREAM_COLORS = ["cyan", "magenta", "yellow", "green", "blue"];
2685
+ const labelsRef = useRef(/* @__PURE__ */ new Map());
2686
+ const labelFor = (id, name) => {
2687
+ const m = labelsRef.current;
2688
+ const existing = m.get(id);
2689
+ if (existing) return existing;
2690
+ const n = m.size + 1;
2691
+ const suffix = name && name !== id ? ` ${name}` : "";
2692
+ const v = {
2693
+ label: `AGENT#${n}${suffix}`,
2694
+ color: STREAM_COLORS[(n - 1) % STREAM_COLORS.length]
2695
+ };
2696
+ m.set(id, v);
2697
+ return v;
2698
+ };
2699
+ const fleetAgents = useMemo(() => {
2700
+ const entries = Object.entries(state.fleet);
2701
+ if (entries.length === 0) return void 0;
2702
+ const active = entries.filter(([_id, e]) => e.status === "running" || e.status === "idle");
2703
+ if (active.length === 0) return void 0;
2704
+ active.sort((a, b) => {
2705
+ const sa = a[1].status === "running" ? 0 : 1;
2706
+ const sb = b[1].status === "running" ? 0 : 1;
2707
+ if (sa !== sb) return sa - sb;
2708
+ return a[1].startedAt - b[1].startedAt;
2709
+ });
2710
+ return active.slice(0, 4).map(([id, e]) => {
2711
+ const lbl = labelFor(id, e.name);
2712
+ return {
2713
+ label: lbl.label,
2714
+ color: lbl.color,
2715
+ elapsedMs: Math.max(0, nowTick - e.startedAt),
2716
+ toolCalls: e.toolCalls,
2717
+ running: e.status === "running"
2718
+ };
2719
+ });
2720
+ }, [state.fleet, nowTick]);
2721
+ const [planCounts, setPlanCounts] = useState(null);
2722
+ useEffect(() => {
2723
+ const planPath = agent.ctx.meta["plan.path"];
2724
+ if (typeof planPath !== "string" || !planPath) return;
2725
+ let cancelled = false;
2726
+ const poll = async () => {
2727
+ try {
2728
+ const data = await fs.readFile(planPath, "utf8");
2729
+ const parsed = JSON.parse(data);
2730
+ if (cancelled) return;
2731
+ if (!Array.isArray(parsed.items)) {
2732
+ setPlanCounts(null);
2733
+ return;
2734
+ }
2735
+ let open = 0;
2736
+ let inProgress = 0;
2737
+ let done = 0;
2738
+ for (const it of parsed.items) {
2739
+ if (it?.status === "done") done++;
2740
+ else if (it?.status === "in_progress") inProgress++;
2741
+ else open++;
2742
+ }
2743
+ setPlanCounts(open + inProgress + done > 0 ? { open, inProgress, done } : null);
2744
+ } catch {
2745
+ if (!cancelled) setPlanCounts(null);
2746
+ }
2747
+ };
2748
+ void poll();
2749
+ const id = setInterval(poll, 3e3);
2750
+ return () => {
2751
+ cancelled = true;
2752
+ clearInterval(id);
2753
+ };
2754
+ }, [agent.ctx.meta]);
2755
+ const prevAnyOverlayOpen = useRef(false);
2756
+ const prevEntriesCount = useRef(0);
2757
+ useEffect(() => {
2758
+ const anyOpenNow = state.picker.open || state.slashPicker.open || state.modelPicker.open || !!state.confirm;
2759
+ const overlayClosed = prevAnyOverlayOpen.current && !anyOpenNow;
2760
+ const newEntryCommitted = state.entries.length > prevEntriesCount.current;
2761
+ prevAnyOverlayOpen.current = anyOpenNow;
2762
+ prevEntriesCount.current = state.entries.length;
2763
+ if (overlayClosed || newEntryCommitted) {
2764
+ try {
2765
+ process.stdout.write("\x1B[J");
2766
+ } catch {
2767
+ }
2768
+ }
2769
+ }, [
2770
+ state.picker.open,
2771
+ state.slashPicker.open,
2772
+ state.modelPicker.open,
2773
+ state.confirm,
2774
+ state.entries.length
2775
+ ]);
2111
2776
  useEffect(() => {
2112
2777
  const detected = detectAtToken(state.buffer, state.cursor);
2113
2778
  if (!detected) {
@@ -2267,6 +2932,128 @@ function App({
2267
2932
  slashRegistry.unregister("queue");
2268
2933
  };
2269
2934
  }, [slashRegistry]);
2935
+ useEffect(() => {
2936
+ const ALT_OFF = "\x1B[?1049l";
2937
+ const ALT_ON = "\x1B[?1049h";
2938
+ const cmd = {
2939
+ name: "altscreen",
2940
+ description: "Toggle the alt-screen buffer. Default is OFF (native scroll); /altscreen on for full-screen mode.",
2941
+ async run(args) {
2942
+ const arg = args.trim().toLowerCase();
2943
+ if (arg === "off") {
2944
+ try {
2945
+ process.stdout.write(ALT_OFF);
2946
+ } catch {
2947
+ return { message: "Failed to exit alt-screen." };
2948
+ }
2949
+ return {
2950
+ message: "Alt-screen disabled. New entries will land in normal scrollback (mouse wheel / Shift+PgUp work). On-screen history rendered before this command is no longer reachable via terminal scroll. Resize may now leak the live region \u2014 `/altscreen on` to re-enable."
2951
+ };
2952
+ }
2953
+ if (arg === "on") {
2954
+ try {
2955
+ process.stdout.write(ALT_ON);
2956
+ } catch {
2957
+ return { message: "Failed to re-enter alt-screen." };
2958
+ }
2959
+ return { message: "Alt-screen re-enabled. Native scroll is now disabled." };
2960
+ }
2961
+ return { message: "Usage: /altscreen on|off" };
2962
+ }
2963
+ };
2964
+ slashRegistry.register(cmd);
2965
+ return () => {
2966
+ slashRegistry.unregister("altscreen");
2967
+ };
2968
+ }, [slashRegistry]);
2969
+ useEffect(() => {
2970
+ const cmd = {
2971
+ name: "steer",
2972
+ description: "Interrupt the running agent (incl. fleet) and redirect: /steer <new direction>",
2973
+ help: [
2974
+ "Usage: /steer <new direction>",
2975
+ "",
2976
+ "Aborts the active iteration, terminates any running subagents,",
2977
+ "drops queued messages, and sends your text to the model with a",
2978
+ "STEERING preamble explaining what was in flight and what the",
2979
+ "model is authorised to do (pivot hard, respawn subagents, ask",
2980
+ "for clarification). Equivalent to pressing Esc then typing."
2981
+ ].join("\n"),
2982
+ async run(args) {
2983
+ const text = args.trim();
2984
+ if (!text) {
2985
+ return { message: "Usage: /steer <new direction>" };
2986
+ }
2987
+ const s = stateRef.current;
2988
+ const runningTools = Array.from(s.runningTools.values()).map((t) => t.name);
2989
+ const subagents = Object.values(s.fleet).filter((e) => e.status === "running").map((e) => ({ label: e.name, status: e.status, tool: e.currentTool?.name }));
2990
+ const subagentsTerminated = subagents.length;
2991
+ const partialAssistantText = streamingTextRef.current.slice(-1500);
2992
+ activeCtrlRef.current?.abort();
2993
+ dispatch({
2994
+ type: "steerStart",
2995
+ snapshot: { runningTools, subagents, subagentsTerminated, partialAssistantText }
2996
+ });
2997
+ const droppedCount = s.queue.length;
2998
+ if (droppedCount > 0) dispatch({ type: "queueClear" });
2999
+ if (director && subagentsTerminated > 0) {
3000
+ const cap = new Promise((resolve) => {
3001
+ const t = setTimeout(resolve, 1500);
3002
+ t.unref?.();
3003
+ });
3004
+ void Promise.race([director.terminateAll().catch(() => void 0), cap]);
3005
+ }
3006
+ const preamble = buildSteeringPreamble(
3007
+ { runningTools, subagents, subagentsTerminated, partialAssistantText },
3008
+ text
3009
+ );
3010
+ dispatch({ type: "steerConsume" });
3011
+ const droppedTag = droppedCount > 0 ? ` \xB7 dropped ${droppedCount} queued` : "";
3012
+ const fleetTag = subagentsTerminated > 0 ? ` \xB7 stopped ${subagentsTerminated} subagent${subagentsTerminated === 1 ? "" : "s"}` : "";
3013
+ return {
3014
+ message: `\u21AF Steering${droppedTag}${fleetTag}.`,
3015
+ runText: preamble
3016
+ };
3017
+ }
3018
+ };
3019
+ slashRegistry.register(cmd);
3020
+ return () => {
3021
+ slashRegistry.unregister("steer");
3022
+ };
3023
+ }, [slashRegistry, director]);
3024
+ useEffect(() => {
3025
+ const cmd = {
3026
+ name: "goal",
3027
+ description: "Lock in a goal \u2014 no budgets, no hedging, no premature done. /goal <description>",
3028
+ help: [
3029
+ "Usage: /goal <description>",
3030
+ "",
3031
+ "Hands the agent a task it must drive to a verifiable finish.",
3032
+ "Adds a preamble to the next turn that grants full autonomy",
3033
+ "(unlimited subagents, any provider/model, retry-until-it-works),",
3034
+ 'spells out what "done" actually means, and forbids hedge-style',
3035
+ 'completions ("I believe this works", "should I continue?").',
3036
+ "",
3037
+ "Combine with /steer to redirect mid-goal, or Ctrl+C / /fleet kill",
3038
+ "to bail out \u2014 only the user can stop a /goal."
3039
+ ].join("\n"),
3040
+ async run(args) {
3041
+ const goal = args.trim();
3042
+ if (!goal) return { message: "Usage: /goal <description>" };
3043
+ const preamble = buildGoalPreamble(goal);
3044
+ const shortGoal = goal.length > 80 ? `${goal.slice(0, 80)}\u2026` : goal;
3045
+ return {
3046
+ message: `\u{1F3AF} Goal locked: ${shortGoal}
3047
+ Agent will work until verifiably complete. Esc / /steer to redirect, Ctrl+C to stop.`,
3048
+ runText: preamble
3049
+ };
3050
+ }
3051
+ };
3052
+ slashRegistry.register(cmd);
3053
+ return () => {
3054
+ slashRegistry.unregister("goal");
3055
+ };
3056
+ }, [slashRegistry]);
2270
3057
  useEffect(() => {
2271
3058
  if (!getPickableProviders || !switchProviderAndModel) return;
2272
3059
  const cmd = {
@@ -2397,18 +3184,298 @@ function App({
2397
3184
  offConfirmNeeded();
2398
3185
  if (flushTimerRef.current) clearTimeout(flushTimerRef.current);
2399
3186
  };
3187
+ }, [events, agent.ctx.todos]);
3188
+ const streamFleetRef = useRef(state.streamFleet);
3189
+ useEffect(() => {
3190
+ streamFleetRef.current = state.streamFleet;
3191
+ }, [state.streamFleet]);
3192
+ useEffect(() => {
3193
+ const offSpawned = events.on("subagent.spawned", (e) => {
3194
+ const lbl = labelFor(e.subagentId, e.name);
3195
+ dispatch({
3196
+ type: "fleetSpawn",
3197
+ id: e.subagentId,
3198
+ name: e.name,
3199
+ provider: e.provider,
3200
+ model: e.model,
3201
+ transcriptPath: e.transcriptPath
3202
+ });
3203
+ const where = e.provider && e.model ? `${e.provider}/${e.model}` : "spawned";
3204
+ const desc = e.description ? ` \u2014 ${e.description.slice(0, 80)}` : "";
3205
+ dispatch({
3206
+ type: "addEntry",
3207
+ entry: {
3208
+ kind: "subagent",
3209
+ agentLabel: lbl.label,
3210
+ agentColor: lbl.color,
3211
+ icon: "\u25B6",
3212
+ text: `${where}${desc}`
3213
+ }
3214
+ });
3215
+ });
3216
+ const offStarted = events.on("subagent.task_started", (e) => {
3217
+ const lbl = labelFor(e.subagentId);
3218
+ dispatch({ type: "fleetStart", id: e.subagentId, taskId: e.taskId });
3219
+ const desc = e.description ? ` \u2014 ${e.description.slice(0, 80)}` : "";
3220
+ dispatch({
3221
+ type: "addEntry",
3222
+ entry: {
3223
+ kind: "subagent",
3224
+ agentLabel: lbl.label,
3225
+ agentColor: lbl.color,
3226
+ icon: "\u25CF",
3227
+ text: `task started${desc}`
3228
+ }
3229
+ });
3230
+ });
3231
+ const offCompleted = events.on("subagent.task_completed", (e) => {
3232
+ const lbl = labelFor(e.subagentId);
3233
+ dispatch({
3234
+ type: "fleetDone",
3235
+ id: e.subagentId,
3236
+ status: e.status,
3237
+ iterations: e.iterations,
3238
+ toolCalls: e.toolCalls
3239
+ });
3240
+ const icon = e.status === "success" ? "\u2713" : e.status === "timeout" ? "\u23F1" : e.status === "stopped" ? "\u2298" : "\u2717";
3241
+ const errKind = e.error?.kind;
3242
+ const errMsg = e.error?.message;
3243
+ const errMsgTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 100)}${errMsg.length > 100 ? "\u2026" : ""}` : "";
3244
+ const errChip = errKind ? ` [${errKind}]` : "";
3245
+ const secs = (e.durationMs / 1e3).toFixed(e.durationMs < 1e4 ? 1 : 0);
3246
+ dispatch({
3247
+ type: "addEntry",
3248
+ entry: {
3249
+ kind: "subagent",
3250
+ agentLabel: lbl.label,
3251
+ agentColor: lbl.color,
3252
+ icon,
3253
+ text: `${e.status} (${e.iterations} iter \xB7 ${e.toolCalls} tools \xB7 ${secs}s)${errChip}${errMsgTail}`
3254
+ }
3255
+ });
3256
+ });
3257
+ const offTool = events.on("subagent.tool_executed", (e) => {
3258
+ const lbl = labelFor(e.subagentId);
3259
+ dispatch({ type: "fleetTool", id: e.subagentId });
3260
+ dispatch({ type: "fleetToolEnd", id: e.subagentId });
3261
+ const bytesTag = typeof e.outputBytes === "number" && e.outputBytes > 0 ? ` \xB7 ${e.outputBytes < 1024 ? `${e.outputBytes}B` : `${(e.outputBytes / 1024).toFixed(1)}KB`}` : "";
3262
+ dispatch({
3263
+ type: "addEntry",
3264
+ entry: {
3265
+ kind: "subagent",
3266
+ agentLabel: lbl.label,
3267
+ agentColor: lbl.color,
3268
+ icon: e.ok === false ? "\u2717" : "\u25CF",
3269
+ text: e.name,
3270
+ detail: `${e.durationMs}ms${bytesTag}`
3271
+ }
3272
+ });
3273
+ });
3274
+ return () => {
3275
+ offSpawned();
3276
+ offStarted();
3277
+ offCompleted();
3278
+ offTool();
3279
+ };
2400
3280
  }, [events]);
3281
+ useEffect(() => {
3282
+ if (!fleetStreamController) return;
3283
+ fleetStreamController.enabled = state.streamFleet;
3284
+ fleetStreamController.setEnabled = (enabled) => {
3285
+ dispatch({ type: "setStreamFleet", enabled });
3286
+ };
3287
+ return () => {
3288
+ fleetStreamController.setEnabled = (enabled) => {
3289
+ fleetStreamController.enabled = enabled;
3290
+ };
3291
+ };
3292
+ }, [fleetStreamController, state.streamFleet]);
3293
+ useEffect(() => {
3294
+ if (fleetStreamController) fleetStreamController.enabled = state.streamFleet;
3295
+ }, [state.streamFleet, fleetStreamController]);
3296
+ useEffect(() => {
3297
+ const d = director;
3298
+ if (!d) return;
3299
+ const FLUSH_MS = 150;
3300
+ const streamBuf = /* @__PURE__ */ new Map();
3301
+ let streamFlushTimer = null;
3302
+ const flushStreamBufs = () => {
3303
+ for (const [id, text] of streamBuf) {
3304
+ if (!text.trim()) continue;
3305
+ const lbl = labelFor(id);
3306
+ dispatch({
3307
+ type: "addEntry",
3308
+ entry: {
3309
+ kind: "subagent",
3310
+ agentLabel: lbl.label,
3311
+ agentColor: lbl.color,
3312
+ icon: "\u{1F4AC}",
3313
+ text: text.trim()
3314
+ }
3315
+ });
3316
+ }
3317
+ streamBuf.clear();
3318
+ streamFlushTimer = null;
3319
+ };
3320
+ const status = d.status();
3321
+ for (const s of status.subagents) {
3322
+ const meta = d.getSubagentMeta(s.id);
3323
+ dispatch({
3324
+ type: "fleetSpawn",
3325
+ id: s.id,
3326
+ name: meta?.name ?? s.name,
3327
+ provider: meta?.provider,
3328
+ model: meta?.model
3329
+ });
3330
+ labelFor(s.id, meta?.name ?? s.name);
3331
+ }
3332
+ dispatch({ type: "fleetCost", cost: d.snapshot().total.cost });
3333
+ const seen = new Set(Object.keys(status.subagents));
3334
+ const pending = /* @__PURE__ */ new Map();
3335
+ let flushTimer = null;
3336
+ const doFlush = () => {
3337
+ for (const [id, text] of pending) {
3338
+ if (text) dispatch({ type: "fleetDelta", id, text });
3339
+ }
3340
+ pending.clear();
3341
+ flushTimer = null;
3342
+ };
3343
+ const offFleet = d.fleet.onAny((e) => {
3344
+ const fresh = !seen.has(e.subagentId);
3345
+ if (fresh) {
3346
+ seen.add(e.subagentId);
3347
+ const meta = d.getSubagentMeta(e.subagentId);
3348
+ dispatch({
3349
+ type: "fleetSpawn",
3350
+ id: e.subagentId,
3351
+ name: meta?.name,
3352
+ provider: meta?.provider,
3353
+ model: meta?.model
3354
+ });
3355
+ const lbl = labelFor(e.subagentId, meta?.name);
3356
+ if (streamFleetRef.current) {
3357
+ const where = meta?.provider && meta?.model ? `${meta.provider}/${meta.model}` : "spawned";
3358
+ dispatch({
3359
+ type: "addEntry",
3360
+ entry: {
3361
+ kind: "subagent",
3362
+ agentLabel: lbl.label,
3363
+ agentColor: lbl.color,
3364
+ icon: "\u25B6",
3365
+ text: where
3366
+ }
3367
+ });
3368
+ }
3369
+ }
3370
+ switch (e.type) {
3371
+ case "iteration.started":
3372
+ dispatch({ type: "fleetStart", id: e.subagentId });
3373
+ break;
3374
+ case "provider.text_delta": {
3375
+ const p = e.payload;
3376
+ if (p?.text) {
3377
+ const cur = pending.get(e.subagentId) ?? "";
3378
+ pending.set(e.subagentId, cur + p.text);
3379
+ if (!flushTimer) flushTimer = setTimeout(doFlush, FLUSH_MS);
3380
+ if (streamFleetRef.current) {
3381
+ streamBuf.set(e.subagentId, (streamBuf.get(e.subagentId) ?? "") + p.text);
3382
+ if (!streamFlushTimer) streamFlushTimer = setTimeout(flushStreamBufs, FLUSH_MS * 4);
3383
+ }
3384
+ }
3385
+ break;
3386
+ }
3387
+ case "tool.started": {
3388
+ const p = e.payload;
3389
+ if (p?.name) {
3390
+ dispatch({ type: "fleetToolStart", id: e.subagentId, name: p.name });
3391
+ }
3392
+ break;
3393
+ }
3394
+ case "tool.executed": {
3395
+ dispatch({ type: "fleetTool", id: e.subagentId });
3396
+ dispatch({ type: "fleetToolEnd", id: e.subagentId });
3397
+ if (streamFleetRef.current) {
3398
+ if (streamFlushTimer) {
3399
+ clearTimeout(streamFlushTimer);
3400
+ flushStreamBufs();
3401
+ }
3402
+ const p = e.payload;
3403
+ const args = p?.input ? formatToolArgs(p.name ?? "", p.input) : "";
3404
+ const lbl = labelFor(e.subagentId);
3405
+ dispatch({
3406
+ type: "addEntry",
3407
+ entry: {
3408
+ kind: "subagent",
3409
+ agentLabel: lbl.label,
3410
+ agentColor: lbl.color,
3411
+ icon: p?.ok === false ? "\u2717" : "\u25CF",
3412
+ text: args ? `${p?.name ?? "tool"} ${args}` : p?.name ?? "tool",
3413
+ detail: typeof p?.durationMs === "number" ? `${p.durationMs}ms` : void 0
3414
+ }
3415
+ });
3416
+ }
3417
+ break;
3418
+ }
3419
+ case "provider.response": {
3420
+ dispatch({ type: "fleetCost", cost: d.snapshot().total.cost });
3421
+ break;
3422
+ }
3423
+ }
3424
+ });
3425
+ const offDone = d.on("task.completed", (payload) => {
3426
+ dispatch({
3427
+ type: "fleetDone",
3428
+ id: payload.result.subagentId,
3429
+ status: payload.result.status,
3430
+ iterations: payload.result.iterations,
3431
+ toolCalls: payload.result.toolCalls
3432
+ });
3433
+ dispatch({ type: "fleetCost", cost: d.snapshot().total.cost });
3434
+ if (streamFleetRef.current && streamFlushTimer) {
3435
+ clearTimeout(streamFlushTimer);
3436
+ flushStreamBufs();
3437
+ }
3438
+ });
3439
+ return () => {
3440
+ offFleet();
3441
+ offDone();
3442
+ if (flushTimer) clearTimeout(flushTimer);
3443
+ doFlush();
3444
+ if (streamFlushTimer) clearTimeout(streamFlushTimer);
3445
+ flushStreamBufs();
3446
+ };
3447
+ }, [director]);
2401
3448
  useEffect(() => {
2402
3449
  const onSigint = () => {
2403
- if (state.interrupts >= 1 && state.status === "idle") {
2404
- exit();
2405
- onExit(130);
3450
+ if (state.interrupts >= 1) {
3451
+ if (state.interrupts >= 2) {
3452
+ process.exit(130);
3453
+ }
3454
+ try {
3455
+ exit();
3456
+ onExit(130);
3457
+ } catch {
3458
+ }
3459
+ setTimeout(() => {
3460
+ try {
3461
+ process.exit(130);
3462
+ } catch {
3463
+ }
3464
+ }, 500).unref?.();
3465
+ dispatch({ type: "interrupt" });
2406
3466
  return;
2407
3467
  }
2408
3468
  dispatch({ type: "interrupt" });
2409
3469
  if (activeCtrlRef.current) {
2410
3470
  activeCtrlRef.current.abort();
2411
3471
  dispatch({ type: "status", status: "aborting" });
3472
+ if (director) {
3473
+ const cap = new Promise((resolve) => {
3474
+ const t = setTimeout(resolve, 1500);
3475
+ t.unref?.();
3476
+ });
3477
+ void Promise.race([director.terminateAll().catch(() => void 0), cap]);
3478
+ }
2412
3479
  const droppedCount = stateRef.current.queue.length;
2413
3480
  if (droppedCount > 0) {
2414
3481
  dispatch({ type: "queueClear" });
@@ -2416,13 +3483,16 @@ function App({
2416
3483
  type: "addEntry",
2417
3484
  entry: {
2418
3485
  kind: "warn",
2419
- text: `Iteration cancelled. Dropped ${droppedCount} queued message${droppedCount === 1 ? "" : "s"}. Press Ctrl+C again to exit.`
3486
+ text: `Iteration cancelled${director ? " + fleet terminated" : ""}. Dropped ${droppedCount} queued message${droppedCount === 1 ? "" : "s"}. Press Ctrl+C again to exit.`
2420
3487
  }
2421
3488
  });
2422
3489
  } else {
2423
3490
  dispatch({
2424
3491
  type: "addEntry",
2425
- entry: { kind: "warn", text: "Iteration cancelled. Press Ctrl+C again to exit." }
3492
+ entry: {
3493
+ kind: "warn",
3494
+ text: `Iteration cancelled${director ? " + fleet terminated" : ""}. Press Ctrl+C again to exit.`
3495
+ }
2426
3496
  });
2427
3497
  }
2428
3498
  } else {
@@ -2436,9 +3506,11 @@ function App({
2436
3506
  return () => {
2437
3507
  process.off("SIGINT", onSigint);
2438
3508
  };
2439
- }, [state.interrupts, state.status, exit, onExit]);
3509
+ }, [state.interrupts, exit, onExit, director]);
2440
3510
  const handleKey = async (input, key) => {
2441
3511
  if (state.status === "aborting") return;
3512
+ if (inputGateRef.current) return;
3513
+ const isEnter = key.return || input === "\r" || input === "\n";
2442
3514
  if (state.modelPicker.open) {
2443
3515
  if (key.escape) {
2444
3516
  if (state.modelPicker.step === "model") {
@@ -2456,33 +3528,38 @@ function App({
2456
3528
  dispatch({ type: "modelPickerMove", delta: 1 });
2457
3529
  return;
2458
3530
  }
2459
- if (key.return) {
2460
- if (state.modelPicker.step === "provider") {
2461
- const opt = state.modelPicker.providerOptions[state.modelPicker.selected];
2462
- if (!opt) return;
3531
+ if (isEnter) {
3532
+ inputGateRef.current = true;
3533
+ try {
3534
+ if (state.modelPicker.step === "provider") {
3535
+ const opt = state.modelPicker.providerOptions[state.modelPicker.selected];
3536
+ if (!opt) return;
3537
+ dispatch({
3538
+ type: "modelPickerPickProvider",
3539
+ providerId: opt.id,
3540
+ models: opt.models
3541
+ });
3542
+ return;
3543
+ }
3544
+ const providerId = state.modelPicker.pickedProviderId;
3545
+ const modelId = state.modelPicker.modelOptions[state.modelPicker.selected];
3546
+ if (!providerId || !modelId) return;
3547
+ const err = switchProviderAndModel?.(providerId, modelId);
3548
+ if (err) {
3549
+ dispatch({ type: "modelPickerHint", text: err });
3550
+ return;
3551
+ }
3552
+ setLiveProvider(providerId);
3553
+ setLiveModel(modelId);
2463
3554
  dispatch({
2464
- type: "modelPickerPickProvider",
2465
- providerId: opt.id,
2466
- models: opt.models
3555
+ type: "addEntry",
3556
+ entry: { kind: "info", text: `Switched to ${providerId} / ${modelId}.` }
2467
3557
  });
3558
+ dispatch({ type: "modelPickerClose" });
2468
3559
  return;
3560
+ } finally {
3561
+ inputGateRef.current = false;
2469
3562
  }
2470
- const providerId = state.modelPicker.pickedProviderId;
2471
- const modelId = state.modelPicker.modelOptions[state.modelPicker.selected];
2472
- if (!providerId || !modelId) return;
2473
- const err = switchProviderAndModel?.(providerId, modelId);
2474
- if (err) {
2475
- dispatch({ type: "modelPickerHint", text: err });
2476
- return;
2477
- }
2478
- setLiveProvider(providerId);
2479
- setLiveModel(modelId);
2480
- dispatch({
2481
- type: "addEntry",
2482
- entry: { kind: "info", text: `Switched to ${providerId} / ${modelId}.` }
2483
- });
2484
- dispatch({ type: "modelPickerClose" });
2485
- return;
2486
3563
  }
2487
3564
  return;
2488
3565
  }
@@ -2499,8 +3576,10 @@ function App({
2499
3576
  dispatch({ type: "slashPickerMove", delta: 1 });
2500
3577
  return;
2501
3578
  }
2502
- if (key.return) {
2503
- await acceptSlashPickerSelection();
3579
+ if (isEnter) {
3580
+ inputGateRef.current = true;
3581
+ acceptSlashPickerSelection();
3582
+ inputGateRef.current = false;
2504
3583
  return;
2505
3584
  }
2506
3585
  if (key.tab && state.slashPicker.matches.length > 0) {
@@ -2525,13 +3604,61 @@ function App({
2525
3604
  dispatch({ type: "pickerMove", delta: 1 });
2526
3605
  return;
2527
3606
  }
2528
- if (key.return) {
2529
- await acceptPickerSelection();
3607
+ if (isEnter) {
3608
+ inputGateRef.current = true;
3609
+ try {
3610
+ await acceptPickerSelection();
3611
+ } finally {
3612
+ inputGateRef.current = false;
3613
+ }
2530
3614
  return;
2531
3615
  }
2532
3616
  }
2533
- if (key.return) {
2534
- await submit();
3617
+ if (key.escape && state.status !== "idle" && !state.confirm) {
3618
+ const runningTools = Array.from(state.runningTools.values()).map((t) => t.name);
3619
+ const subagents = Object.values(state.fleet).filter((e) => e.status === "running").map((e) => ({
3620
+ label: e.name,
3621
+ status: e.status,
3622
+ tool: e.currentTool?.name
3623
+ }));
3624
+ const subagentsTerminated = subagents.length;
3625
+ const partialAssistantText = streamingTextRef.current.slice(-1500);
3626
+ activeCtrlRef.current?.abort();
3627
+ dispatch({ type: "status", status: "aborting" });
3628
+ dispatch({
3629
+ type: "steerStart",
3630
+ snapshot: {
3631
+ runningTools,
3632
+ subagents,
3633
+ subagentsTerminated,
3634
+ partialAssistantText
3635
+ }
3636
+ });
3637
+ if (director && subagentsTerminated > 0) {
3638
+ const cap = new Promise((resolve) => {
3639
+ const t = setTimeout(resolve, 1500);
3640
+ t.unref?.();
3641
+ });
3642
+ void Promise.race([director.terminateAll().catch(() => void 0), cap]);
3643
+ }
3644
+ const droppedCount = state.queue.length;
3645
+ if (droppedCount > 0) dispatch({ type: "queueClear" });
3646
+ const droppedTag = droppedCount > 0 ? ` \xB7 dropped ${droppedCount} queued` : "";
3647
+ const fleetTag = subagentsTerminated > 0 ? ` \xB7 stopped ${subagentsTerminated} subagent${subagentsTerminated === 1 ? "" : "s"}` : "";
3648
+ dispatch({
3649
+ type: "addEntry",
3650
+ entry: {
3651
+ kind: "warn",
3652
+ text: `\u21AF Interrupted${droppedTag}${fleetTag}. Type your new direction.`
3653
+ }
3654
+ });
3655
+ return;
3656
+ }
3657
+ if (isEnter) {
3658
+ const now = Date.now();
3659
+ if (now - lastEnterAtRef.current < 50) return;
3660
+ lastEnterAtRef.current = now;
3661
+ void submit();
2535
3662
  return;
2536
3663
  }
2537
3664
  if (key.backspace || key.delete) {
@@ -2586,6 +3713,14 @@ function App({
2586
3713
  dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor + 1 });
2587
3714
  return;
2588
3715
  }
3716
+ if (key.home) {
3717
+ dispatch({ type: "setBuffer", buffer: state.buffer, cursor: 0 });
3718
+ return;
3719
+ }
3720
+ if (key.end) {
3721
+ dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.buffer.length });
3722
+ return;
3723
+ }
2589
3724
  if (key.upArrow) {
2590
3725
  if (state.inputHistory.length > 0) dispatch({ type: "historyUp" });
2591
3726
  return;
@@ -2725,6 +3860,18 @@ function App({
2725
3860
  exit();
2726
3861
  onExit(0);
2727
3862
  }
3863
+ if (res?.runText) {
3864
+ const b = builderRef.current;
3865
+ if (b) {
3866
+ b.appendText(res.runText);
3867
+ const blocks2 = await b.submit();
3868
+ const start = Date.now();
3869
+ while (stateRef.current.status !== "idle" && Date.now() - start < 1500) {
3870
+ await new Promise((r) => setTimeout(r, 25));
3871
+ }
3872
+ await runBlocks(blocks2);
3873
+ }
3874
+ }
2728
3875
  const cmd = trimmed.slice(1).split(/\s+/, 1)[0];
2729
3876
  if (cmd === "clear") {
2730
3877
  onClearHistory?.(dispatch);
@@ -2739,9 +3886,14 @@ function App({
2739
3886
  }
2740
3887
  const builder = builderRef.current;
2741
3888
  if (!builder) return;
2742
- if (trimmed) builder.appendText(trimmed);
3889
+ const steering = state.steeringPending;
3890
+ if (trimmed) {
3891
+ const toAppend = steering ? buildSteeringPreamble(state.steerSnapshot, trimmed) : trimmed;
3892
+ builder.appendText(toAppend);
3893
+ }
3894
+ if (steering) dispatch({ type: "steerConsume" });
2743
3895
  const blocks = await builder.submit();
2744
- const displayText = trimmed || "(attachments only)";
3896
+ const displayText = trimmed ? steering ? `\u21AF ${trimmed}` : trimmed : "(attachments only)";
2745
3897
  dispatch({ type: "clearInput" });
2746
3898
  if (state.status !== "idle") {
2747
3899
  dispatch({
@@ -2756,6 +3908,36 @@ function App({
2756
3908
  if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
2757
3909
  await runBlocks(blocks);
2758
3910
  };
3911
+ const bootInjectedRef = useRef(false);
3912
+ useEffect(() => {
3913
+ if (bootInjectedRef.current) return;
3914
+ bootInjectedRef.current = true;
3915
+ const goal = initialGoal?.trim();
3916
+ const ask = initialAsk?.trim();
3917
+ if (!goal && !ask) return;
3918
+ void (async () => {
3919
+ await new Promise((r) => setTimeout(r, 50));
3920
+ const b = builderRef.current;
3921
+ if (!b) return;
3922
+ if (goal) {
3923
+ const shortGoal = goal.length > 80 ? `${goal.slice(0, 80)}\u2026` : goal;
3924
+ dispatch({
3925
+ type: "addEntry",
3926
+ entry: {
3927
+ kind: "info",
3928
+ text: `\u{1F3AF} Goal locked: ${shortGoal}
3929
+ Agent will work until verifiably complete. Esc / /steer to redirect, Ctrl+C to stop.`
3930
+ }
3931
+ });
3932
+ b.appendText(buildGoalPreamble(goal));
3933
+ } else if (ask) {
3934
+ dispatch({ type: "addEntry", entry: { kind: "user", text: ask } });
3935
+ b.appendText(ask);
3936
+ }
3937
+ const blocks = await b.submit();
3938
+ await runBlocks(blocks);
3939
+ })();
3940
+ }, []);
2759
3941
  const inputHint = useMemo(() => {
2760
3942
  if (state.status !== "idle") return "";
2761
3943
  if (state.buffer.startsWith("/")) return "slash command \u2014 Enter to dispatch";
@@ -2771,6 +3953,7 @@ function App({
2771
3953
  toolStream: state.toolStream
2772
3954
  }
2773
3955
  ),
3956
+ /* @__PURE__ */ jsx(LiveActivityStrip, { entries: state.fleet, nowTick }),
2774
3957
  /* @__PURE__ */ jsx(
2775
3958
  Input,
2776
3959
  {
@@ -2829,11 +4012,16 @@ function App({
2829
4012
  yolo,
2830
4013
  elapsedMs,
2831
4014
  todos,
4015
+ plan: planCounts ?? void 0,
4016
+ fleet: fleetCounts,
4017
+ fleetAgents,
2832
4018
  git: gitInfo,
2833
4019
  context: contextWindow,
2834
- projectName
4020
+ projectName,
4021
+ subagentCount: Object.keys(state.fleet).length
2835
4022
  }
2836
- )
4023
+ ),
4024
+ director ? /* @__PURE__ */ jsx(FleetPanel, { entries: state.fleet, totalCost: state.fleetCost, roster: fleetRoster }) : null
2837
4025
  ] });
2838
4026
  }
2839
4027
  function renderRunningTools(running) {
@@ -2889,6 +4077,15 @@ async function runTui(opts) {
2889
4077
  stdout.write(CURSOR_HOME);
2890
4078
  }
2891
4079
  stdout.write(BRACKETED_PASTE_ON);
4080
+ const swallowSignals = ["SIGTSTP", "SIGQUIT", "SIGTTIN", "SIGTTOU"];
4081
+ const swallow = () => {
4082
+ };
4083
+ for (const s of swallowSignals) {
4084
+ try {
4085
+ process.on(s, swallow);
4086
+ } catch {
4087
+ }
4088
+ }
2892
4089
  let cleaned = false;
2893
4090
  const cleanup = () => {
2894
4091
  if (cleaned) return;
@@ -2908,6 +4105,12 @@ async function runTui(opts) {
2908
4105
  process.on("exit", exitHandler);
2909
4106
  const detachListeners = () => {
2910
4107
  for (const s of signals) process.off(s, signalHandler);
4108
+ for (const s of swallowSignals) {
4109
+ try {
4110
+ process.off(s, swallow);
4111
+ } catch {
4112
+ }
4113
+ }
2911
4114
  process.off("exit", exitHandler);
2912
4115
  };
2913
4116
  return new Promise((resolve) => {
@@ -2947,7 +4150,12 @@ async function runTui(opts) {
2947
4150
  switchProviderAndModel: opts.switchProviderAndModel,
2948
4151
  effectiveMaxContext: opts.effectiveMaxContext,
2949
4152
  onExit,
2950
- onClearHistory: opts.onClearHistory ? (dispatch) => opts.onClearHistory(dispatch) : void 0
4153
+ director: opts.director ?? null,
4154
+ fleetRoster: opts.fleetRoster,
4155
+ onClearHistory: opts.onClearHistory ? (dispatch) => opts.onClearHistory(dispatch) : void 0,
4156
+ fleetStreamController: opts.fleetStreamController,
4157
+ initialGoal: opts.initialGoal,
4158
+ initialAsk: opts.initialAsk
2951
4159
  }),
2952
4160
  { exitOnCtrlC: false }
2953
4161
  );
@@ -2959,7 +4167,24 @@ async function runTui(opts) {
2959
4167
  settle(1);
2960
4168
  return;
2961
4169
  }
2962
- instance.waitUntilExit().then(() => settle(exitCode)).catch(() => settle(1));
4170
+ let detachResize = null;
4171
+ if (!useAltScreen) {
4172
+ const onResize = () => {
4173
+ try {
4174
+ stdout.write("\x1B[J");
4175
+ } catch {
4176
+ }
4177
+ };
4178
+ stdout.on("resize", onResize);
4179
+ detachResize = () => stdout.off("resize", onResize);
4180
+ }
4181
+ instance.waitUntilExit().then(() => {
4182
+ detachResize?.();
4183
+ settle(exitCode);
4184
+ }).catch(() => {
4185
+ detachResize?.();
4186
+ settle(1);
4187
+ });
2963
4188
  });
2964
4189
  }
2965
4190