@wrongstack/tui 0.1.10 → 0.3.1

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,223 @@ 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 fmtDuration(ms) {
237
+ if (ms < 1e3) return `${ms}ms`;
238
+ return `${(ms / 1e3).toFixed(1)}s`;
239
+ }
240
+ function fmtBytes(n) {
241
+ if (n < 1024) return `${n}B`;
242
+ return `${(n / 1024).toFixed(1)}KB`;
243
+ }
244
+ function fmtRecentTool(tool) {
245
+ const status = tool.ok === false ? "fail" : "ok";
246
+ const name = tool.name.length > 24 ? `${tool.name.slice(0, 23)}...` : tool.name;
247
+ const parts = [status, name];
248
+ if (typeof tool.durationMs === "number") parts.push(fmtDuration(tool.durationMs));
249
+ if (typeof tool.outputBytes === "number" && tool.outputBytes > 0) parts.push(fmtBytes(tool.outputBytes));
250
+ if (typeof tool.outputLines === "number" && tool.outputLines > 0) parts.push(`${tool.outputLines}L`);
251
+ return parts.join(" ");
252
+ }
253
+ function fmtRecentMessage(message) {
254
+ const text = message.text.replace(/\s+/g, " ");
255
+ return text.length > 80 ? `${text.slice(0, 79)}...` : text;
256
+ }
257
+ function fmtModel(provider, model) {
258
+ if (!provider && !model) return "";
259
+ const p = provider ?? "";
260
+ const m = model ?? "";
261
+ return p && m ? `${p}/${m}` : p || m;
262
+ }
263
+ function resolveName(entry, roster) {
264
+ const rosterEntry = roster?.[entry.id];
265
+ if (rosterEntry) return rosterEntry.name;
266
+ return entry.name;
267
+ }
268
+ function FleetPanel({ entries, totalCost, roster }) {
269
+ const list = Object.values(entries);
270
+ if (list.length === 0) return null;
271
+ const sorted = [...list].sort((a, b) => {
272
+ const order = { running: 0, success: 1, failed: 2, timeout: 3, stopped: 4, idle: 5 };
273
+ const ao = order[a.status] ?? 9;
274
+ const bo = order[b.status] ?? 9;
275
+ if (ao !== bo) return ao - bo;
276
+ return b.lastEventAt - a.lastEventAt;
277
+ });
278
+ const runningCount = list.filter((e) => e.status === "running").length;
279
+ const totalLabel = totalCost > 0 ? `$${totalCost.toFixed(3)} \xB7 ${runningCount} active` : `${runningCount} active`;
280
+ return /* @__PURE__ */ jsxs(
281
+ Box,
282
+ {
283
+ flexDirection: "column",
284
+ paddingX: 1,
285
+ borderStyle: "single",
286
+ borderTop: false,
287
+ borderBottom: false,
288
+ borderLeft: false,
289
+ borderRight: false,
290
+ children: [
291
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, children: [
292
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Fleet" }),
293
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
294
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
295
+ list.length,
296
+ " agent",
297
+ list.length === 1 ? "" : "s"
298
+ ] }),
299
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
300
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: totalLabel })
301
+ ] }),
302
+ sorted.map((entry) => {
303
+ const si = STATUS_ICON[entry.status];
304
+ const modelTag = fmtModel(entry.provider, entry.model);
305
+ const name = resolveName(entry, roster);
306
+ const recentTools = (entry.recentTools ?? []).slice(-2).map(fmtRecentTool).join(" | ");
307
+ const recentMessages = (entry.recentMessages ?? []).slice(-2).map(fmtRecentMessage);
308
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
309
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
310
+ /* @__PURE__ */ jsx(Text, { color: si.color, children: si.icon }),
311
+ /* @__PURE__ */ jsx(Text, { children: name.slice(0, 16).padEnd(16) }),
312
+ modelTag ? /* @__PURE__ */ jsxs(Fragment, { children: [
313
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
314
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: modelTag })
315
+ ] }) : null,
316
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
317
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
318
+ fmtCount(entry.iterations).padStart(3),
319
+ "it"
320
+ ] }),
321
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
322
+ fmtCount(entry.toolCalls).padStart(3),
323
+ "tc"
324
+ ] }),
325
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
326
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: fmtCost(entry.cost) })
327
+ ] }),
328
+ entry.status === "running" && entry.currentTool ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, children: [
329
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
330
+ "\u2192 ",
331
+ entry.currentTool.name
332
+ ] }),
333
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
334
+ " (",
335
+ Math.max(0, Date.now() - entry.currentTool.startedAt),
336
+ "ms)"
337
+ ] })
338
+ ] }) : null,
339
+ recentTools ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
340
+ "tools: ",
341
+ recentTools
342
+ ] }) }) : null,
343
+ recentMessages.map((message, index) => /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
344
+ "msg: ",
345
+ message
346
+ ] }) }, `${entry.id}-msg-${index}-${message}`)),
347
+ entry.status === "running" && entry.streamingText ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
348
+ ">",
349
+ " ",
350
+ entry.streamingText.slice(-80)
351
+ ] }) }) : null,
352
+ entry.transcriptPath ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
353
+ "log: ",
354
+ entry.transcriptPath
355
+ ] }) }) : null
356
+ ] }, entry.id);
357
+ })
358
+ ]
359
+ }
360
+ );
361
+ }
362
+ function fmtElapsed(ms) {
363
+ if (ms < 1e3) return `${ms}ms`;
364
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
365
+ const m = Math.floor(ms / 6e4);
366
+ const s = Math.floor(ms % 6e4 / 1e3);
367
+ return `${m}m${s.toString().padStart(2, "0")}s`;
368
+ }
369
+ function fmtBytes2(n) {
370
+ if (n < 1024) return `${n}B`;
371
+ return `${(n / 1024).toFixed(1)}KB`;
372
+ }
373
+ function fmtRecentTool2(tool) {
374
+ const status = tool.ok === false ? "fail" : "ok";
375
+ const name = tool.name.length > 18 ? `${tool.name.slice(0, 17)}...` : tool.name;
376
+ const parts = [status, name];
377
+ if (typeof tool.durationMs === "number") parts.push(fmtElapsed(tool.durationMs));
378
+ if (typeof tool.outputBytes === "number" && tool.outputBytes > 0) parts.push(fmtBytes2(tool.outputBytes));
379
+ if (typeof tool.outputLines === "number" && tool.outputLines > 0) parts.push(`${tool.outputLines}L`);
380
+ return parts.join(" ");
381
+ }
382
+ function fmtRecentMessage2(message) {
383
+ const text = message.text.replace(/\s+/g, " ");
384
+ return text.length > 48 ? `${text.slice(0, 47)}...` : text;
385
+ }
386
+ function LiveActivityStrip({
387
+ entries,
388
+ nowTick,
389
+ maxRows = 4
390
+ }) {
391
+ const running = Object.values(entries).filter((e) => e.status === "running").sort((a, b) => a.startedAt - b.startedAt).slice(0, maxRows);
392
+ if (running.length === 0) return null;
393
+ const now = Date.now();
394
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
395
+ running.map((e) => {
396
+ const toolElapsed = e.currentTool ? now - e.currentTool.startedAt : 0;
397
+ const taskElapsed = now - e.startedAt;
398
+ const toolSeg = e.currentTool ? `\u2192 ${e.currentTool.name} (${fmtElapsed(toolElapsed)})` : "idle between tools";
399
+ const recentTools = (e.recentTools ?? []).slice(-2).map(fmtRecentTool2).join(" | ");
400
+ const messageText = e.streamingText.trim() || (e.recentMessages ?? []).slice(-1).map(fmtRecentMessage2).join("");
401
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
402
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u25CF" }),
403
+ /* @__PURE__ */ jsx(Text, { children: e.name.slice(0, 14).padEnd(14) }),
404
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
405
+ /* @__PURE__ */ jsx(Text, { color: e.currentTool ? "green" : "yellow", children: toolSeg }),
406
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
407
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
408
+ e.iterations,
409
+ "it ",
410
+ e.toolCalls,
411
+ "tc \xB7 ",
412
+ fmtElapsed(taskElapsed)
413
+ ] }),
414
+ recentTools ? /* @__PURE__ */ jsxs(Fragment, { children: [
415
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "|" }),
416
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
417
+ "last: ",
418
+ recentTools
419
+ ] })
420
+ ] }) : null,
421
+ messageText ? /* @__PURE__ */ jsxs(Fragment, { children: [
422
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "|" }),
423
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
424
+ "msg: ",
425
+ fmtRecentMessage2({ text: messageText})
426
+ ] })
427
+ ] }) : null
428
+ ] }, e.id);
429
+ }),
430
+ Object.values(entries).filter((e) => e.status === "running").length > maxRows ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
431
+ "\u2026+",
432
+ Object.values(entries).filter((e) => e.status === "running").length - maxRows,
433
+ " more"
434
+ ] }) }) : null
435
+ ] });
436
+ }
220
437
 
221
438
  // src/markdown-table.ts
222
439
  function renderMarkdownTables(text, maxWidth) {
@@ -441,7 +658,7 @@ function ToolStreamBox({
441
658
  /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
442
659
  /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u25C6 " }),
443
660
  /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: name }),
444
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u23F1 ${fmtDuration(elapsedMs)}` }),
661
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u23F1 ${fmtDuration2(elapsedMs)}` }),
445
662
  hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` (${totalLines} lines, showing last ${MAX_STREAM_LINES})` }) : null
446
663
  ] }),
447
664
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [
@@ -464,6 +681,15 @@ function tailForDisplay(text, maxChars) {
464
681
  return `\u2026 ${text.slice(cut)}`;
465
682
  }
466
683
  function DiffBlock({ rows, hidden }) {
684
+ let gutterWidth = 1;
685
+ for (const r of rows) {
686
+ const n = r.kind === "del" ? r.oldLine : r.newLine;
687
+ if (typeof n === "number") {
688
+ const w = String(n).length;
689
+ if (w > gutterWidth) gutterWidth = w;
690
+ }
691
+ }
692
+ const blank = " ".repeat(gutterWidth);
467
693
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 4, marginTop: 0, children: [
468
694
  rows.map((row, i) => {
469
695
  const key = i;
@@ -471,16 +697,20 @@ function DiffBlock({ rows, hidden }) {
471
697
  return /* @__PURE__ */ jsx(Text, { color: "cyan", dimColor: true, children: row.text }, key);
472
698
  }
473
699
  if (row.kind === "meta") {
474
- return /* @__PURE__ */ jsx(Text, { dimColor: true, children: row.text }, key);
700
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: `${blank} ${row.text}` }, key);
475
701
  }
702
+ const lnNumber = row.kind === "del" ? row.oldLine : row.newLine;
703
+ const lnText = typeof lnNumber === "number" ? String(lnNumber).padStart(gutterWidth, " ") : blank;
476
704
  if (row.kind === "ctx") {
477
- return /* @__PURE__ */ jsx(Text, { dimColor: true, children: row.text }, key);
705
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: `${lnText} ${row.text}` }, key);
478
706
  }
479
- const bg = row.kind === "add" ? "green" : "red";
480
- const fg = row.kind === "add" ? "black" : "white";
481
- return /* @__PURE__ */ jsx(Text, { backgroundColor: bg, color: fg, children: row.text }, key);
707
+ const bg = row.kind === "add" ? "greenBright" : "redBright";
708
+ return /* @__PURE__ */ jsxs(Text, { children: [
709
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: `${lnText} ` }),
710
+ /* @__PURE__ */ jsx(Text, { backgroundColor: bg, color: "black", children: row.text })
711
+ ] }, key);
482
712
  }),
