@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/README.md +2 -2
- package/dist/index.d.ts +48 -7
- package/dist/index.js +1427 -87
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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 ${
|
|
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" ? "
|
|
480
|
-
|
|
481
|
-
|
|
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:
|
|
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(
|
|
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 ${
|
|
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
|
|
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 ${
|
|
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 [`${
|
|
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 ${
|
|
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 ${
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1124
|
-
|
|
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
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
-
|
|
1467
|
-
|
|
1468
|
-
"\u2610
|
|
1469
|
-
|
|
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
|
-
|
|
1472
|
-
|
|
1473
|
-
"\u2713
|
|
1474
|
-
|
|
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
|
-
|
|
1479
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
|
|
1480
|
-
/* @__PURE__ */ jsxs(Text, {
|
|
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
|
|
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
|
-
|
|
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
|
|
2404
|
-
|
|
2405
|
-
|
|
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: {
|
|
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,
|
|
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 (
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
if (
|
|
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: "
|
|
2465
|
-
|
|
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 (
|
|
2503
|
-
|
|
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 (
|
|
2529
|
-
|
|
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.
|
|
2534
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|