483
- hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: ` \u2026 ${hidden} more line${hidden === 1 ? "" : "s"}` }) : null
713
+ hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: `${blank} \u2026 ${hidden} more line${hidden === 1 ? "" : "s"}` }) : null
484
714
  ] });
485
715
  }
486
716
  function Entry({
@@ -514,7 +744,7 @@ function Entry({
514
744
  parts.push(`${entry.outputLines} L`);
515
745
  }
516
746
  if (entry.outputBytes && entry.outputBytes > 0) {
517
- parts.push(fmtBytes(entry.outputBytes));
747
+ parts.push(fmtBytes3(entry.outputBytes));
518
748
  }
519
749
  if (entry.outputTokens && entry.outputTokens > 0) {
520
750
  parts.push(`\u2248${fmtTok(entry.outputTokens)} tok`);
@@ -530,7 +760,7 @@ function Entry({
530
760
  /* @__PURE__ */ jsx(Text, { children: " " }),
531
761
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: argSummary })
532
762
  ] }) : null,
533
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${fmtDuration(entry.durationMs)}` }),
763
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${fmtDuration2(entry.durationMs)}` }),
534
764
  sizeChip ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${sizeChip}` }) : null
535
765
  ] }),
536
766
  outLines.map((line, i) => (
@@ -596,6 +826,29 @@ function Entry({
596
826
  );
597
827
  case "banner":
598
828
  return /* @__PURE__ */ jsx(Banner, { entry });
829
+ case "subagent": {
830
+ const lines = entry.text.split("\n");
831
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
832
+ /* @__PURE__ */ jsxs(Text, { children: [
833
+ /* @__PURE__ */ jsx(Text, { color: entry.agentColor, bold: true, children: `[${entry.agentLabel}]` }),
834
+ /* @__PURE__ */ jsx(Text, { children: " " }),
835
+ /* @__PURE__ */ jsx(Text, { color: entry.agentColor, children: entry.icon }),
836
+ /* @__PURE__ */ jsx(Text, { children: " " }),
837
+ /* @__PURE__ */ jsx(Text, { children: lines[0] ?? "" }),
838
+ entry.detail ? /* @__PURE__ */ jsxs(Fragment, { children: [
839
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
840
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.detail })
841
+ ] }) : null
842
+ ] }),
843
+ lines.slice(1).map((line, i) => (
844
+ // biome-ignore lint/suspicious/noArrayIndexKey: stable line index
845
+ /* @__PURE__ */ jsxs(Text, { children: [
846
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
847
+ /* @__PURE__ */ jsx(Text, { children: line })
848
+ ] }, i)
849
+ ))
850
+ ] });
851
+ }
599
852
  }
600
853
  }
601
854
  function Banner({
@@ -647,7 +900,7 @@ function fmtTok(n) {
647
900
  if (n >= 1e3) return `${(n / 1e3).toFixed(n >= 1e4 ? 0 : 1)}k`;
648
901
  return String(n);
649
902
  }
650
- function fmtDuration(ms) {
903
+ function fmtDuration2(ms) {
651
904
  if (ms < 1e3) return `${ms}ms`;
652
905
  if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
653
906
  const totalSec = Math.floor(ms / 1e3);
@@ -775,7 +1028,7 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
775
1028
  const bytes = numOf(o["bytes_written"]) ?? numOf(o["bytes"]);
776
1029
  const created = o["created"] === true;
777
1030
  const tag = created ? "created" : "updated";
778
- if (bytes !== void 0) return [`${tag} \xB7 ${fmtBytes(bytes)}`];
1031
+ if (bytes !== void 0) return [`${tag} \xB7 ${fmtBytes3(bytes)}`];
779
1032
  return [tag];
780
1033
  }
781
1034
  }
@@ -840,17 +1093,17 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
840
1093
  if (json && typeof json === "object") {
841
1094
  const o = json;
842
1095
  const bytes = numOf(o["bytes"]);
843
- if (bytes !== void 0) return [`${fmtBytes(bytes)} read`];
1096
+ if (bytes !== void 0) return [`${fmtBytes3(bytes)} read`];
844
1097
  }
845
1098
  const range = scanNumberedRange(text);
846
1099
  if (range.count > 0 && range.first !== void 0 && range.last !== void 0) {
847
1100
  if (range.first === range.last) {
848
- return [`L${range.first} \xB7 ${fmtBytes(text.length)}`];
1101
+ return [`L${range.first} \xB7 ${fmtBytes3(text.length)}`];
849
1102
  }
850
1103
  const contiguous = range.count === range.last - range.first + 1;
851
1104
  const head = `L${range.first}\u2013${range.last}`;
852
1105
  const tail = contiguous ? `${range.count} line${range.count === 1 ? "" : "s"}` : `${range.count} lines (gaps)`;
853
- return [`${head} \xB7 ${tail} \xB7 ${fmtBytes(text.length)}`];
1106
+ return [`${head} \xB7 ${tail} \xB7 ${fmtBytes3(text.length)}`];
854
1107
  }
855
1108
  }
856
1109
  if (toolName === "grep" || toolName === "glob") {
@@ -908,7 +1161,7 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
908
1161
  const head = [];
909
1162
  if (status !== void 0) head.push(`HTTP ${status}`);
910
1163
  if (ct) head.push(ct.split(";")[0] ?? ct);
911
- if (content) head.push(fmtBytes(Buffer.byteLength(content, "utf8")));
1164
+ if (content) head.push(fmtBytes3(Buffer.byteLength(content, "utf8")));
912
1165
  const lines = [];
913
1166
  if (head.length > 0) lines.push(head.join(" \xB7 "));
914
1167
  if (url && status !== void 0 && (status < 200 || status >= 400)) {
@@ -999,7 +1252,7 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
999
1252
  if (runner && runner !== "none") head.push(runner);
1000
1253
  head.push(`${passed}/${total} passed`);
1001
1254
  if (failed > 0) head.push(`${failed} failed`);
1002
- if (duration !== void 0) head.push(fmtDuration(duration));
1255
+ if (duration !== void 0) head.push(fmtDuration2(duration));
1003
1256
  return [head.join(" \xB7 ")];
1004
1257
  }
1005
1258
  }
@@ -1120,8 +1373,51 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
1120
1373
  const lastLine = lines[lines.length - 1];
1121
1374
  return lastLine ? [head, `"${truncMid(lastLine.trim(), 70)}"`] : [head];
1122
1375
  }
1123
- const firstLine = text.split("\n").find((l) => l.trim()) ?? text;
1124
- return [truncMid(firstLine.replace(/\s+/g, " "), OUT_BUDGET)];
1376
+ if (json && typeof json === "object" && !Array.isArray(json)) {
1377
+ const summary = summarizeJsonObject(json);
1378
+ if (summary) return [summary];
1379
+ }
1380
+ const collapsed = text.replace(/\s+/g, " ").trim();
1381
+ return [truncMid(collapsed, GENERIC_BUDGET)];
1382
+ }
1383
+ var GENERIC_BUDGET = 240;
1384
+ function summarizeJsonObject(obj) {
1385
+ const keys = Object.keys(obj);
1386
+ if (keys.length === 0) return null;
1387
+ const priority = [
1388
+ "ok",
1389
+ "status",
1390
+ "timedOut",
1391
+ "stopReason",
1392
+ "reason",
1393
+ "error",
1394
+ "message",
1395
+ "result",
1396
+ "summary",
1397
+ "iterations",
1398
+ "toolCalls",
1399
+ "durationMs",
1400
+ "subagentId",
1401
+ "taskId"
1402
+ ];
1403
+ const ordered = [
1404
+ ...priority.filter((k) => keys.includes(k)),
1405
+ ...keys.filter((k) => !priority.includes(k))
1406
+ ];
1407
+ const parts = [];
1408
+ let used = 0;
1409
+ for (const key of ordered) {
1410
+ const v = obj[key];
1411
+ if (v === void 0 || v === null) continue;
1412
+ 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}`;
1413
+ if (used + rendered.length > GENERIC_BUDGET) {
1414
+ parts.push("\u2026");
1415
+ break;
1416
+ }
1417
+ parts.push(rendered);
1418
+ used += rendered.length + 3;
1419
+ }
1420
+ return parts.length > 0 ? parts.join(" \xB7 ") : null;
1125
1421
  }
1126
1422
  function firstNonEmpty(text) {
1127
1423
  if (!text) return void 0;
@@ -1167,20 +1463,29 @@ function extractDiffPreview(toolName, output) {
1167
1463
  }
1168
1464
  function parseUnifiedDiff(diff, maxLines) {
1169
1465
  const all = [];
1466
+ let oldLn = 0;
1467
+ let newLn = 0;
1170
1468
  for (const raw of diff.split("\n")) {
1171
1469
  const line = raw.replace(/\r$/, "");
1172
1470
  if (line.startsWith("+++") || line.startsWith("---")) continue;
1173
1471
  if (line.startsWith("diff --git") || line.startsWith("index ")) continue;
1174
1472
  if (line.startsWith("@@")) {
1473
+ const m = line.match(/^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
1474
+ if (m) {
1475
+ oldLn = Number.parseInt(m[1] ?? "0", 10) || 0;
1476
+ newLn = Number.parseInt(m[2] ?? "0", 10) || 0;
1477
+ }
1175
1478
  all.push({ kind: "hunk", text: truncMid(line, 60) });
1176
1479
  continue;
1177
1480
  }
1178
1481
  if (line.startsWith("+")) {
1179
- all.push({ kind: "add", text: truncMid(line, 100) });
1482
+ all.push({ kind: "add", text: truncMid(line, 100), newLine: newLn });
1483
+ newLn++;
1180
1484
  continue;
1181
1485
  }
1182
1486
  if (line.startsWith("-")) {
1183
- all.push({ kind: "del", text: truncMid(line, 100) });
1487
+ all.push({ kind: "del", text: truncMid(line, 100), oldLine: oldLn });
1488
+ oldLn++;
1184
1489
  continue;
1185
1490
  }
1186
1491
  if (line.startsWith("\\ No newline")) {
@@ -1188,7 +1493,9 @@ function parseUnifiedDiff(diff, maxLines) {
1188
1493
  continue;
1189
1494
  }
1190
1495
  if (line.length === 0) continue;
1191
- all.push({ kind: "ctx", text: truncMid(line, 100) });
1496
+ all.push({ kind: "ctx", text: truncMid(line, 100), oldLine: oldLn, newLine: newLn });
1497
+ oldLn++;
1498
+ newLn++;
1192
1499
  }
1193
1500
  if (all.length === 0) return { rows: [], hidden: 0 };
1194
1501
  if (all.length <= maxLines) return { rows: all, hidden: 0 };
@@ -1230,7 +1537,7 @@ function countLines(text) {
1230
1537
  if (!text) return 0;
1231
1538
  return text.replace(/\n$/, "").split("\n").length;
1232
1539
  }
1233
- function fmtBytes(n) {
1540
+ function fmtBytes3(n) {
1234
1541
  if (n < 1024) return `${n}B`;
1235
1542
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
1236
1543
  return `${(n / (1024 * 1024)).toFixed(1)}MB`;
@@ -1239,6 +1546,31 @@ function truncMid(s, max) {
1239
1546
  if (s.length <= max) return s;
1240
1547
  return `${s.slice(0, max - 1)}\u2026`;
1241
1548
  }
1549
+ function isHomeEnd(data) {
1550
+ if (data === "\x1B[H" || data === "\x1B[1~" || data === "\x1BOH" || data === "\x1B[7~")
1551
+ return "home";
1552
+ if (data === "\x1B[F" || data === "\x1B[4~" || data === "\x1BOF" || data === "\x1B[8~")
1553
+ return "end";
1554
+ return null;
1555
+ }
1556
+ var EMPTY_KEY = {
1557
+ upArrow: false,
1558
+ downArrow: false,
1559
+ leftArrow: false,
1560
+ rightArrow: false,
1561
+ return: false,
1562
+ escape: false,
1563
+ ctrl: false,
1564
+ meta: false,
1565
+ shift: false,
1566
+ tab: false,
1567
+ backspace: false,
1568
+ delete: false,
1569
+ pageUp: false,
1570
+ pageDown: false,
1571
+ home: false,
1572
+ end: false
1573
+ };
1242
1574
  function Input({
1243
1575
  prompt = "\u203A ",
1244
1576
  value,
@@ -1252,6 +1584,19 @@ function Input({
1252
1584
  if (disabled) return;
1253
1585
  onKey(input, key);
1254
1586
  });
1587
+ const { stdin } = useStdin();
1588
+ useEffect(() => {
1589
+ if (!stdin || disabled) return;
1590
+ const handleData = (data) => {
1591
+ const kind = isHomeEnd(data.toString());
1592
+ if (kind === "home") onKey("", { ...EMPTY_KEY, home: true });
1593
+ else if (kind === "end") onKey("", { ...EMPTY_KEY, end: true });
1594
+ };
1595
+ stdin.on("data", handleData);
1596
+ return () => {
1597
+ stdin.off("data", handleData);
1598
+ };
1599
+ }, [stdin, disabled, onKey]);
1255
1600
  const before = value.slice(0, cursor);
1256
1601
  const at = value.slice(cursor, cursor + 1) || " ";
1257
1602
  const after = value.slice(cursor + 1);
@@ -1348,6 +1693,9 @@ function StatusBar({
1348
1693
  yolo = false,
1349
1694
  elapsedMs,
1350
1695
  todos,
1696
+ plan,
1697
+ fleet,
1698
+ fleetAgents,
1351
1699
  git,
1352
1700
  subagentCount = 0,
1353
1701
  context,
@@ -1358,7 +1706,9 @@ function StatusBar({
1358
1706
  const cache2 = tokenCounter?.cacheStats();
1359
1707
  const stateColor = state === "idle" ? "cyan" : state === "aborting" ? "yellow" : "green";
1360
1708
  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;
1709
+ const hasSecondLine = yolo || elapsedMs !== void 0 || git !== null && git !== void 0 || projectName !== void 0 && projectName.length > 0;
1710
+ const fleetHasActivity = fleet && (fleet.running > 0 || fleet.idle > 0 || fleet.pending > 0 || fleet.completed > 0) || subagentCount > 0;
1711
+ const hasThirdLine = todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) || plan && (plan.open > 0 || plan.inProgress > 0 || plan.done > 0) || fleetHasActivity;
1362
1712
  return /* @__PURE__ */ jsxs(
1363
1713
  Box,
1364
1714
  {
@@ -1425,7 +1775,7 @@ function StatusBar({
1425
1775
  yolo ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }) : null,
1426
1776
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1427
1777
  "\u23F1 ",
1428
- fmtElapsed(elapsedMs)
1778
+ fmtElapsed2(elapsedMs)
1429
1779
  ] })
1430
1780
  ] }) : null,
1431
1781
  projectName ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1455,36 +1805,93 @@ function StatusBar({
1455
1805
  git.untracked
1456
1806
  ] }) : null
1457
1807
  ] })
1808
+ ] }) : null
1809
+ ] }) : null,
1810
+ hasThirdLine ? /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, children: [
1811
+ todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) ? /* @__PURE__ */ jsxs(Text, { children: [
1812
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "todos " }),
1813
+ todos.inProgress > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1814
+ "\u231B",
1815
+ todos.inProgress
1816
+ ] }) : null,
1817
+ todos.inProgress > 0 && (todos.pending > 0 || todos.completed > 0) ? " " : "",
1818
+ todos.pending > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1819
+ "\u2610",
1820
+ todos.pending
1821
+ ] }) : null,
1822
+ todos.pending > 0 && todos.completed > 0 ? " " : "",
1823
+ todos.completed > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1824
+ "\u2713",
1825
+ todos.completed
1826
+ ] }) : null
1458
1827
  ] }) : null,
1459
- todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) ? /* @__PURE__ */ jsxs(Fragment, { children: [
1460
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1828
+ plan && (plan.open > 0 || plan.inProgress > 0 || plan.done > 0) ? /* @__PURE__ */ jsxs(Fragment, { children: [
1829
+ todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }) : null,
1461
1830
  /* @__PURE__ */ jsxs(Text, { children: [
1462
- todos.inProgress > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1463
- "\u231B ",
1464
- todos.inProgress
1831
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u{1F4CB} " }),
1832
+ plan.inProgress > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1833
+ "\u231B",
1834
+ plan.inProgress
1465
1835
  ] }) : 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
1836
+ plan.inProgress > 0 && (plan.open > 0 || plan.done > 0) ? " " : "",
1837
+ plan.open > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1838
+ "\u2610",
1839
+ plan.open
1470
1840
  ] }) : null,
1471
- todos.pending > 0 && todos.completed > 0 ? " " : "",
1472
- todos.completed > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1473
- "\u2713 ",
1474
- todos.completed
1841
+ plan.open > 0 && plan.done > 0 ? " " : "",
1842
+ plan.done > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1843
+ "\u2713",
1844
+ plan.done
1475
1845
  ] }) : null
1476
1846
  ] })
1477
1847
  ] }) : null,
1478
- subagentCount > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1479
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1480
- /* @__PURE__ */ jsxs(Text, { color: "blue", children: [
1848
+ fleetHasActivity ? /* @__PURE__ */ jsxs(Fragment, { children: [
1849
+ 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,
1850
+ fleet ? /* @__PURE__ */ jsxs(Text, { children: [
1851
+ /* @__PURE__ */ jsx(Text, { color: "blue", children: "\u{1F310} " }),
1852
+ fleet.running > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1853
+ "\u25B6",
1854
+ fleet.running
1855
+ ] }) : null,
1856
+ fleet.running > 0 && (fleet.pending > 0 || fleet.idle > 0 || fleet.completed > 0) ? " " : "",
1857
+ fleet.pending > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1858
+ "\u2610",
1859
+ fleet.pending
1860
+ ] }) : null,
1861
+ fleet.pending > 0 && (fleet.idle > 0 || fleet.completed > 0) ? " " : "",
1862
+ fleet.idle > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1863
+ "\xB7",
1864
+ fleet.idle,
1865
+ "idle"
1866
+ ] }) : null,
1867
+ fleet.idle > 0 && fleet.completed > 0 ? " " : "",
1868
+ fleet.completed > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1869
+ "\u2713",
1870
+ fleet.completed
1871
+ ] }) : null
1872
+ ] }) : /* @__PURE__ */ jsxs(Text, { color: "blue", children: [
1481
1873
  "\u{1F310} ",
1482
1874
  subagentCount,
1483
1875
  " agent",
1484
1876
  subagentCount === 1 ? "" : "s"
1485
1877
  ] })
1486
1878
  ] }) : null
1487
- ] }) : null
1879
+ ] }) : null,
1880
+ fleetAgents && fleetAgents.length > 0 ? /* @__PURE__ */ jsx(Box, { flexDirection: "row", gap: 2, children: fleetAgents.map((a, i) => (
1881
+ // biome-ignore lint/suspicious/noArrayIndexKey: agent list is stable per render
1882
+ /* @__PURE__ */ jsxs(Text, { children: [
1883
+ /* @__PURE__ */ jsx(Text, { color: a.color, bold: true, children: a.label }),
1884
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
1885
+ /* @__PURE__ */ jsx(Text, { color: a.running ? "yellow" : void 0, dimColor: !a.running, children: a.running ? "\u25B6" : "\xB7" }),
1886
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
1887
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: fmtElapsed2(a.elapsedMs) }),
1888
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
1889
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1890
+ a.toolCalls,
1891
+ "t"
1892
+ ] })
1893
+ ] }, i)
1894
+ )) }) : null
1488
1895
  ]
1489
1896
  }
1490
1897
  );
@@ -1524,7 +1931,7 @@ function fmtTok2(n) {
1524
1931
  if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
1525
1932
  return `${(n / 1e6).toFixed(1)}M`;
1526
1933
  }
1527
- function fmtElapsed(ms) {
1934
+ function fmtElapsed2(ms) {
1528
1935
  const totalSec = Math.floor(ms / 1e3);
1529
1936
  const h = Math.floor(totalSec / 3600);
1530
1937
  const m = Math.floor(totalSec % 3600 / 60);
@@ -1782,6 +2189,10 @@ function reducer(state, action) {
1782
2189
  return { ...state, status: action.status };
1783
2190
  case "interrupt":
1784
2191
  return { ...state, interrupts: state.interrupts + 1 };
2192
+ case "steerStart":
2193
+ return { ...state, steeringPending: true, steerSnapshot: action.snapshot };
2194
+ case "steerConsume":
2195
+ return { ...state, steeringPending: false, steerSnapshot: null };
1785
2196
  case "resetInterrupts":
1786
2197
  return { ...state, interrupts: 0 };
1787
2198
  case "hint":
@@ -1982,9 +2393,274 @@ function reducer(state, action) {
1982
2393
  return { ...state, confirm: null };
1983
2394
  case "resetContextChip":
1984
2395
  return { ...state, contextChipVersion: state.contextChipVersion + 1 };
2396
+ // --- Fleet ---
2397
+ case "fleetSeed": {
2398
+ const seeded = {};
2399
+ for (const e of action.entries) {
2400
+ seeded[e.id] = {
2401
+ ...e,
2402
+ recentTools: e.recentTools ?? [],
2403
+ recentMessages: e.recentMessages ?? []
2404
+ };
2405
+ }
2406
+ return { ...state, fleet: seeded, fleetCost: action.cost };
2407
+ }
2408
+ case "fleetSpawn": {
2409
+ if (state.fleet[action.id]) return state;
2410
+ const entry = {
2411
+ id: action.id,
2412
+ name: action.name ?? action.id.slice(0, 8),
2413
+ provider: action.provider,
2414
+ model: action.model,
2415
+ status: "idle",
2416
+ streamingText: "",
2417
+ iterations: 0,
2418
+ toolCalls: 0,
2419
+ recentTools: [],
2420
+ recentMessages: [],
2421
+ cost: 0,
2422
+ startedAt: Date.now(),
2423
+ lastEventAt: Date.now(),
2424
+ transcriptPath: action.transcriptPath
2425
+ };
2426
+ return { ...state, fleet: { ...state.fleet, [action.id]: entry } };
2427
+ }
2428
+ case "fleetToolStart": {
2429
+ const cur = state.fleet[action.id];
2430
+ if (!cur) return state;
2431
+ return {
2432
+ ...state,
2433
+ fleet: {
2434
+ ...state.fleet,
2435
+ [action.id]: {
2436
+ ...cur,
2437
+ currentTool: { name: action.name, startedAt: Date.now() },
2438
+ lastEventAt: Date.now()
2439
+ }
2440
+ }
2441
+ };
2442
+ }
2443
+ case "fleetToolEnd": {
2444
+ const cur = state.fleet[action.id];
2445
+ if (!cur) return state;
2446
+ return {
2447
+ ...state,
2448
+ fleet: {
2449
+ ...state.fleet,
2450
+ [action.id]: { ...cur, currentTool: void 0, lastEventAt: Date.now() }
2451
+ }
2452
+ };
2453
+ }
2454
+ case "fleetStart": {
2455
+ const cur = state.fleet[action.id];
2456
+ if (!cur) return state;
2457
+ return {
2458
+ ...state,
2459
+ fleet: {
2460
+ ...state.fleet,
2461
+ [action.id]: {
2462
+ ...cur,
2463
+ status: "running",
2464
+ streamingText: "",
2465
+ startedAt: Date.now()
2466
+ }
2467
+ }
2468
+ };
2469
+ }
2470
+ case "fleetDelta": {
2471
+ const cur = state.fleet[action.id];
2472
+ if (!cur) return state;
2473
+ const appended = (cur.streamingText + action.text).slice(-200);
2474
+ return {
2475
+ ...state,
2476
+ fleet: {
2477
+ ...state.fleet,
2478
+ [action.id]: { ...cur, streamingText: appended, lastEventAt: Date.now() }
2479
+ }
2480
+ };
2481
+ }
2482
+ case "fleetMessage": {
2483
+ const cur = state.fleet[action.id];
2484
+ const text = action.text.trim().replace(/\s+/g, " ");
2485
+ if (!cur || !text) return state;
2486
+ const now = Date.now();
2487
+ const recentMessages = [...cur.recentMessages ?? [], { text, at: now }].slice(-2);
2488
+ return {
2489
+ ...state,
2490
+ fleet: {
2491
+ ...state.fleet,
2492
+ [action.id]: { ...cur, recentMessages, lastEventAt: now }
2493
+ }
2494
+ };
2495
+ }
2496
+ case "fleetTool": {
2497
+ const cur = state.fleet[action.id];
2498
+ if (!cur) return state;
2499
+ const now = Date.now();
2500
+ const recentTools = action.name !== void 0 ? [
2501
+ ...cur.recentTools ?? [],
2502
+ {
2503
+ name: action.name,
2504
+ ok: action.ok,
2505
+ durationMs: action.durationMs,
2506
+ outputBytes: action.outputBytes,
2507
+ outputLines: action.outputLines,
2508
+ at: now
2509
+ }
2510
+ ].slice(-2) : cur.recentTools ?? [];
2511
+ return {
2512
+ ...state,
2513
+ fleet: {
2514
+ ...state.fleet,
2515
+ [action.id]: {
2516
+ ...cur,
2517
+ toolCalls: cur.toolCalls + 1,
2518
+ recentTools,
2519
+ lastEventAt: now
2520
+ }
2521
+ }
2522
+ };
2523
+ }
2524
+ case "fleetUsage": {
2525
+ const cur = state.fleet[action.id];
2526
+ if (!cur) return state;
2527
+ const cost = cur.cost;
2528
+ return {
2529
+ ...state,
2530
+ fleet: { ...state.fleet, [action.id]: { ...cur, cost, lastEventAt: Date.now() } }
2531
+ };
2532
+ }
2533
+ case "fleetDone": {
2534
+ const cur = state.fleet[action.id];
2535
+ if (!cur) return state;
2536
+ return {
2537
+ ...state,
2538
+ fleet: {
2539
+ ...state.fleet,
2540
+ [action.id]: {
2541
+ ...cur,
2542
+ status: action.status,
2543
+ iterations: action.iterations,
2544
+ toolCalls: action.toolCalls,
2545
+ streamingText: "",
2546
+ currentTool: void 0,
2547
+ lastEventAt: Date.now()
2548
+ }
2549
+ }
2550
+ };
2551
+ }
2552
+ case "fleetCost": {
2553
+ return { ...state, fleetCost: action.cost };
2554
+ }
2555
+ case "setStreamFleet": {
2556
+ return { ...state, streamFleet: action.enabled };
2557
+ }
1985
2558
  }
1986
2559
  }
1987
2560
  var PASTE_THRESHOLD_CHARS = 200;
2561
+ function buildSteeringPreamble(snapshot, newDirection) {
2562
+ const lines = ["[STEERING \u2014 I pressed Esc to interrupt you mid-task on purpose.", ""];
2563
+ const ctx = [];
2564
+ if (snapshot?.runningTools && snapshot.runningTools.length > 0) {
2565
+ ctx.push(`- in-flight tools (now cancelled): ${snapshot.runningTools.join(", ")}`);
2566
+ }
2567
+ if (snapshot?.subagentsTerminated && snapshot.subagentsTerminated > 0) {
2568
+ const subDetails = snapshot.subagents.map((s) => `${s.label}${s.tool ? ` (was running: ${s.tool})` : ""}`).join(", ");
2569
+ ctx.push(
2570
+ `- subagents (${snapshot.subagentsTerminated} terminated by me, do NOT await them): ${subDetails}`
2571
+ );
2572
+ }
2573
+ if (snapshot?.partialAssistantText && snapshot.partialAssistantText.trim().length > 0) {
2574
+ const tail = snapshot.partialAssistantText.trim().slice(-300);
2575
+ ctx.push(`- your last partial output (truncated, for context only): "${tail}"`);
2576
+ }
2577
+ if (ctx.length > 0) {
2578
+ lines.push("What was happening when I cut you off:");
2579
+ lines.push(...ctx);
2580
+ lines.push("");
2581
+ }
2582
+ lines.push("You have authority to:");
2583
+ lines.push("- Abandon the prior plan entirely if the new direction makes it stale.");
2584
+ lines.push("- Re-spawn fresh subagents (with different roles or tasks) if needed.");
2585
+ lines.push('- Skip a polite "should I continue?" \u2014 just pivot.');
2586
+ lines.push("- Ask me to clarify if the new direction is genuinely ambiguous.");
2587
+ lines.push("");
2588
+ lines.push("New direction:");
2589
+ lines.push("---");
2590
+ lines.push(newDirection);
2591
+ lines.push("---");
2592
+ lines.push("]");
2593
+ return lines.join("\n");
2594
+ }
2595
+ function buildGoalPreamble(goal) {
2596
+ return [
2597
+ "[GOAL \u2014 LOCKED IN. You will work on this until it is verifiably done.",
2598
+ "The user granted you full autonomy. Read these constraints once, then act.",
2599
+ "",
2600
+ "YOUR GOAL:",
2601
+ "---",
2602
+ goal,
2603
+ "---",
2604
+ "",
2605
+ "AUTHORITY YOU HAVE:",
2606
+ "- Spawn as many subagents as the work needs (delegate / spawn_subagent).",
2607
+ " Parallel + recursive fan-out are both fine. There is no spawn budget.",
2608
+ "- Use any provider/model per subagent \u2014 pick the right tool for each",
2609
+ " piece of work. Heavy reasoning model for planning, fast model for",
2610
+ " batch work, specialist model for domain code.",
2611
+ "- Run unlimited tool calls and iterations. There is NO hidden budget.",
2612
+ " The Agent loop auto-extends every 100 iterations forever.",
2613
+ "- Retry failed tools with different inputs, alternative paths, fresh",
2614
+ " subagents. Switch providers mid-run if one is rate-limited.",
2615
+ "- Re-plan freely when an approach hits a dead end. You are not obliged",
2616
+ " to stick with the first plan you proposed.",
2617
+ "",
2618
+ 'WHAT "DONE" MEANS \u2014 non-negotiable:',
2619
+ "- You can name a concrete artifact (a passing test, a written file at",
2620
+ " a specific path, a fixed bug verified by re-running the failing case,",
2621
+ " a clean grep that previously had matches).",
2622
+ "- You can tell the user HOW to verify it themselves in 10 seconds.",
2623
+ '- You have NOT hedged. None of: "looks like it should work", "I',
2624
+ ' believe this fixes it", "the changes appear correct".',
2625
+ "",
2626
+ "WHAT IS NOT DONE \u2014 never report any of these as completion:",
2627
+ "- An error message you didn't recover from.",
2628
+ '- An empty result, a 0-line file, a "no matches found" you accepted',
2629
+ " without questioning the search.",
2630
+ '- "Should I continue?" / "Want me to also...?" / "Let me know if you',
2631
+ ' want X." Those are hedges. The user already told you to finish the',
2632
+ " goal \u2014 just do it.",
2633
+ "- Partial progress dressed up as success. Fixed 3 of 5 bugs = 60%",
2634
+ " done, not done.",
2635
+ "- A subagent's failed/timeout/stopped TaskResult that you didn't",
2636
+ " respond to with a fresh attempt (different role, different model,",
2637
+ " tighter prompt).",
2638
+ "",
2639
+ "PERSISTENCE PROTOCOL:",
2640
+ "- If blocked, try at least 3 different angles before reporting the",
2641
+ " problem to the user. Different tool inputs, different subagent",
2642
+ " roles, different providers, different decomposition of the task.",
2643
+ "- If a tool fails, read its error, alter the input, try again. Do",
2644
+ " not just report the failure back.",
2645
+ "- If a subagent returns useless output, respawn with a tighter prompt",
2646
+ ' or a different role. Do not accept "I could not determine\u2026" as the',
2647
+ " final answer.",
2648
+ "- Use `ask_subagent` for one-shot questions when you don't need a",
2649
+ " full delegated task.",
2650
+ "",
2651
+ "REPORTING:",
2652
+ "- Stream short progress notes between major actions so the user can",
2653
+ " monitor. Do not go silent for 50 tool calls then dump a wall of",
2654
+ " text \u2014 but also do not narrate every tool call.",
2655
+ "- Use the shared scratchpad (if available) to leave breadcrumbs",
2656
+ " subagents can read.",
2657
+ "- Final response must include: (a) what was accomplished, (b) how",
2658
+ " to verify, (c) any caveats (residual TODOs, things the user",
2659
+ " should know about).",
2660
+ "",
2661
+ "BEGIN.]"
2662
+ ].join("\n");
2663
+ }
1988
2664
  function App({
1989
2665
  agent,
1990
2666
  slashRegistry,
@@ -2003,7 +2679,12 @@ function App({
2003
2679
  switchProviderAndModel,
2004
2680
  effectiveMaxContext,
2005
2681
  onExit,
2006
- onClearHistory
2682
+ director,
2683
+ fleetRoster,
2684
+ onClearHistory,
2685
+ fleetStreamController,
2686
+ initialGoal,
2687
+ initialAsk
2007
2688
  }) {
2008
2689
  const { exit } = useApp();
2009
2690
  const [liveModel, setLiveModel] = useState(model);
@@ -2028,6 +2709,8 @@ function App({
2028
2709
  toolStream: null,
2029
2710
  status: "idle",
2030
2711
  interrupts: 0,
2712
+ steeringPending: false,
2713
+ steerSnapshot: null,
2031
2714
  hint: "",
2032
2715
  nextId: 1,
2033
2716
  picker: { open: false, query: "", matches: [], selected: 0 },
@@ -2045,13 +2728,18 @@ function App({
2045
2728
  selected: 0
2046
2729
  },
2047
2730
  confirm: null,
2048
- contextChipVersion: 0
2731
+ contextChipVersion: 0,
2732
+ fleet: {},
2733
+ fleetCost: 0,
2734
+ streamFleet: true
2049
2735
  });
2050
2736
  const builderRef = useRef(null);
2051
2737
  if (builderRef.current === null) {
2052
2738
  builderRef.current = new InputBuilder({ store: attachments });
2053
2739
  }
2054
2740
  const activeCtrlRef = useRef(null);
2741
+ const inputGateRef = useRef(false);
2742
+ const lastEnterAtRef = useRef(0);
2055
2743
  const projectRoot = agent.ctx.projectRoot;
2056
2744
  const projectName = React.useMemo(() => {
2057
2745
  const base = path3.basename(projectRoot);
@@ -2108,6 +2796,111 @@ function App({
2108
2796
  }
2109
2797
  return counts;
2110
2798
  }, [nowTick, agent.ctx.todos]);
2799
+ const fleetCounts = useMemo(() => {
2800
+ const entries = Object.values(state.fleet);
2801
+ if (entries.length === 0) return void 0;
2802
+ let running = 0;
2803
+ let idle = 0;
2804
+ let completed = 0;
2805
+ for (const e of entries) {
2806
+ if (e.status === "running") running += 1;
2807
+ else if (e.status === "idle") idle += 1;
2808
+ else completed += 1;
2809
+ }
2810
+ return { running, idle, pending: 0, completed };
2811
+ }, [state.fleet]);
2812
+ const STREAM_COLORS = ["cyan", "magenta", "yellow", "green", "blue"];
2813
+ const labelsRef = useRef(/* @__PURE__ */ new Map());
2814
+ const labelFor = (id, name) => {
2815
+ const m = labelsRef.current;
2816
+ const existing = m.get(id);
2817
+ if (existing) return existing;
2818
+ const n = m.size + 1;
2819
+ const suffix = name && name !== id ? ` ${name}` : "";
2820
+ const v = {
2821
+ label: `AGENT#${n}${suffix}`,
2822
+ color: STREAM_COLORS[(n - 1) % STREAM_COLORS.length]
2823
+ };
2824
+ m.set(id, v);
2825
+ return v;
2826
+ };
2827
+ const fleetAgents = useMemo(() => {
2828
+ const entries = Object.entries(state.fleet);
2829
+ if (entries.length === 0) return void 0;
2830
+ const active = entries.filter(([_id, e]) => e.status === "running" || e.status === "idle");
2831
+ if (active.length === 0) return void 0;
2832
+ active.sort((a, b) => {
2833
+ const sa = a[1].status === "running" ? 0 : 1;
2834
+ const sb = b[1].status === "running" ? 0 : 1;
2835
+ if (sa !== sb) return sa - sb;
2836
+ return a[1].startedAt - b[1].startedAt;
2837
+ });
2838
+ return active.slice(0, 4).map(([id, e]) => {
2839
+ const lbl = labelFor(id, e.name);
2840
+ return {
2841
+ label: lbl.label,
2842
+ color: lbl.color,
2843
+ elapsedMs: Math.max(0, nowTick - e.startedAt),
2844
+ toolCalls: e.toolCalls,
2845
+ running: e.status === "running"
2846
+ };
2847
+ });
2848
+ }, [state.fleet, nowTick]);
2849
+ const [planCounts, setPlanCounts] = useState(null);
2850
+ useEffect(() => {
2851
+ const planPath = agent.ctx.meta["plan.path"];
2852
+ if (typeof planPath !== "string" || !planPath) return;
2853
+ let cancelled = false;
2854
+ const poll = async () => {
2855
+ try {
2856
+ const data = await fs.readFile(planPath, "utf8");
2857
+ const parsed = JSON.parse(data);
2858
+ if (cancelled) return;
2859
+ if (!Array.isArray(parsed.items)) {
2860
+ setPlanCounts(null);
2861
+ return;
2862
+ }
2863
+ let open = 0;
2864
+ let inProgress = 0;
2865
+ let done = 0;
2866
+ for (const it of parsed.items) {
2867
+ if (it?.status === "done") done++;
2868
+ else if (it?.status === "in_progress") inProgress++;
2869
+ else open++;
2870
+ }
2871
+ setPlanCounts(open + inProgress + done > 0 ? { open, inProgress, done } : null);
2872
+ } catch {
2873
+ if (!cancelled) setPlanCounts(null);
2874
+ }
2875
+ };
2876
+ void poll();
2877
+ const id = setInterval(poll, 3e3);
2878
+ return () => {
2879
+ cancelled = true;
2880
+ clearInterval(id);
2881
+ };
2882
+ }, [agent.ctx.meta]);
2883
+ const prevAnyOverlayOpen = useRef(false);
2884
+ const prevEntriesCount = useRef(0);
2885
+ useEffect(() => {
2886
+ const anyOpenNow = state.picker.open || state.slashPicker.open || state.modelPicker.open || !!state.confirm;
2887
+ const overlayClosed = prevAnyOverlayOpen.current && !anyOpenNow;
2888
+ const newEntryCommitted = state.entries.length > prevEntriesCount.current;
2889
+ prevAnyOverlayOpen.current = anyOpenNow;
2890
+ prevEntriesCount.current = state.entries.length;
2891
+ if (overlayClosed || newEntryCommitted) {
2892
+ try {
2893
+ process.stdout.write("\x1B[J");
2894
+ } catch {
2895
+ }
2896
+ }
2897
+ }, [
2898
+ state.picker.open,
2899
+ state.slashPicker.open,
2900
+ state.modelPicker.open,
2901
+ state.confirm,
2902
+ state.entries.length
2903
+ ]);
2111
2904
  useEffect(() => {
2112
2905
  const detected = detectAtToken(state.buffer, state.cursor);
2113
2906
  if (!detected) {
@@ -2267,6 +3060,128 @@ function App({
2267
3060
  slashRegistry.unregister("queue");
2268
3061
  };
2269
3062
  }, [slashRegistry]);
3063
+ useEffect(() => {
3064
+ const ALT_OFF = "\x1B[?1049l";
3065
+ const ALT_ON = "\x1B[?1049h";
3066
+ const cmd = {
3067
+ name: "altscreen",
3068
+ description: "Toggle the alt-screen buffer. Default is OFF (native scroll); /altscreen on for full-screen mode.",
3069
+ async run(args) {
3070
+ const arg = args.trim().toLowerCase();
3071
+ if (arg === "off") {
3072
+ try {
3073
+ process.stdout.write(ALT_OFF);
3074
+ } catch {
3075
+ return { message: "Failed to exit alt-screen." };
3076
+ }
3077
+ return {
3078
+ 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."
3079
+ };
3080
+ }
3081
+ if (arg === "on") {
3082
+ try {
3083
+ process.stdout.write(ALT_ON);
3084
+ } catch {
3085
+ return { message: "Failed to re-enter alt-screen." };
3086
+ }
3087
+ return { message: "Alt-screen re-enabled. Native scroll is now disabled." };
3088
+ }
3089
+ return { message: "Usage: /altscreen on|off" };
3090
+ }
3091
+ };
3092
+ slashRegistry.register(cmd);
3093
+ return () => {
3094
+ slashRegistry.unregister("altscreen");
3095
+ };
3096
+ }, [slashRegistry]);
3097
+ useEffect(() => {
3098
+ const cmd = {
3099
+ name: "steer",
3100
+ description: "Interrupt the running agent (incl. fleet) and redirect: /steer <new direction>",
3101
+ help: [
3102
+ "Usage: /steer <new direction>",
3103
+ "",
3104
+ "Aborts the active iteration, terminates any running subagents,",
3105
+ "drops queued messages, and sends your text to the model with a",
3106
+ "STEERING preamble explaining what was in flight and what the",
3107
+ "model is authorised to do (pivot hard, respawn subagents, ask",
3108
+ "for clarification). Equivalent to pressing Esc then typing."
3109
+ ].join("\n"),
3110
+ async run(args) {
3111
+ const text = args.trim();
3112
+ if (!text) {
3113
+ return { message: "Usage: /steer <new direction>" };
3114
+ }
3115
+ const s = stateRef.current;
3116
+ const runningTools = Array.from(s.runningTools.values()).map((t) => t.name);
3117
+ const subagents = Object.values(s.fleet).filter((e) => e.status === "running").map((e) => ({ label: e.name, status: e.status, tool: e.currentTool?.name }));
3118
+ const subagentsTerminated = subagents.length;
3119
+ const partialAssistantText = streamingTextRef.current.slice(-1500);
3120
+ activeCtrlRef.current?.abort();
3121
+ dispatch({
3122
+ type: "steerStart",
3123
+ snapshot: { runningTools, subagents, subagentsTerminated, partialAssistantText }
3124
+ });
3125
+ const droppedCount = s.queue.length;
3126
+ if (droppedCount > 0) dispatch({ type: "queueClear" });
3127
+ if (director && subagentsTerminated > 0) {
3128
+ const cap = new Promise((resolve) => {
3129
+ const t = setTimeout(resolve, 1500);
3130
+ t.unref?.();
3131
+ });
3132
+ void Promise.race([director.terminateAll().catch(() => void 0), cap]);
3133
+ }
3134
+ const preamble = buildSteeringPreamble(
3135
+ { runningTools, subagents, subagentsTerminated, partialAssistantText },
3136
+ text
3137
+ );
3138
+ dispatch({ type: "steerConsume" });
3139
+ const droppedTag = droppedCount > 0 ? ` \xB7 dropped ${droppedCount} queued` : "";
3140
+ const fleetTag = subagentsTerminated > 0 ? ` \xB7 stopped ${subagentsTerminated} subagent${subagentsTerminated === 1 ? "" : "s"}` : "";
3141
+ return {
3142
+ message: `\u21AF Steering${droppedTag}${fleetTag}.`,
3143
+ runText: preamble
3144
+ };
3145
+ }
3146
+ };
3147
+ slashRegistry.register(cmd);
3148
+ return () => {
3149
+ slashRegistry.unregister("steer");
3150
+ };
3151
+ }, [slashRegistry, director]);
3152
+ useEffect(() => {
3153
+ const cmd = {
3154
+ name: "goal",
3155
+ description: "Lock in a goal \u2014 no budgets, no hedging, no premature done. /goal <description>",
3156
+ help: [
3157
+ "Usage: /goal <description>",
3158
+ "",
3159
+ "Hands the agent a task it must drive to a verifiable finish.",
3160
+ "Adds a preamble to the next turn that grants full autonomy",
3161
+ "(unlimited subagents, any provider/model, retry-until-it-works),",
3162
+ 'spells out what "done" actually means, and forbids hedge-style',
3163
+ 'completions ("I believe this works", "should I continue?").',
3164
+ "",
3165
+ "Combine with /steer to redirect mid-goal, or Ctrl+C / /fleet kill",
3166
+ "to bail out \u2014 only the user can stop a /goal."
3167
+ ].join("\n"),
3168
+ async run(args) {
3169
+ const goal = args.trim();
3170
+ if (!goal) return { message: "Usage: /goal <description>" };
3171
+ const preamble = buildGoalPreamble(goal);
3172
+ const shortGoal = goal.length > 80 ? `${goal.slice(0, 80)}\u2026` : goal;
3173
+ return {
3174
+ message: `\u{1F3AF} Goal locked: ${shortGoal}
3175
+ Agent will work until verifiably complete. Esc / /steer to redirect, Ctrl+C to stop.`,
3176
+ runText: preamble
3177
+ };
3178
+ }
3179
+ };
3180
+ slashRegistry.register(cmd);
3181
+ return () => {
3182
+ slashRegistry.unregister("goal");
3183
+ };
3184
+ }, [slashRegistry]);
2270
3185
  useEffect(() => {
2271
3186
  if (!getPickableProviders || !switchProviderAndModel) return;
2272
3187
  const cmd = {
@@ -2397,18 +3312,285 @@ function App({
2397
3312
  offConfirmNeeded();
2398
3313
  if (flushTimerRef.current) clearTimeout(flushTimerRef.current);
2399
3314
  };
2400
- }, [events]);
3315
+ }, [events, agent.ctx.todos]);
3316
+ const streamFleetRef = useRef(state.streamFleet);
3317
+ useEffect(() => {
3318
+ streamFleetRef.current = state.streamFleet;
3319
+ }, [state.streamFleet]);
3320
+ useEffect(() => {
3321
+ const offSpawned = events.on("subagent.spawned", (e) => {
3322
+ const lbl = labelFor(e.subagentId, e.name);
3323
+ dispatch({
3324
+ type: "fleetSpawn",
3325
+ id: e.subagentId,
3326
+ name: e.name,
3327
+ provider: e.provider,
3328
+ model: e.model,
3329
+ transcriptPath: e.transcriptPath
3330
+ });
3331
+ const where = e.provider && e.model ? `${e.provider}/${e.model}` : "spawned";
3332
+ const desc = e.description ? ` \u2014 ${e.description.slice(0, 80)}` : "";
3333
+ dispatch({
3334
+ type: "addEntry",
3335
+ entry: {
3336
+ kind: "subagent",
3337
+ agentLabel: lbl.label,
3338
+ agentColor: lbl.color,
3339
+ icon: "\u25B6",
3340
+ text: `${where}${desc}`
3341
+ }
3342
+ });
3343
+ });
3344
+ const offStarted = events.on("subagent.task_started", (e) => {
3345
+ const lbl = labelFor(e.subagentId);
3346
+ dispatch({ type: "fleetStart", id: e.subagentId, taskId: e.taskId });
3347
+ const desc = e.description ? ` \u2014 ${e.description.slice(0, 80)}` : "";
3348
+ dispatch({
3349
+ type: "addEntry",
3350
+ entry: {
3351
+ kind: "subagent",
3352
+ agentLabel: lbl.label,
3353
+ agentColor: lbl.color,
3354
+ icon: "\u25CF",
3355
+ text: `task started${desc}`
3356
+ }
3357
+ });
3358
+ });
3359
+ const offCompleted = events.on("subagent.task_completed", (e) => {
3360
+ const lbl = labelFor(e.subagentId);
3361
+ dispatch({
3362
+ type: "fleetDone",
3363
+ id: e.subagentId,
3364
+ status: e.status,
3365
+ iterations: e.iterations,
3366
+ toolCalls: e.toolCalls
3367
+ });
3368
+ const icon = e.status === "success" ? "\u2713" : e.status === "timeout" ? "\u23F1" : e.status === "stopped" ? "\u2298" : "\u2717";
3369
+ const errKind = e.error?.kind;
3370
+ const errMsg = e.error?.message;
3371
+ const errMsgTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 100)}${errMsg.length > 100 ? "\u2026" : ""}` : "";
3372
+ const errChip = errKind ? ` [${errKind}]` : "";
3373
+ const secs = (e.durationMs / 1e3).toFixed(e.durationMs < 1e4 ? 1 : 0);
3374
+ dispatch({
3375
+ type: "addEntry",
3376
+ entry: {
3377
+ kind: "subagent",
3378
+ agentLabel: lbl.label,
3379
+ agentColor: lbl.color,
3380
+ icon,
3381
+ text: `${e.status} (${e.iterations} iter \xB7 ${e.toolCalls} tools \xB7 ${secs}s)${errChip}${errMsgTail}`
3382
+ }
3383
+ });
3384
+ });
3385
+ const offTool = events.on("subagent.tool_executed", (e) => {
3386
+ if (director) return;
3387
+ dispatch({
3388
+ type: "fleetTool",
3389
+ id: e.subagentId,
3390
+ name: e.name,
3391
+ ok: e.ok,
3392
+ durationMs: e.durationMs,
3393
+ outputBytes: e.outputBytes
3394
+ });
3395
+ dispatch({ type: "fleetToolEnd", id: e.subagentId });
3396
+ });
3397
+ return () => {
3398
+ offSpawned();
3399
+ offStarted();
3400
+ offCompleted();
3401
+ offTool();
3402
+ };
3403
+ }, [events, director]);
3404
+ useEffect(() => {
3405
+ if (!fleetStreamController) return;
3406
+ fleetStreamController.enabled = state.streamFleet;
3407
+ fleetStreamController.setEnabled = (enabled) => {
3408
+ dispatch({ type: "setStreamFleet", enabled });
3409
+ };
3410
+ return () => {
3411
+ fleetStreamController.setEnabled = (enabled) => {
3412
+ fleetStreamController.enabled = enabled;
3413
+ };
3414
+ };
3415
+ }, [fleetStreamController, state.streamFleet]);
3416
+ useEffect(() => {
3417
+ if (fleetStreamController) fleetStreamController.enabled = state.streamFleet;
3418
+ }, [state.streamFleet, fleetStreamController]);
3419
+ useEffect(() => {
3420
+ const d = director;
3421
+ if (!d) return;
3422
+ const FLUSH_MS = 150;
3423
+ const streamBuf = /* @__PURE__ */ new Map();
3424
+ let streamFlushTimer = null;
3425
+ const flushStreamBufs = () => {
3426
+ for (const [id, text] of streamBuf) {
3427
+ const trimmed = text.trim();
3428
+ if (!trimmed) continue;
3429
+ const lbl = labelFor(id);
3430
+ dispatch({ type: "fleetMessage", id, text: trimmed });
3431
+ if (streamFleetRef.current) {
3432
+ dispatch({
3433
+ type: "addEntry",
3434
+ entry: {
3435
+ kind: "subagent",
3436
+ agentLabel: lbl.label,
3437
+ agentColor: lbl.color,
3438
+ icon: "\u{1F4AC}",
3439
+ text: trimmed
3440
+ }
3441
+ });
3442
+ }
3443
+ }
3444
+ streamBuf.clear();
3445
+ streamFlushTimer = null;
3446
+ };
3447
+ const status = d.status();
3448
+ for (const s of status.subagents) {
3449
+ const meta = d.getSubagentMeta(s.id);
3450
+ dispatch({
3451
+ type: "fleetSpawn",
3452
+ id: s.id,
3453
+ name: meta?.name ?? s.name,
3454
+ provider: meta?.provider,
3455
+ model: meta?.model
3456
+ });
3457
+ labelFor(s.id, meta?.name ?? s.name);
3458
+ }
3459
+ dispatch({ type: "fleetCost", cost: d.snapshot().total.cost });
3460
+ const seen = new Set(Object.keys(status.subagents));
3461
+ const pending = /* @__PURE__ */ new Map();
3462
+ let flushTimer = null;
3463
+ const doFlush = () => {
3464
+ for (const [id, text] of pending) {
3465
+ if (text) dispatch({ type: "fleetDelta", id, text });
3466
+ }
3467
+ pending.clear();
3468
+ flushTimer = null;
3469
+ };
3470
+ const offFleet = d.fleet.onAny((e) => {
3471
+ const fresh = !seen.has(e.subagentId);
3472
+ if (fresh) {
3473
+ seen.add(e.subagentId);
3474
+ const meta = d.getSubagentMeta(e.subagentId);
3475
+ dispatch({
3476
+ type: "fleetSpawn",
3477
+ id: e.subagentId,
3478
+ name: meta?.name,
3479
+ provider: meta?.provider,
3480
+ model: meta?.model
3481
+ });
3482
+ const lbl = labelFor(e.subagentId, meta?.name);
3483
+ if (streamFleetRef.current) {
3484
+ const where = meta?.provider && meta?.model ? `${meta.provider}/${meta.model}` : "spawned";
3485
+ dispatch({
3486
+ type: "addEntry",
3487
+ entry: {
3488
+ kind: "subagent",
3489
+ agentLabel: lbl.label,
3490
+ agentColor: lbl.color,
3491
+ icon: "\u25B6",
3492
+ text: where
3493
+ }
3494
+ });
3495
+ }
3496
+ }
3497
+ switch (e.type) {
3498
+ case "iteration.started":
3499
+ dispatch({ type: "fleetStart", id: e.subagentId });
3500
+ break;
3501
+ case "provider.text_delta": {
3502
+ const p = e.payload;
3503
+ if (p?.text) {
3504
+ const cur = pending.get(e.subagentId) ?? "";
3505
+ pending.set(e.subagentId, cur + p.text);
3506
+ if (!flushTimer) flushTimer = setTimeout(doFlush, FLUSH_MS);
3507
+ streamBuf.set(e.subagentId, (streamBuf.get(e.subagentId) ?? "") + p.text);
3508
+ if (streamFlushTimer) clearTimeout(streamFlushTimer);
3509
+ streamFlushTimer = setTimeout(flushStreamBufs, FLUSH_MS * 4);
3510
+ }
3511
+ break;
3512
+ }
3513
+ case "tool.started": {
3514
+ const p = e.payload;
3515
+ if (p?.name) {
3516
+ dispatch({ type: "fleetToolStart", id: e.subagentId, name: p.name });
3517
+ }
3518
+ break;
3519
+ }
3520
+ case "tool.executed": {
3521
+ const p = e.payload;
3522
+ dispatch({
3523
+ type: "fleetTool",
3524
+ id: e.subagentId,
3525
+ name: p?.name,
3526
+ ok: p?.ok,
3527
+ durationMs: p?.durationMs,
3528
+ outputBytes: p?.outputBytes,
3529
+ outputLines: p?.outputLines
3530
+ });
3531
+ dispatch({ type: "fleetToolEnd", id: e.subagentId });
3532
+ break;
3533
+ }
3534
+ case "provider.response": {
3535
+ dispatch({ type: "fleetCost", cost: d.snapshot().total.cost });
3536
+ break;
3537
+ }
3538
+ }
3539
+ });
3540
+ const offDone = d.on("task.completed", (payload) => {
3541
+ dispatch({
3542
+ type: "fleetDone",
3543
+ id: payload.result.subagentId,
3544
+ status: payload.result.status,
3545
+ iterations: payload.result.iterations,
3546
+ toolCalls: payload.result.toolCalls
3547
+ });
3548
+ dispatch({ type: "fleetCost", cost: d.snapshot().total.cost });
3549
+ if (streamFlushTimer) {
3550
+ clearTimeout(streamFlushTimer);
3551
+ flushStreamBufs();
3552
+ }
3553
+ });
3554
+ return () => {
3555
+ offFleet();
3556
+ offDone();
3557
+ if (flushTimer) clearTimeout(flushTimer);
3558
+ doFlush();
3559
+ if (streamFlushTimer) clearTimeout(streamFlushTimer);
3560
+ flushStreamBufs();
3561
+ };
3562
+ }, [director]);
2401
3563
  useEffect(() => {
2402
3564
  const onSigint = () => {
2403
- if (state.interrupts >= 1 && state.status === "idle") {
2404
- exit();
2405
- onExit(130);
3565
+ if (state.interrupts >= 1) {
3566
+ if (state.interrupts >= 2) {
3567
+ process.exit(130);
3568
+ }
3569
+ try {
3570
+ exit();
3571
+ onExit(130);
3572
+ } catch {
3573
+ }
3574
+ setTimeout(() => {
3575
+ try {
3576
+ process.exit(130);
3577
+ } catch {
3578
+ }
3579
+ }, 500).unref?.();
3580
+ dispatch({ type: "interrupt" });
2406
3581
  return;
2407
3582
  }
2408
3583
  dispatch({ type: "interrupt" });
2409
3584
  if (activeCtrlRef.current) {
2410
3585
  activeCtrlRef.current.abort();
2411
3586
  dispatch({ type: "status", status: "aborting" });
3587
+ if (director) {
3588
+ const cap = new Promise((resolve) => {
3589
+ const t = setTimeout(resolve, 1500);
3590
+ t.unref?.();
3591
+ });
3592
+ void Promise.race([director.terminateAll().catch(() => void 0), cap]);
3593
+ }
2412
3594
  const droppedCount = stateRef.current.queue.length;
2413
3595
  if (droppedCount > 0) {
2414
3596
  dispatch({ type: "queueClear" });
@@ -2416,13 +3598,16 @@ function App({
2416
3598
  type: "addEntry",
2417
3599
  entry: {
2418
3600
  kind: "warn",
2419
- text: `Iteration cancelled. Dropped ${droppedCount} queued message${droppedCount === 1 ? "" : "s"}. Press Ctrl+C again to exit.`
3601
+ text: `Iteration cancelled${director ? " + fleet terminated" : ""}. Dropped ${droppedCount} queued message${droppedCount === 1 ? "" : "s"}. Press Ctrl+C again to exit.`
2420
3602
  }
2421
3603
  });
2422
3604
  } else {
2423
3605
  dispatch({
2424
3606
  type: "addEntry",
2425
- entry: { kind: "warn", text: "Iteration cancelled. Press Ctrl+C again to exit." }
3607
+ entry: {
3608
+ kind: "warn",
3609
+ text: `Iteration cancelled${director ? " + fleet terminated" : ""}. Press Ctrl+C again to exit.`
3610
+ }
2426
3611
  });
2427
3612
  }
2428
3613
  } else {
@@ -2436,9 +3621,11 @@ function App({
2436
3621
  return () => {
2437
3622
  process.off("SIGINT", onSigint);
2438
3623
  };
2439
- }, [state.interrupts, state.status, exit, onExit]);
3624
+ }, [state.interrupts, exit, onExit, director]);
2440
3625
  const handleKey = async (input, key) => {
2441
3626
  if (state.status === "aborting") return;
3627
+ if (inputGateRef.current) return;
3628
+ const isEnter = key.return || input === "\r" || input === "\n";
2442
3629
  if (state.modelPicker.open) {
2443
3630
  if (key.escape) {
2444
3631
  if (state.modelPicker.step === "model") {
@@ -2456,33 +3643,38 @@ function App({
2456
3643
  dispatch({ type: "modelPickerMove", delta: 1 });
2457
3644
  return;
2458
3645
  }
2459
- if (key.return) {
2460
- if (state.modelPicker.step === "provider") {
2461
- const opt = state.modelPicker.providerOptions[state.modelPicker.selected];
2462
- if (!opt) return;
3646
+ if (isEnter) {
3647
+ inputGateRef.current = true;
3648
+ try {
3649
+ if (state.modelPicker.step === "provider") {
3650
+ const opt = state.modelPicker.providerOptions[state.modelPicker.selected];
3651
+ if (!opt) return;
3652
+ dispatch({
3653
+ type: "modelPickerPickProvider",
3654
+ providerId: opt.id,
3655
+ models: opt.models
3656
+ });
3657
+ return;
3658
+ }
3659
+ const providerId = state.modelPicker.pickedProviderId;
3660
+ const modelId = state.modelPicker.modelOptions[state.modelPicker.selected];
3661
+ if (!providerId || !modelId) return;
3662
+ const err = switchProviderAndModel?.(providerId, modelId);
3663
+ if (err) {
3664
+ dispatch({ type: "modelPickerHint", text: err });
3665
+ return;
3666
+ }
3667
+ setLiveProvider(providerId);
3668
+ setLiveModel(modelId);
2463
3669
  dispatch({
2464
- type: "modelPickerPickProvider",
2465
- providerId: opt.id,
2466
- models: opt.models
3670
+ type: "addEntry",
3671
+ entry: { kind: "info", text: `Switched to ${providerId} / ${modelId}.` }
2467
3672
  });
3673
+ dispatch({ type: "modelPickerClose" });
2468
3674
  return;
3675
+ } finally {
3676
+ inputGateRef.current = false;
2469
3677
  }
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
3678
  }
2487
3679
  return;
2488
3680
  }
@@ -2499,8 +3691,10 @@ function App({
2499
3691
  dispatch({ type: "slashPickerMove", delta: 1 });
2500
3692
  return;
2501
3693
  }
2502
- if (key.return) {
2503
- await acceptSlashPickerSelection();
3694
+ if (isEnter) {
3695
+ inputGateRef.current = true;
3696
+ acceptSlashPickerSelection();
3697
+ inputGateRef.current = false;
2504
3698
  return;
2505
3699
  }
2506
3700
  if (key.tab && state.slashPicker.matches.length > 0) {
@@ -2525,13 +3719,61 @@ function App({
2525
3719
  dispatch({ type: "pickerMove", delta: 1 });
2526
3720
  return;
2527
3721
  }
2528
- if (key.return) {
2529
- await acceptPickerSelection();
3722
+ if (isEnter) {
3723
+ inputGateRef.current = true;
3724
+ try {
3725
+ await acceptPickerSelection();
3726
+ } finally {
3727
+ inputGateRef.current = false;
3728
+ }
2530
3729
  return;
2531
3730
  }
2532
3731
  }
2533
- if (key.return) {
2534
- await submit();
3732
+ if (key.escape && state.status !== "idle" && !state.confirm) {
3733
+ const runningTools = Array.from(state.runningTools.values()).map((t) => t.name);
3734
+ const subagents = Object.values(state.fleet).filter((e) => e.status === "running").map((e) => ({
3735
+ label: e.name,
3736
+ status: e.status,
3737
+ tool: e.currentTool?.name
3738
+ }));
3739
+ const subagentsTerminated = subagents.length;
3740
+ const partialAssistantText = streamingTextRef.current.slice(-1500);
3741
+ activeCtrlRef.current?.abort();
3742
+ dispatch({ type: "status", status: "aborting" });
3743
+ dispatch({
3744
+ type: "steerStart",
3745
+ snapshot: {
3746
+ runningTools,
3747
+ subagents,
3748
+ subagentsTerminated,
3749
+ partialAssistantText
3750
+ }
3751
+ });
3752
+ if (director && subagentsTerminated > 0) {
3753
+ const cap = new Promise((resolve) => {
3754
+ const t = setTimeout(resolve, 1500);
3755
+ t.unref?.();
3756
+ });
3757
+ void Promise.race([director.terminateAll().catch(() => void 0), cap]);
3758
+ }
3759
+ const droppedCount = state.queue.length;
3760
+ if (droppedCount > 0) dispatch({ type: "queueClear" });
3761
+ const droppedTag = droppedCount > 0 ? ` \xB7 dropped ${droppedCount} queued` : "";
3762
+ const fleetTag = subagentsTerminated > 0 ? ` \xB7 stopped ${subagentsTerminated} subagent${subagentsTerminated === 1 ? "" : "s"}` : "";
3763
+ dispatch({
3764
+ type: "addEntry",
3765
+ entry: {
3766
+ kind: "warn",
3767
+ text: `\u21AF Interrupted${droppedTag}${fleetTag}. Type your new direction.`
3768
+ }
3769
+ });
3770
+ return;
3771
+ }
3772
+ if (isEnter) {
3773
+ const now = Date.now();
3774
+ if (now - lastEnterAtRef.current < 50) return;
3775
+ lastEnterAtRef.current = now;
3776
+ void submit();
2535
3777
  return;
2536
3778
  }
2537
3779
  if (key.backspace || key.delete) {
@@ -2586,6 +3828,14 @@ function App({
2586
3828
  dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor + 1 });
2587
3829
  return;
2588
3830
  }
3831
+ if (key.home) {
3832
+ dispatch({ type: "setBuffer", buffer: state.buffer, cursor: 0 });
3833
+ return;
3834
+ }
3835
+ if (key.end) {
3836
+ dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.buffer.length });
3837
+ return;
3838
+ }
2589
3839
  if (key.upArrow) {
2590
3840
  if (state.inputHistory.length > 0) dispatch({ type: "historyUp" });
2591
3841
  return;
@@ -2725,6 +3975,18 @@ function App({
2725
3975
  exit();
2726
3976
  onExit(0);
2727
3977
  }
3978
+ if (res?.runText) {
3979
+ const b = builderRef.current;
3980
+ if (b) {
3981
+ b.appendText(res.runText);
3982
+ const blocks2 = await b.submit();
3983
+ const start = Date.now();
3984
+ while (stateRef.current.status !== "idle" && Date.now() - start < 1500) {
3985
+ await new Promise((r) => setTimeout(r, 25));
3986
+ }
3987
+ await runBlocks(blocks2);
3988
+ }
3989
+ }
2728
3990
  const cmd = trimmed.slice(1).split(/\s+/, 1)[0];
2729
3991
  if (cmd === "clear") {
2730
3992
  onClearHistory?.(dispatch);
@@ -2739,9 +4001,14 @@ function App({
2739
4001
  }
2740
4002
  const builder = builderRef.current;
2741
4003
  if (!builder) return;
2742
- if (trimmed) builder.appendText(trimmed);
4004
+ const steering = state.steeringPending;
4005
+ if (trimmed) {
4006
+ const toAppend = steering ? buildSteeringPreamble(state.steerSnapshot, trimmed) : trimmed;
4007
+ builder.appendText(toAppend);
4008
+ }
4009
+ if (steering) dispatch({ type: "steerConsume" });
2743
4010
  const blocks = await builder.submit();
2744
- const displayText = trimmed || "(attachments only)";
4011
+ const displayText = trimmed ? steering ? `\u21AF ${trimmed}` : trimmed : "(attachments only)";
2745
4012
  dispatch({ type: "clearInput" });
2746
4013
  if (state.status !== "idle") {
2747
4014
  dispatch({
@@ -2756,6 +4023,36 @@ function App({
2756
4023
  if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
2757
4024
  await runBlocks(blocks);
2758
4025
  };
4026
+ const bootInjectedRef = useRef(false);
4027
+ useEffect(() => {
4028
+ if (bootInjectedRef.current) return;
4029
+ bootInjectedRef.current = true;
4030
+ const goal = initialGoal?.trim();
4031
+ const ask = initialAsk?.trim();
4032
+ if (!goal && !ask) return;
4033
+ void (async () => {
4034
+ await new Promise((r) => setTimeout(r, 50));
4035
+ const b = builderRef.current;
4036
+ if (!b) return;
4037
+ if (goal) {
4038
+ const shortGoal = goal.length > 80 ? `${goal.slice(0, 80)}\u2026` : goal;
4039
+ dispatch({
4040
+ type: "addEntry",
4041
+ entry: {
4042
+ kind: "info",
4043
+ text: `\u{1F3AF} Goal locked: ${shortGoal}
4044
+ Agent will work until verifiably complete. Esc / /steer to redirect, Ctrl+C to stop.`
4045
+ }
4046
+ });
4047
+ b.appendText(buildGoalPreamble(goal));
4048
+ } else if (ask) {
4049
+ dispatch({ type: "addEntry", entry: { kind: "user", text: ask } });
4050
+ b.appendText(ask);
4051
+ }
4052
+ const blocks = await b.submit();
4053
+ await runBlocks(blocks);
4054
+ })();
4055
+ }, []);
2759
4056
  const inputHint = useMemo(() => {
2760
4057
  if (state.status !== "idle") return "";
2761
4058
  if (state.buffer.startsWith("/")) return "slash command \u2014 Enter to dispatch";
@@ -2771,6 +4068,7 @@ function App({
2771
4068
  toolStream: state.toolStream
2772
4069
  }
2773
4070
  ),
4071
+ /* @__PURE__ */ jsx(LiveActivityStrip, { entries: state.fleet, nowTick }),
2774
4072
  /* @__PURE__ */ jsx(
2775
4073
  Input,
2776
4074
  {
@@ -2829,11 +4127,16 @@ function App({
2829
4127
  yolo,
2830
4128
  elapsedMs,
2831
4129
  todos,
4130
+ plan: planCounts ?? void 0,
4131
+ fleet: fleetCounts,
4132
+ fleetAgents,
2832
4133
  git: gitInfo,
2833
4134
  context: contextWindow,
2834
- projectName
4135
+ projectName,
4136
+ subagentCount: Object.keys(state.fleet).length
2835
4137
  }
2836
- )
4138
+ ),
4139
+ director ? /* @__PURE__ */ jsx(FleetPanel, { entries: state.fleet, totalCost: state.fleetCost, roster: fleetRoster }) : null
2837
4140
  ] });
2838
4141
  }
2839
4142
  function renderRunningTools(running) {
@@ -2889,6 +4192,15 @@ async function runTui(opts) {
2889
4192
  stdout.write(CURSOR_HOME);
2890
4193
  }
2891
4194
  stdout.write(BRACKETED_PASTE_ON);
4195
+ const swallowSignals = ["SIGTSTP", "SIGQUIT", "SIGTTIN", "SIGTTOU"];
4196
+ const swallow = () => {
4197
+ };
4198
+ for (const s of swallowSignals) {
4199
+ try {
4200
+ process.on(s, swallow);
4201
+ } catch {
4202
+ }
4203
+ }
2892
4204
  let cleaned = false;
2893
4205
  const cleanup = () => {
2894
4206
  if (cleaned) return;
@@ -2908,6 +4220,12 @@ async function runTui(opts) {
2908
4220
  process.on("exit", exitHandler);
2909
4221
  const detachListeners = () => {
2910
4222
  for (const s of signals) process.off(s, signalHandler);
4223
+ for (const s of swallowSignals) {
4224
+ try {
4225
+ process.off(s, swallow);
4226
+ } catch {
4227
+ }
4228
+ }
2911
4229
  process.off("exit", exitHandler);
2912
4230
  };
2913
4231
  return new Promise((resolve) => {
@@ -2947,7 +4265,12 @@ async function runTui(opts) {
2947
4265
  switchProviderAndModel: opts.switchProviderAndModel,
2948
4266
  effectiveMaxContext: opts.effectiveMaxContext,
2949
4267
  onExit,
2950
- onClearHistory: opts.onClearHistory ? (dispatch) => opts.onClearHistory(dispatch) : void 0
4268
+ director: opts.director ?? null,
4269
+ fleetRoster: opts.fleetRoster,
4270
+ onClearHistory: opts.onClearHistory ? (dispatch) => opts.onClearHistory(dispatch) : void 0,
4271
+ fleetStreamController: opts.fleetStreamController,
4272
+ initialGoal: opts.initialGoal,
4273
+ initialAsk: opts.initialAsk
2951
4274
  }),
2952
4275
  { exitOnCtrlC: false }
2953
4276
  );
@@ -2959,7 +4282,24 @@ async function runTui(opts) {
2959
4282
  settle(1);
2960
4283
  return;
2961
4284
  }
2962
- instance.waitUntilExit().then(() => settle(exitCode)).catch(() => settle(1));
4285
+ let detachResize = null;
4286
+ if (!useAltScreen) {
4287
+ const onResize = () => {
4288
+ try {
4289
+ stdout.write("\x1B[J");
4290
+ } catch {
4291
+ }
4292
+ };
4293
+ stdout.on("resize", onResize);
4294
+ detachResize = () => stdout.off("resize", onResize);
4295
+ }
4296
+ instance.waitUntilExit().then(() => {
4297
+ detachResize?.();
4298
+ settle(exitCode);
4299
+ }).catch(() => {
4300
+ detachResize?.();
4301
+ settle(1);
4302
+ });
2963
4303
  });
2964
4304
  }
2965
4305