@wrongstack/tui 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,13 +1,375 @@
1
+ import { render, useApp, Box, useStdout, Static, Text, useInput, useStdin } from 'ink';
1
2
  import React, { useState, useReducer, useRef, useEffect, useMemo } from 'react';
2
- import { render, useApp, Box, useStdout, Static, Text, useInput } from 'ink';
3
+ import * as fs from 'fs/promises';
3
4
  import * as path3 from 'path';
4
- import * as fs2 from 'fs/promises';
5
- import { InputBuilder } from '@wrongstack/core';
6
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+ import { InputBuilder, formatTodosList } from '@wrongstack/core';
7
6
  import { spawn } from 'child_process';
8
7
  import * as os from 'os';
8
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
9
9
 
10
10
  // src/run-tui.ts
11
+ var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
12
+ async function readClipboardImage() {
13
+ const platform = process.platform;
14
+ if (platform === "win32") return readWindows();
15
+ if (platform === "darwin") return readDarwin();
16
+ if (platform === "linux") return readLinux();
17
+ return null;
18
+ }
19
+ async function readWindows() {
20
+ const tmp = path3.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
21
+ const ps = [
22
+ "Add-Type -AssemblyName System.Windows.Forms",
23
+ "Add-Type -AssemblyName System.Drawing",
24
+ "$img = [System.Windows.Forms.Clipboard]::GetImage()",
25
+ 'if ($img -eq $null) { Write-Output "NO_IMAGE"; exit 0 }',
26
+ `$img.Save('${tmp.replace(/\\/g, "\\\\")}', [System.Drawing.Imaging.ImageFormat]::Png)`,
27
+ 'Write-Output "OK"'
28
+ ].join("; ");
29
+ const out = await runCmd("powershell", ["-NoProfile", "-Command", ps]);
30
+ if (!out || out.trim() === "NO_IMAGE") return null;
31
+ if (!out.includes("OK")) return null;
32
+ return readPngFile(tmp);
33
+ }
34
+ async function readDarwin() {
35
+ const tmp = path3.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
36
+ const script = [
37
+ "try",
38
+ ` set the_file to (open for access POSIX file "${tmp}" with write permission)`,
39
+ " write (the clipboard as \xABclass PNGf\xBB) to the_file",
40
+ " close access the_file",
41
+ "on error",
42
+ " try",
43
+ ' close access POSIX file "' + tmp + '"',
44
+ " end try",
45
+ ' return "NO_IMAGE"',
46
+ "end try",
47
+ 'return "OK"'
48
+ ].join("\n");
49
+ const out = await runCmd("osascript", ["-e", script]);
50
+ if (!out || out.trim() !== "OK") return null;
51
+ return readPngFile(tmp);
52
+ }
53
+ async function readLinux() {
54
+ const tmp = path3.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
55
+ const tries = [
56
+ ["wl-paste", ["--type", "image/png"]],
57
+ ["xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]]
58
+ ];
59
+ for (const [cmd, args] of tries) {
60
+ const ok = await runCmdToFile(cmd, args, tmp).catch(() => false);
61
+ if (ok) return readPngFile(tmp);
62
+ }
63
+ return null;
64
+ }
65
+ async function readPngFile(p) {
66
+ try {
67
+ const buf = await fs.readFile(p);
68
+ if (buf.length === 0) {
69
+ await fs.unlink(p).catch(() => void 0);
70
+ return null;
71
+ }
72
+ if (buf.length > MAX_IMAGE_BYTES) {
73
+ await fs.unlink(p).catch(() => void 0);
74
+ throw new Error(`Clipboard image exceeds ${MAX_IMAGE_BYTES / 1024 / 1024}MB limit`);
75
+ }
76
+ if (buf[0] !== 137 || buf[1] !== 80 || buf[2] !== 78 || buf[3] !== 71) {
77
+ await fs.unlink(p).catch(() => void 0);
78
+ return null;
79
+ }
80
+ await fs.unlink(p).catch(() => void 0);
81
+ return { base64: buf.toString("base64"), mediaType: "image/png", bytes: buf.length };
82
+ } catch (err) {
83
+ if (err.code === "ENOENT") return null;
84
+ throw err;
85
+ }
86
+ }
87
+ function runCmd(cmd, args) {
88
+ return new Promise((resolve) => {
89
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
90
+ let out = "";
91
+ child.stdout.on("data", (c) => {
92
+ out += String(c);
93
+ });
94
+ child.on("error", () => resolve(null));
95
+ child.on("exit", (code) => resolve(code === 0 ? out : null));
96
+ });
97
+ }
98
+ function runCmdToFile(cmd, args, outPath) {
99
+ return new Promise((resolve) => {
100
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
101
+ const chunks = [];
102
+ child.stdout.on("data", (c) => chunks.push(c));
103
+ child.on("error", () => resolve(false));
104
+ child.on("exit", async (code) => {
105
+ if (code !== 0 || chunks.length === 0) return resolve(false);
106
+ try {
107
+ await fs.writeFile(outPath, Buffer.concat(chunks));
108
+ resolve(true);
109
+ } catch {
110
+ resolve(false);
111
+ }
112
+ });
113
+ });
114
+ }
115
+ function stringifyInput(input) {
116
+ if (!input || typeof input !== "object") return "";
117
+ const obj = input;
118
+ return Object.entries(obj).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${truncate(JSON.stringify(v), 80)}`).join(" ");
119
+ }
120
+ function truncate(s, max) {
121
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
122
+ }
123
+ function hasDiff(input) {
124
+ return Boolean(
125
+ input && typeof input === "object" && "diff" in input
126
+ );
127
+ }
128
+ function renderDiffLine(line) {
129
+ const prefix = line.startsWith("+") ? "green" : line.startsWith("-") ? "red" : line.startsWith("@@") ? "cyan" : void 0;
130
+ return /* @__PURE__ */ jsxs(Text, { color: prefix, children: [
131
+ line,
132
+ "\n"
133
+ ] }, line);
134
+ }
135
+ function renderDiff(diff) {
136
+ const lines = diff.split("\n").filter((l) => l.length > 0).slice(0, 20);
137
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 2, children: lines.map((l) => renderDiffLine(l)) });
138
+ }
139
+ function ConfirmPrompt({
140
+ toolName,
141
+ input,
142
+ suggestedPattern,
143
+ onDecision
144
+ }) {
145
+ useInput((_, key) => {
146
+ if (key.return) {
147
+ onDecision("yes");
148
+ } else if (key.escape) {
149
+ onDecision("no");
150
+ } else if (key.ctrl && _.toLowerCase() === "a") {
151
+ onDecision("always");
152
+ } else if (key.ctrl && _.toLowerCase() === "d") {
153
+ onDecision("deny");
154
+ }
155
+ });
156
+ const inputSummary = stringifyInput(input);
157
+ const showDiff = hasDiff(input);
158
+ const inp = input;
159
+ const diff = typeof inp?.diff === "string" ? inp.diff : "";
160
+ return /* @__PURE__ */ jsxs(
161
+ Box,
162
+ {
163
+ flexDirection: "column",
164
+ borderStyle: "single",
165
+ borderTop: false,
166
+ borderLeft: false,
167
+ borderRight: false,
168
+ borderBottom: false,
169
+ paddingX: 1,
170
+ children: [
171
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
172
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "\u26A0 Confirm" }),
173
+ /* @__PURE__ */ jsx(Text, { children: " " }),
174
+ /* @__PURE__ */ jsx(Text, { bold: true, children: toolName })
175
+ ] }),
176
+ inputSummary ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: inputSummary }) : null,
177
+ showDiff && diff ? /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginY: 1, children: renderDiff(diff) }) : null,
178
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
179
+ /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: /* @__PURE__ */ jsxs(Text, { children: [
180
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "[\u21B5]" }),
181
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " yes " }),
182
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Esc]" }),
183
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " no " }),
184
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[Ctrl+A]" }),
185
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
186
+ " always (",
187
+ suggestedPattern,
188
+ ") "
189
+ ] }),
190
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Ctrl+D]" }),
191
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " deny" })
192
+ ] }) })
193
+ ]
194
+ }
195
+ );
196
+ }
197
+ function FilePicker({ query, matches, selected }) {
198
+ if (matches.length === 0) {
199
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
200
+ "@",
201
+ query,
202
+ " \u2014 no matches"
203
+ ] }) });
204
+ }
205
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
206
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
207
+ "@",
208
+ query || "\u2026",
209
+ " \u2014 \u2191/\u2193 select, Enter attach, Esc cancel"
210
+ ] }),
211
+ matches.map((m, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
212
+ i === selected ? "\u203A " : " ",
213
+ highlight(m)
214
+ ] }, m))
215
+ ] });
216
+ }
217
+ function highlight(path4, query) {
218
+ return path4;
219
+ }
220
+ var STATUS_ICON = {
221
+ idle: { icon: "\u25CB", color: "gray" },
222
+ running: { icon: "\u25CF", color: "green" },
223
+ success: { icon: "\u2713", color: "green" },
224
+ failed: { icon: "\u2717", color: "red" },
225
+ timeout: { icon: "\u23F1", color: "yellow" },
226
+ stopped: { icon: "\u2298", color: "yellow" }
227
+ };
228
+ function fmtCost(n) {
229
+ if (n === 0) return "\u2014";
230
+ return `$${n.toFixed(3)}`;
231
+ }
232
+ function fmtCount(n) {
233
+ if (n === 0) return "\u2014";
234
+ return String(n);
235
+ }
236
+ function fmtModel(provider, model) {
237
+ if (!provider && !model) return "";
238
+ const p = provider ?? "";
239
+ const m = model ?? "";
240
+ return p && m ? `${p}/${m}` : p || m;
241
+ }
242
+ function resolveName(entry, roster) {
243
+ const rosterEntry = roster?.[entry.id];
244
+ if (rosterEntry) return rosterEntry.name;
245
+ return entry.name;
246
+ }
247
+ function FleetPanel({ entries, totalCost, roster }) {
248
+ const list = Object.values(entries);
249
+ if (list.length === 0) return null;
250
+ const sorted = [...list].sort((a, b) => {
251
+ const order = { running: 0, success: 1, failed: 2, timeout: 3, stopped: 4, idle: 5 };
252
+ const ao = order[a.status] ?? 9;
253
+ const bo = order[b.status] ?? 9;
254
+ if (ao !== bo) return ao - bo;
255
+ return b.lastEventAt - a.lastEventAt;
256
+ });
257
+ const runningCount = list.filter((e) => e.status === "running").length;
258
+ const totalLabel = totalCost > 0 ? `$${totalCost.toFixed(3)} \xB7 ${runningCount} active` : `${runningCount} active`;
259
+ return /* @__PURE__ */ jsxs(
260
+ Box,
261
+ {
262
+ flexDirection: "column",
263
+ paddingX: 1,
264
+ borderStyle: "single",
265
+ borderTop: false,
266
+ borderBottom: false,
267
+ borderLeft: false,
268
+ borderRight: false,
269
+ children: [
270
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, children: [
271
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Fleet" }),
272
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
273
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
274
+ list.length,
275
+ " agent",
276
+ list.length === 1 ? "" : "s"
277
+ ] }),
278
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
279
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: totalLabel })
280
+ ] }),
281
+ sorted.map((entry) => {
282
+ const si = STATUS_ICON[entry.status];
283
+ const modelTag = fmtModel(entry.provider, entry.model);
284
+ const name = resolveName(entry, roster);
285
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
286
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
287
+ /* @__PURE__ */ jsx(Text, { color: si.color, children: si.icon }),
288
+ /* @__PURE__ */ jsx(Text, { children: name.slice(0, 16).padEnd(16) }),
289
+ modelTag ? /* @__PURE__ */ jsxs(Fragment, { children: [
290
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
291
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: modelTag })
292
+ ] }) : null,
293
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
294
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
295
+ fmtCount(entry.iterations).padStart(3),
296
+ "it"
297
+ ] }),
298
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
299
+ fmtCount(entry.toolCalls).padStart(3),
300
+ "tc"
301
+ ] }),
302
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
303
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: fmtCost(entry.cost) })
304
+ ] }),
305
+ entry.status === "running" && entry.currentTool ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, children: [
306
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
307
+ "\u2192 ",
308
+ entry.currentTool.name
309
+ ] }),
310
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
311
+ " (",
312
+ Math.max(0, Date.now() - entry.currentTool.startedAt),
313
+ "ms)"
314
+ ] })
315
+ ] }) : null,
316
+ entry.status === "running" && entry.streamingText ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
317
+ ">",
318
+ " ",
319
+ entry.streamingText.slice(-80)
320
+ ] }) }) : null,
321
+ entry.transcriptPath ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
322
+ "log: ",
323
+ entry.transcriptPath
324
+ ] }) }) : null
325
+ ] }, entry.id);
326
+ })
327
+ ]
328
+ }
329
+ );
330
+ }
331
+ function fmtElapsed(ms) {
332
+ if (ms < 1e3) return `${ms}ms`;
333
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
334
+ const m = Math.floor(ms / 6e4);
335
+ const s = Math.floor(ms % 6e4 / 1e3);
336
+ return `${m}m${s.toString().padStart(2, "0")}s`;
337
+ }
338
+ function LiveActivityStrip({
339
+ entries,
340
+ nowTick,
341
+ maxRows = 4
342
+ }) {
343
+ const running = Object.values(entries).filter((e) => e.status === "running").sort((a, b) => a.startedAt - b.startedAt).slice(0, maxRows);
344
+ if (running.length === 0) return null;
345
+ const now = Date.now();
346
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
347
+ running.map((e) => {
348
+ const toolElapsed = e.currentTool ? now - e.currentTool.startedAt : 0;
349
+ const taskElapsed = now - e.startedAt;
350
+ const toolSeg = e.currentTool ? `\u2192 ${e.currentTool.name} (${fmtElapsed(toolElapsed)})` : "idle between tools";
351
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
352
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u25CF" }),
353
+ /* @__PURE__ */ jsx(Text, { children: e.name.slice(0, 14).padEnd(14) }),
354
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
355
+ /* @__PURE__ */ jsx(Text, { color: e.currentTool ? "green" : "yellow", children: toolSeg }),
356
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
357
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
358
+ e.iterations,
359
+ "it ",
360
+ e.toolCalls,
361
+ "tc \xB7 ",
362
+ fmtElapsed(taskElapsed)
363
+ ] })
364
+ ] }, e.id);
365
+ }),
366
+ Object.values(entries).filter((e) => e.status === "running").length > maxRows ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
367
+ "\u2026+",
368
+ Object.values(entries).filter((e) => e.status === "running").length - maxRows,
369
+ " more"
370
+ ] }) }) : null
371
+ ] });
372
+ }
11
373
 
12
374
  // src/markdown-table.ts
13
375
  function renderMarkdownTables(text, maxWidth) {
@@ -198,13 +560,53 @@ function History({ entries, streamingText, toolStream }) {
198
560
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "> " }),
199
561
  /* @__PURE__ */ jsx(Text, { children: tail })
200
562
  ] }) : null,
201
- toolTail ? /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
202
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: `\u25C6 ${toolStream.name} ` }),
203
- /* @__PURE__ */ jsx(Text, { children: toolTail })
204
- ] }) : null
563
+ toolTail ? /* @__PURE__ */ jsx(
564
+ ToolStreamBox,
565
+ {
566
+ name: toolStream.name,
567
+ text: toolTail,
568
+ startedAt: toolStream.startedAt,
569
+ termWidth
570
+ }
571
+ ) : null
205
572
  ] });
206
573
  }
207
574
  var MAX_STREAM_DISPLAY_CHARS = 480;
575
+ var MAX_STREAM_LINES = 8;
576
+ function ToolStreamBox({
577
+ name,
578
+ text,
579
+ startedAt,
580
+ termWidth
581
+ }) {
582
+ const [tick, setTick] = useState(0);
583
+ useEffect(() => {
584
+ const t = setInterval(() => setTick((n) => n + 1), 500);
585
+ return () => clearInterval(t);
586
+ }, []);
587
+ const elapsedMs = Date.now() - startedAt;
588
+ const lines = text.split("\n");
589
+ const totalLines = lines.length;
590
+ const hidden = Math.max(0, totalLines - MAX_STREAM_LINES);
591
+ const visible = hidden > 0 ? lines.slice(hidden) : lines;
592
+ const contentWidth = Math.max(20, Math.min(termWidth - 4, 100));
593
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 0, children: [
594
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
595
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u25C6 " }),
596
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: name }),
597
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u23F1 ${fmtDuration(elapsedMs)}` }),
598
+ hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` (${totalLines} lines, showing last ${MAX_STREAM_LINES})` }) : null
599
+ ] }),
600
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [
601
+ hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: ` \u2026 ${hidden} more line${hidden === 1 ? "" : "s"} above` }) : null,
602
+ visible.map((line, i) => {
603
+ const key = i;
604
+ const trimmed = line.length > contentWidth ? `${line.slice(0, contentWidth - 1)}\u2026` : line;
605
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: trimmed || " " }, key);
606
+ })
607
+ ] })
608
+ ] });
609
+ }
208
610
  function tailForDisplay(text, maxChars) {
209
611
  if (text.length <= maxChars) return text;
210
612
  const cut = text.length - maxChars;
@@ -234,7 +636,10 @@ function DiffBlock({ rows, hidden }) {
234
636
  hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: ` \u2026 ${hidden} more line${hidden === 1 ? "" : "s"}` }) : null
235
637
  ] });
236
638
  }
237
- function Entry({ entry, termWidth }) {
639
+ function Entry({
640
+ entry,
641
+ termWidth
642
+ }) {
238
643
  switch (entry.kind) {
239
644
  case "user":
240
645
  return /* @__PURE__ */ jsxs(Text, { children: [
@@ -247,8 +652,28 @@ function Entry({ entry, termWidth }) {
247
652
  return /* @__PURE__ */ jsx(Text, { children: renderMarkdownTables(entry.text, termWidth) });
248
653
  case "tool": {
249
654
  const argSummary = formatToolArgs(entry.name, entry.input);
250
- const outLines = formatToolOutput(entry.name, entry.output, entry.ok);
655
+ const outLines = formatToolOutput(
656
+ entry.name,
657
+ entry.output,
658
+ entry.ok,
659
+ entry.outputBytes,
660
+ entry.outputLines
661
+ );
251
662
  const diff = entry.ok ? extractDiffPreview(entry.name, entry.output) : void 0;
663
+ const sizeChip = (() => {
664
+ if (!entry.ok) return "";
665
+ const parts = [];
666
+ if (entry.outputLines !== void 0 && entry.outputLines > 0) {
667
+ parts.push(`${entry.outputLines} L`);
668
+ }
669
+ if (entry.outputBytes && entry.outputBytes > 0) {
670
+ parts.push(fmtBytes(entry.outputBytes));
671
+ }
672
+ if (entry.outputTokens && entry.outputTokens > 0) {
673
+ parts.push(`\u2248${fmtTok(entry.outputTokens)} tok`);
674
+ }
675
+ return parts.join(" \xB7 ");
676
+ })();
252
677
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
253
678
  /* @__PURE__ */ jsxs(Text, { children: [
254
679
  /* @__PURE__ */ jsx(Text, { color: entry.ok ? "green" : "red", children: entry.ok ? "\u25CF" : "\u2717" }),
@@ -258,13 +683,21 @@ function Entry({ entry, termWidth }) {
258
683
  /* @__PURE__ */ jsx(Text, { children: " " }),
259
684
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: argSummary })
260
685
  ] }) : null,
261
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${fmtDuration(entry.durationMs)}` })
686
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${fmtDuration(entry.durationMs)}` }),
687
+ sizeChip ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${sizeChip}` }) : null
262
688
  ] }),
263
689
  outLines.map((line, i) => (
264
690
  // biome-ignore lint/suspicious/noArrayIndexKey: tool output lines are static, index is stable
265
691
  /* @__PURE__ */ jsxs(Text, { children: [
266
692
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: i === outLines.length - 1 && !diff ? " \u2514\u2500 " : " \u251C\u2500 " }),
267
- /* @__PURE__ */ jsx(Text, { color: !entry.ok || line.startsWith("!") ? "red" : void 0, dimColor: entry.ok && !line.startsWith("!"), children: line })
693
+ /* @__PURE__ */ jsx(
694
+ Text,
695
+ {
696
+ color: !entry.ok || line.startsWith("!") ? "red" : void 0,
697
+ dimColor: entry.ok && !line.startsWith("!"),
698
+ children: line
699
+ }
700
+ )
268
701
  ] }, i)
269
702
  )),
270
703
  diff ? /* @__PURE__ */ jsx(DiffBlock, { rows: diff.rows, hidden: diff.hidden }) : null
@@ -279,86 +712,117 @@ function Entry({ entry, termWidth }) {
279
712
  case "turn-summary":
280
713
  return /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.text });
281
714
  case "confirm":
282
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderBottom: false, paddingX: 1, children: [
283
- /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
284
- /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "\u26A0 Confirm" }),
285
- /* @__PURE__ */ jsx(Text, { children: " " }),
286
- /* @__PURE__ */ jsx(Text, { bold: true, children: entry.toolName })
287
- ] }),
288
- entry.input && typeof entry.input === "object" ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: Object.entries(entry.input).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${String(v).slice(0, 80)}`).join(" ") }) : null,
289
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
290
- /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: /* @__PURE__ */ jsxs(Text, { children: [
291
- /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "[\u21B5]" }),
292
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " yes " }),
293
- /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Esc]" }),
294
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " no " }),
295
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[Ctrl+A]" }),
296
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
297
- " always (",
298
- entry.suggestedPattern,
299
- ") "
300
- ] }),
301
- /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Ctrl+D]" }),
302
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " deny" })
303
- ] }) })
304
- ] });
715
+ return /* @__PURE__ */ jsxs(
716
+ Box,
717
+ {
718
+ flexDirection: "column",
719
+ borderStyle: "single",
720
+ borderTop: false,
721
+ borderLeft: false,
722
+ borderRight: false,
723
+ borderBottom: false,
724
+ paddingX: 1,
725
+ children: [
726
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
727
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "\u26A0 Confirm" }),
728
+ /* @__PURE__ */ jsx(Text, { children: " " }),
729
+ /* @__PURE__ */ jsx(Text, { bold: true, children: entry.toolName })
730
+ ] }),
731
+ entry.input && typeof entry.input === "object" ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: Object.entries(entry.input).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${String(v).slice(0, 80)}`).join(" ") }) : null,
732
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
733
+ /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: /* @__PURE__ */ jsxs(Text, { children: [
734
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "[\u21B5]" }),
735
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " yes " }),
736
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Esc]" }),
737
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " no " }),
738
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[Ctrl+A]" }),
739
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
740
+ " always (",
741
+ entry.suggestedPattern,
742
+ ") "
743
+ ] }),
744
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Ctrl+D]" }),
745
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " deny" })
746
+ ] }) })
747
+ ]
748
+ }
749
+ );
305
750
  case "banner":
306
751
  return /* @__PURE__ */ jsx(Banner, { entry });
752
+ case "subagent": {
753
+ const lines = entry.text.split("\n");
754
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
755
+ /* @__PURE__ */ jsxs(Text, { children: [
756
+ /* @__PURE__ */ jsx(Text, { color: entry.agentColor, bold: true, children: `[${entry.agentLabel}]` }),
757
+ /* @__PURE__ */ jsx(Text, { children: " " }),
758
+ /* @__PURE__ */ jsx(Text, { color: entry.agentColor, children: entry.icon }),
759
+ /* @__PURE__ */ jsx(Text, { children: " " }),
760
+ /* @__PURE__ */ jsx(Text, { children: lines[0] ?? "" }),
761
+ entry.detail ? /* @__PURE__ */ jsxs(Fragment, { children: [
762
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
763
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.detail })
764
+ ] }) : null
765
+ ] }),
766
+ lines.slice(1).map((line, i) => (
767
+ // biome-ignore lint/suspicious/noArrayIndexKey: stable line index
768
+ /* @__PURE__ */ jsxs(Text, { children: [
769
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
770
+ /* @__PURE__ */ jsx(Text, { children: line })
771
+ ] }, i)
772
+ ))
773
+ ] });
774
+ }
307
775
  }
308
776
  }
309
777
  function Banner({
310
778
  entry
311
779
  }) {
312
780
  const cwdShort = shortenPath(entry.cwd, 48);
313
- return /* @__PURE__ */ jsxs(
314
- Box,
315
- {
316
- flexDirection: "column",
317
- borderStyle: "round",
318
- borderColor: "magenta",
319
- paddingX: 2,
320
- paddingY: 0,
321
- children: [
322
- /* @__PURE__ */ jsxs(Text, { children: [
323
- /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: " \u259F\u259B " }),
324
- /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "WrongStack" }),
325
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " v" }),
326
- /* @__PURE__ */ jsx(Text, { children: entry.version })
327
- ] }),
328
- /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: " Built on the wrong stack. Shipped anyway." }),
329
- /* @__PURE__ */ jsxs(Text, { children: [
330
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: " provider " }),
331
- /* @__PURE__ */ jsxs(Text, { children: [
332
- entry.provider,
333
- "/",
334
- entry.model
335
- ] })
336
- ] }),
337
- entry.family ? /* @__PURE__ */ jsxs(Text, { children: [
338
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: " family " }),
339
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.family })
340
- ] }) : null,
341
- entry.keyTail ? /* @__PURE__ */ jsxs(Text, { children: [
342
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: " key " }),
343
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u25CF\u25CF\u25CF\u2026" }),
344
- /* @__PURE__ */ jsx(Text, { children: entry.keyTail })
345
- ] }) : null,
346
- /* @__PURE__ */ jsxs(Text, { children: [
347
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: " cwd " }),
348
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: cwdShort })
349
- ] }),
350
- /* @__PURE__ */ jsxs(Text, { children: [
351
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: " hints " }),
352
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "/help \xB7 /init \xB7 /memory \xB7 /queue \xB7 /exit" })
353
- ] })
354
- ]
355
- }
356
- );
781
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 2, paddingY: 0, children: [
782
+ /* @__PURE__ */ jsxs(Text, { children: [
783
+ /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: " \u259F\u259B " }),
784
+ /* @__PURE__ */ jsx(Text, { color: "magenta", bold: true, children: "WrongStack" }),
785
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " v" }),
786
+ /* @__PURE__ */ jsx(Text, { children: entry.version })
787
+ ] }),
788
+ /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: " Built on the wrong stack. Shipped anyway." }),
789
+ /* @__PURE__ */ jsxs(Text, { children: [
790
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " provider " }),
791
+ /* @__PURE__ */ jsxs(Text, { children: [
792
+ entry.provider,
793
+ "/",
794
+ entry.model
795
+ ] })
796
+ ] }),
797
+ entry.family ? /* @__PURE__ */ jsxs(Text, { children: [
798
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " family " }),
799
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.family })
800
+ ] }) : null,
801
+ entry.keyTail ? /* @__PURE__ */ jsxs(Text, { children: [
802
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " key " }),
803
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u25CF\u25CF\u25CF\u2026" }),
804
+ /* @__PURE__ */ jsx(Text, { children: entry.keyTail })
805
+ ] }) : null,
806
+ /* @__PURE__ */ jsxs(Text, { children: [
807
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " cwd " }),
808
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: cwdShort })
809
+ ] }),
810
+ /* @__PURE__ */ jsxs(Text, { children: [
811
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: " hints " }),
812
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "/help \xB7 /init \xB7 /memory \xB7 /queue \xB7 /exit" })
813
+ ] })
814
+ ] });
357
815
  }
358
816
  function shortenPath(p, max) {
359
817
  if (p.length <= max) return p;
360
818
  return `\u2026${p.slice(p.length - (max - 1))}`;
361
819
  }
820
+ function fmtTok(n) {
821
+ if (!Number.isFinite(n) || n <= 0) return "0";
822
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
823
+ if (n >= 1e3) return `${(n / 1e3).toFixed(n >= 1e4 ? 0 : 1)}k`;
824
+ return String(n);
825
+ }
362
826
  function fmtDuration(ms) {
363
827
  if (ms < 1e3) return `${ms}ms`;
364
828
  if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
@@ -476,7 +940,7 @@ function formatToolArgs(toolName, input) {
476
940
  return "";
477
941
  }
478
942
  }
479
- function formatToolOutput(toolName, output, ok) {
943
+ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
480
944
  if (!output) return ok ? [] : ["failed"];
481
945
  const text = output.trim();
482
946
  if (!text) return ok ? [] : ["failed"];
@@ -541,12 +1005,14 @@ function formatToolOutput(toolName, output, ok) {
541
1005
  if (!diff) return [files && files.length === 0 ? "no changes" : "empty diff"];
542
1006
  const head = [];
543
1007
  if (mode) head.push(mode);
544
- if (files && files.length > 0) head.push(`${files.length} file${files.length === 1 ? "" : "s"}`);
1008
+ if (files && files.length > 0)
1009
+ head.push(`${files.length} file${files.length === 1 ? "" : "s"}`);
545
1010
  if (truncated) head.push("truncated");
546
1011
  return head.length > 0 ? [head.join(" \xB7 ")] : [];
547
1012
  }
548
1013
  }
549
1014
  if (toolName === "read") {
1015
+ if (outputLines !== void 0) return [];
550
1016
  if (json && typeof json === "object") {
551
1017
  const o = json;
552
1018
  const bytes = numOf(o["bytes"]);
@@ -830,8 +1296,51 @@ function formatToolOutput(toolName, output, ok) {
830
1296
  const lastLine = lines[lines.length - 1];
831
1297
  return lastLine ? [head, `"${truncMid(lastLine.trim(), 70)}"`] : [head];
832
1298
  }
833
- const firstLine = text.split("\n").find((l) => l.trim()) ?? text;
834
- return [truncMid(firstLine.replace(/\s+/g, " "), OUT_BUDGET)];
1299
+ if (json && typeof json === "object" && !Array.isArray(json)) {
1300
+ const summary = summarizeJsonObject(json);
1301
+ if (summary) return [summary];
1302
+ }
1303
+ const collapsed = text.replace(/\s+/g, " ").trim();
1304
+ return [truncMid(collapsed, GENERIC_BUDGET)];
1305
+ }
1306
+ var GENERIC_BUDGET = 240;
1307
+ function summarizeJsonObject(obj) {
1308
+ const keys = Object.keys(obj);
1309
+ if (keys.length === 0) return null;
1310
+ const priority = [
1311
+ "ok",
1312
+ "status",
1313
+ "timedOut",
1314
+ "stopReason",
1315
+ "reason",
1316
+ "error",
1317
+ "message",
1318
+ "result",
1319
+ "summary",
1320
+ "iterations",
1321
+ "toolCalls",
1322
+ "durationMs",
1323
+ "subagentId",
1324
+ "taskId"
1325
+ ];
1326
+ const ordered = [
1327
+ ...priority.filter((k) => keys.includes(k)),
1328
+ ...keys.filter((k) => !priority.includes(k))
1329
+ ];
1330
+ const parts = [];
1331
+ let used = 0;
1332
+ for (const key of ordered) {
1333
+ const v = obj[key];
1334
+ if (v === void 0 || v === null) continue;
1335
+ const rendered = typeof v === "string" ? `${key}="${truncMid(v.replace(/\s+/g, " "), 80)}"` : typeof v === "number" || typeof v === "boolean" ? `${key}=${v}` : Array.isArray(v) ? `${key}=[${v.length}]` : `${key}={\u2026}`;
1336
+ if (used + rendered.length > GENERIC_BUDGET) {
1337
+ parts.push("\u2026");
1338
+ break;
1339
+ }
1340
+ parts.push(rendered);
1341
+ used += rendered.length + 3;
1342
+ }
1343
+ return parts.length > 0 ? parts.join(" \xB7 ") : null;
835
1344
  }
836
1345
  function firstNonEmpty(text) {
837
1346
  if (!text) return void 0;
@@ -949,6 +1458,31 @@ function truncMid(s, max) {
949
1458
  if (s.length <= max) return s;
950
1459
  return `${s.slice(0, max - 1)}\u2026`;
951
1460
  }
1461
+ function isHomeEnd(data) {
1462
+ if (data === "\x1B[H" || data === "\x1B[1~" || data === "\x1BOH" || data === "\x1B[7~")
1463
+ return "home";
1464
+ if (data === "\x1B[F" || data === "\x1B[4~" || data === "\x1BOF" || data === "\x1B[8~")
1465
+ return "end";
1466
+ return null;
1467
+ }
1468
+ var EMPTY_KEY = {
1469
+ upArrow: false,
1470
+ downArrow: false,
1471
+ leftArrow: false,
1472
+ rightArrow: false,
1473
+ return: false,
1474
+ escape: false,
1475
+ ctrl: false,
1476
+ meta: false,
1477
+ shift: false,
1478
+ tab: false,
1479
+ backspace: false,
1480
+ delete: false,
1481
+ pageUp: false,
1482
+ pageDown: false,
1483
+ home: false,
1484
+ end: false
1485
+ };
952
1486
  function Input({
953
1487
  prompt = "\u203A ",
954
1488
  value,
@@ -962,6 +1496,19 @@ function Input({
962
1496
  if (disabled) return;
963
1497
  onKey(input, key);
964
1498
  });
1499
+ const { stdin } = useStdin();
1500
+ useEffect(() => {
1501
+ if (!stdin || disabled) return;
1502
+ const handleData = (data) => {
1503
+ const kind = isHomeEnd(data.toString());
1504
+ if (kind === "home") onKey("", { ...EMPTY_KEY, home: true });
1505
+ else if (kind === "end") onKey("", { ...EMPTY_KEY, end: true });
1506
+ };
1507
+ stdin.on("data", handleData);
1508
+ return () => {
1509
+ stdin.off("data", handleData);
1510
+ };
1511
+ }, [stdin, disabled, onKey]);
965
1512
  const before = value.slice(0, cursor);
966
1513
  const at = value.slice(cursor, cursor + 1) || " ";
967
1514
  const after = value.slice(cursor + 1);
@@ -983,6 +1530,72 @@ function Input({
983
1530
  hint ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint }) : null
984
1531
  ] });
985
1532
  }
1533
+ function ModelPicker({
1534
+ step,
1535
+ providerOptions,
1536
+ modelOptions,
1537
+ selected,
1538
+ pickedProviderId,
1539
+ hint
1540
+ }) {
1541
+ if (step === "provider") {
1542
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1543
+ /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u2501\u2501 Switch model \u2014 Step 1/2: Pick provider \u2501\u2501" }),
1544
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc cancel" }),
1545
+ providerOptions.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(no providers with keys \u2014 add one via `wstack auth`)" }) : providerOptions.map((p, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1546
+ i === selected ? "\u203A " : " ",
1547
+ /* @__PURE__ */ jsx(Text, { bold: true, children: p.id.padEnd(28) }),
1548
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1549
+ " [",
1550
+ p.family,
1551
+ "]"
1552
+ ] }),
1553
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1554
+ " ",
1555
+ p.models.length,
1556
+ " model",
1557
+ p.models.length === 1 ? "" : "s"
1558
+ ] })
1559
+ ] }, p.id)),
1560
+ hint ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: hint }) : null
1561
+ ] });
1562
+ }
1563
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1564
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
1565
+ "\u2501\u2501 Switch model \u2014 Step 2/2: Pick model (",
1566
+ pickedProviderId,
1567
+ ") \u2501\u2501"
1568
+ ] }),
1569
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc back \xB7 Ctrl-C cancel" }),
1570
+ modelOptions.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(no models known for this provider)" }) : modelOptions.map((id, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1571
+ i === selected ? "\u203A " : " ",
1572
+ id
1573
+ ] }, id)),
1574
+ hint ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: hint }) : null
1575
+ ] });
1576
+ }
1577
+ function SlashMenu({ query, matches, selected }) {
1578
+ const placeholder = query ? `/${query}` : "/";
1579
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1580
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1581
+ placeholder || "/",
1582
+ " \u2014 \u2191/\u2193 select, Enter dispatch, Tab autocomplete, Esc close"
1583
+ ] }),
1584
+ matches.map((m, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1585
+ i === selected ? "\u203A " : " ",
1586
+ /* @__PURE__ */ jsx(Text, { bold: true, children: m.name }),
1587
+ m.argsHint ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1588
+ " ",
1589
+ m.argsHint
1590
+ ] }) : null,
1591
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1592
+ " \u2014 ",
1593
+ m.description
1594
+ ] })
1595
+ ] }, m.name)),
1596
+ matches.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No matching commands" })
1597
+ ] });
1598
+ }
986
1599
  function StatusBar({
987
1600
  model,
988
1601
  state,
@@ -992,6 +1605,9 @@ function StatusBar({
992
1605
  yolo = false,
993
1606
  elapsedMs,
994
1607
  todos,
1608
+ plan,
1609
+ fleet,
1610
+ fleetAgents,
995
1611
  git,
996
1612
  subagentCount = 0,
997
1613
  context,
@@ -1002,7 +1618,9 @@ function StatusBar({
1002
1618
  const cache2 = tokenCounter?.cacheStats();
1003
1619
  const stateColor = state === "idle" ? "cyan" : state === "aborting" ? "yellow" : "green";
1004
1620
  const stateLabel = state === "idle" ? "idle" : state === "aborting" ? "aborting\u2026" : "thinking\u2026";
1005
- const hasSecondLine = yolo || elapsedMs !== void 0 || todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) || git !== null && git !== void 0 || subagentCount > 0 || projectName !== void 0 && projectName.length > 0;
1621
+ const hasSecondLine = yolo || elapsedMs !== void 0 || git !== null && git !== void 0 || projectName !== void 0 && projectName.length > 0;
1622
+ const fleetHasActivity = fleet && (fleet.running > 0 || fleet.idle > 0 || fleet.pending > 0 || fleet.completed > 0) || subagentCount > 0;
1623
+ const hasThirdLine = todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) || plan && (plan.open > 0 || plan.inProgress > 0 || plan.done > 0) || fleetHasActivity;
1006
1624
  return /* @__PURE__ */ jsxs(
1007
1625
  Box,
1008
1626
  {
@@ -1028,11 +1646,12 @@ function StatusBar({
1028
1646
  usage ? /* @__PURE__ */ jsxs(Fragment, { children: [
1029
1647
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1030
1648
  /* @__PURE__ */ jsxs(Text, { children: [
1031
- "\u2191 ",
1032
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: fmtTok(usage.input) }),
1033
- " \u2193",
1649
+ "\u2191",
1034
1650
  " ",
1035
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: fmtTok(usage.output) })
1651
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: fmtTok2(usage.input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0)) }),
1652
+ " ",
1653
+ "\u2193 ",
1654
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: fmtTok2(usage.output) })
1036
1655
  ] })
1037
1656
  ] }) : null,
1038
1657
  cache2 && cache2.hitRatio > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1068,7 +1687,7 @@ function StatusBar({
1068
1687
  yolo ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }) : null,
1069
1688
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1070
1689
  "\u23F1 ",
1071
- fmtElapsed(elapsedMs)
1690
+ fmtElapsed2(elapsedMs)
1072
1691
  ] })
1073
1692
  ] }) : null,
1074
1693
  projectName ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1098,36 +1717,93 @@ function StatusBar({
1098
1717
  git.untracked
1099
1718
  ] }) : null
1100
1719
  ] })
1720
+ ] }) : null
1721
+ ] }) : null,
1722
+ hasThirdLine ? /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, children: [
1723
+ todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) ? /* @__PURE__ */ jsxs(Text, { children: [
1724
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "todos " }),
1725
+ todos.inProgress > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1726
+ "\u231B",
1727
+ todos.inProgress
1728
+ ] }) : null,
1729
+ todos.inProgress > 0 && (todos.pending > 0 || todos.completed > 0) ? " " : "",
1730
+ todos.pending > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1731
+ "\u2610",
1732
+ todos.pending
1733
+ ] }) : null,
1734
+ todos.pending > 0 && todos.completed > 0 ? " " : "",
1735
+ todos.completed > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1736
+ "\u2713",
1737
+ todos.completed
1738
+ ] }) : null
1101
1739
  ] }) : null,
1102
- todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) ? /* @__PURE__ */ jsxs(Fragment, { children: [
1103
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1740
+ plan && (plan.open > 0 || plan.inProgress > 0 || plan.done > 0) ? /* @__PURE__ */ jsxs(Fragment, { children: [
1741
+ todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }) : null,
1104
1742
  /* @__PURE__ */ jsxs(Text, { children: [
1105
- todos.inProgress > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1106
- "\u231B ",
1107
- todos.inProgress
1743
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u{1F4CB} " }),
1744
+ plan.inProgress > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1745
+ "\u231B",
1746
+ plan.inProgress
1108
1747
  ] }) : null,
1109
- todos.inProgress > 0 && (todos.pending > 0 || todos.completed > 0) ? " " : "",
1110
- todos.pending > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1111
- "\u2610 ",
1112
- todos.pending
1748
+ plan.inProgress > 0 && (plan.open > 0 || plan.done > 0) ? " " : "",
1749
+ plan.open > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1750
+ "\u2610",
1751
+ plan.open
1113
1752
  ] }) : null,
1114
- todos.pending > 0 && todos.completed > 0 ? " " : "",
1115
- todos.completed > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1116
- "\u2713 ",
1117
- todos.completed
1753
+ plan.open > 0 && plan.done > 0 ? " " : "",
1754
+ plan.done > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1755
+ "\u2713",
1756
+ plan.done
1118
1757
  ] }) : null
1119
1758
  ] })
1120
1759
  ] }) : null,
1121
- subagentCount > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1122
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
1123
- /* @__PURE__ */ jsxs(Text, { color: "blue", children: [
1760
+ fleetHasActivity ? /* @__PURE__ */ jsxs(Fragment, { children: [
1761
+ todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) || plan && (plan.open > 0 || plan.inProgress > 0 || plan.done > 0) ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }) : null,
1762
+ fleet ? /* @__PURE__ */ jsxs(Text, { children: [
1763
+ /* @__PURE__ */ jsx(Text, { color: "blue", children: "\u{1F310} " }),
1764
+ fleet.running > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1765
+ "\u25B6",
1766
+ fleet.running
1767
+ ] }) : null,
1768
+ fleet.running > 0 && (fleet.pending > 0 || fleet.idle > 0 || fleet.completed > 0) ? " " : "",
1769
+ fleet.pending > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1770
+ "\u2610",
1771
+ fleet.pending
1772
+ ] }) : null,
1773
+ fleet.pending > 0 && (fleet.idle > 0 || fleet.completed > 0) ? " " : "",
1774
+ fleet.idle > 0 ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1775
+ "\xB7",
1776
+ fleet.idle,
1777
+ "idle"
1778
+ ] }) : null,
1779
+ fleet.idle > 0 && fleet.completed > 0 ? " " : "",
1780
+ fleet.completed > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
1781
+ "\u2713",
1782
+ fleet.completed
1783
+ ] }) : null
1784
+ ] }) : /* @__PURE__ */ jsxs(Text, { color: "blue", children: [
1124
1785
  "\u{1F310} ",
1125
1786
  subagentCount,
1126
1787
  " agent",
1127
1788
  subagentCount === 1 ? "" : "s"
1128
1789
  ] })
1129
1790
  ] }) : null
1130
- ] }) : null
1791
+ ] }) : null,
1792
+ fleetAgents && fleetAgents.length > 0 ? /* @__PURE__ */ jsx(Box, { flexDirection: "row", gap: 2, children: fleetAgents.map((a, i) => (
1793
+ // biome-ignore lint/suspicious/noArrayIndexKey: agent list is stable per render
1794
+ /* @__PURE__ */ jsxs(Text, { children: [
1795
+ /* @__PURE__ */ jsx(Text, { color: a.color, bold: true, children: a.label }),
1796
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
1797
+ /* @__PURE__ */ jsx(Text, { color: a.running ? "yellow" : void 0, dimColor: !a.running, children: a.running ? "\u25B6" : "\xB7" }),
1798
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
1799
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: fmtElapsed2(a.elapsedMs) }),
1800
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
1801
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1802
+ a.toolCalls,
1803
+ "t"
1804
+ ] })
1805
+ ] }, i)
1806
+ )) }) : null
1131
1807
  ]
1132
1808
  }
1133
1809
  );
@@ -1147,9 +1823,9 @@ function ContextChip({ ctx }) {
1147
1823
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1148
1824
  " ",
1149
1825
  "(",
1150
- fmtTok(ctx.used),
1826
+ fmtTok2(ctx.used),
1151
1827
  "/",
1152
- fmtTok(ctx.max),
1828
+ fmtTok2(ctx.max),
1153
1829
  ")"
1154
1830
  ] })
1155
1831
  ] });
@@ -1162,179 +1838,23 @@ function renderProgress(ratio, width) {
1162
1838
  const capped = Math.min(width, filled);
1163
1839
  return FILLED.repeat(capped) + EMPTY.repeat(width - capped);
1164
1840
  }
1165
- function fmtTok(n) {
1841
+ function fmtTok2(n) {
1166
1842
  if (n < 1e3) return String(n);
1167
1843
  if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
1168
1844
  return `${(n / 1e6).toFixed(1)}M`;
1169
1845
  }
1170
- function fmtElapsed(ms) {
1846
+ function fmtElapsed2(ms) {
1171
1847
  const totalSec = Math.floor(ms / 1e3);
1172
1848
  const h = Math.floor(totalSec / 3600);
1173
1849
  const m = Math.floor(totalSec % 3600 / 60);
1174
1850
  const s = totalSec % 60;
1175
1851
  if (h > 0) {
1176
1852
  return `${h}:${pad2(m)}:${pad2(s)}`;
1177
- }
1178
- return `${pad2(m)}:${pad2(s)}`;
1179
- }
1180
- function pad2(n) {
1181
- return n < 10 ? `0${n}` : String(n);
1182
- }
1183
- function FilePicker({ query, matches, selected }) {
1184
- if (matches.length === 0) {
1185
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1186
- "@",
1187
- query,
1188
- " \u2014 no matches"
1189
- ] }) });
1190
- }
1191
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1192
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1193
- "@",
1194
- query || "\u2026",
1195
- " \u2014 \u2191/\u2193 select, Enter attach, Esc cancel"
1196
- ] }),
1197
- matches.map((m, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1198
- i === selected ? "\u203A " : " ",
1199
- highlight(m)
1200
- ] }, m))
1201
- ] });
1202
- }
1203
- function highlight(path4, query) {
1204
- return path4;
1205
- }
1206
- function SlashMenu({ query, matches, selected }) {
1207
- const placeholder = query ? `/${query}` : "/";
1208
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1209
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1210
- placeholder || "/",
1211
- " \u2014 \u2191/\u2193 select, Enter dispatch, Tab autocomplete, Esc close"
1212
- ] }),
1213
- matches.map((m, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1214
- i === selected ? "\u203A " : " ",
1215
- /* @__PURE__ */ jsx(Text, { bold: true, children: m.name }),
1216
- m.argsHint ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1217
- " ",
1218
- m.argsHint
1219
- ] }) : null,
1220
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1221
- " \u2014 ",
1222
- m.description
1223
- ] })
1224
- ] }, m.name)),
1225
- matches.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No matching commands" })
1226
- ] });
1227
- }
1228
- function ModelPicker({
1229
- step,
1230
- providerOptions,
1231
- modelOptions,
1232
- selected,
1233
- pickedProviderId,
1234
- hint
1235
- }) {
1236
- if (step === "provider") {
1237
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1238
- /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u2501\u2501 Switch model \u2014 Step 1/2: Pick provider \u2501\u2501" }),
1239
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc cancel" }),
1240
- providerOptions.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(no providers with keys \u2014 add one via `wstack auth`)" }) : providerOptions.map((p, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1241
- i === selected ? "\u203A " : " ",
1242
- /* @__PURE__ */ jsx(Text, { bold: true, children: p.id.padEnd(28) }),
1243
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1244
- " [",
1245
- p.family,
1246
- "]"
1247
- ] }),
1248
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1249
- " ",
1250
- p.models.length,
1251
- " model",
1252
- p.models.length === 1 ? "" : "s"
1253
- ] })
1254
- ] }, p.id)),
1255
- hint ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: hint }) : null
1256
- ] });
1257
- }
1258
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1259
- /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
1260
- "\u2501\u2501 Switch model \u2014 Step 2/2: Pick model",
1261
- " ",
1262
- "(",
1263
- pickedProviderId,
1264
- ") \u2501\u2501"
1265
- ] }),
1266
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc back \xB7 Ctrl-C cancel" }),
1267
- modelOptions.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(no models known for this provider)" }) : modelOptions.map((id, i) => /* @__PURE__ */ jsxs(Text, { color: i === selected ? "cyan" : void 0, inverse: i === selected, children: [
1268
- i === selected ? "\u203A " : " ",
1269
- id
1270
- ] }, id)),
1271
- hint ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: hint }) : null
1272
- ] });
1273
- }
1274
- function stringifyInput(input) {
1275
- if (!input || typeof input !== "object") return "";
1276
- const obj = input;
1277
- return Object.entries(obj).filter(([k]) => k !== "content" && k !== "new_string").map(([k, v]) => `${k}: ${truncate(JSON.stringify(v), 80)}`).join(" ");
1278
- }
1279
- function truncate(s, max) {
1280
- return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
1281
- }
1282
- function hasDiff(input) {
1283
- return Boolean(
1284
- input && typeof input === "object" && "diff" in input
1285
- );
1286
- }
1287
- function renderDiffLine(line) {
1288
- const prefix = line.startsWith("+") ? "green" : line.startsWith("-") ? "red" : line.startsWith("@@") ? "cyan" : void 0;
1289
- return /* @__PURE__ */ jsxs(Text, { color: prefix, children: [
1290
- line,
1291
- "\n"
1292
- ] }, line);
1293
- }
1294
- function renderDiff(diff) {
1295
- const lines = diff.split("\n").filter((l) => l.length > 0).slice(0, 20);
1296
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 2, children: lines.map((l) => renderDiffLine(l)) });
1853
+ }
1854
+ return `${pad2(m)}:${pad2(s)}`;
1297
1855
  }
1298
- function ConfirmPrompt({ toolName, input, suggestedPattern, onDecision }) {
1299
- useInput((_, key) => {
1300
- if (key.return) {
1301
- onDecision("yes");
1302
- } else if (key.escape) {
1303
- onDecision("no");
1304
- } else if (key.ctrl && _.toLowerCase() === "a") {
1305
- onDecision("always");
1306
- } else if (key.ctrl && _.toLowerCase() === "d") {
1307
- onDecision("deny");
1308
- }
1309
- });
1310
- const inputSummary = stringifyInput(input);
1311
- const showDiff = hasDiff(input);
1312
- const inp = input;
1313
- const diff = typeof inp?.diff === "string" ? inp.diff : "";
1314
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderBottom: false, paddingX: 1, children: [
1315
- /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
1316
- /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "\u26A0 Confirm" }),
1317
- /* @__PURE__ */ jsx(Text, { children: " " }),
1318
- /* @__PURE__ */ jsx(Text, { bold: true, children: toolName })
1319
- ] }),
1320
- inputSummary ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: inputSummary }) : null,
1321
- showDiff && diff ? /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginY: 1, children: renderDiff(diff) }) : null,
1322
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
1323
- /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: /* @__PURE__ */ jsxs(Text, { children: [
1324
- /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "[\u21B5]" }),
1325
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " yes " }),
1326
- /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Esc]" }),
1327
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " no " }),
1328
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "[Ctrl+A]" }),
1329
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1330
- " always (",
1331
- suggestedPattern,
1332
- ") "
1333
- ] }),
1334
- /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "[Ctrl+D]" }),
1335
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " deny" })
1336
- ] }) })
1337
- ] });
1856
+ function pad2(n) {
1857
+ return n < 10 ? `0${n}` : String(n);
1338
1858
  }
1339
1859
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
1340
1860
  "node_modules",
@@ -1369,7 +1889,7 @@ async function walk(root, rel, depth, out) {
1369
1889
  const dir = rel ? path3.join(root, rel) : root;
1370
1890
  let entries;
1371
1891
  try {
1372
- entries = await fs2.readdir(dir, { withFileTypes: true });
1892
+ entries = await fs.readdir(dir, { withFileTypes: true });
1373
1893
  } catch {
1374
1894
  return;
1375
1895
  }
@@ -1414,108 +1934,56 @@ async function searchFiles(root, query, limit = 8) {
1414
1934
  scored.sort((a, b) => a.score - b.score);
1415
1935
  return scored.slice(0, limit).map((x) => x.path);
1416
1936
  }
1417
- var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
1418
- async function readClipboardImage() {
1419
- const platform = process.platform;
1420
- if (platform === "win32") return readWindows();
1421
- if (platform === "darwin") return readDarwin();
1422
- if (platform === "linux") return readLinux();
1423
- return null;
1424
- }
1425
- async function readWindows() {
1426
- const tmp = path3.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
1427
- const ps = [
1428
- "Add-Type -AssemblyName System.Windows.Forms",
1429
- "Add-Type -AssemblyName System.Drawing",
1430
- "$img = [System.Windows.Forms.Clipboard]::GetImage()",
1431
- 'if ($img -eq $null) { Write-Output "NO_IMAGE"; exit 0 }',
1432
- `$img.Save('${tmp.replace(/\\/g, "\\\\")}', [System.Drawing.Imaging.ImageFormat]::Png)`,
1433
- 'Write-Output "OK"'
1434
- ].join("; ");
1435
- const out = await runCmd("powershell", ["-NoProfile", "-Command", ps]);
1436
- if (!out || out.trim() === "NO_IMAGE") return null;
1437
- if (!out.includes("OK")) return null;
1438
- return readPngFile(tmp);
1439
- }
1440
- async function readDarwin() {
1441
- const tmp = path3.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
1442
- const script = [
1443
- "try",
1444
- ` set the_file to (open for access POSIX file "${tmp}" with write permission)`,
1445
- " write (the clipboard as \xABclass PNGf\xBB) to the_file",
1446
- " close access the_file",
1447
- "on error",
1448
- " try",
1449
- ' close access POSIX file "' + tmp + '"',
1450
- " end try",
1451
- ' return "NO_IMAGE"',
1452
- "end try",
1453
- 'return "OK"'
1454
- ].join("\n");
1455
- const out = await runCmd("osascript", ["-e", script]);
1456
- if (!out || out.trim() !== "OK") return null;
1457
- return readPngFile(tmp);
1458
- }
1459
- async function readLinux() {
1460
- const tmp = path3.join(os.tmpdir(), `wstack-clip-${Date.now()}.png`);
1461
- const tries = [
1462
- ["wl-paste", ["--type", "image/png"]],
1463
- ["xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]]
1464
- ];
1465
- for (const [cmd, args] of tries) {
1466
- const ok = await runCmdToFile(cmd, args, tmp).catch(() => false);
1467
- if (ok) return readPngFile(tmp);
1937
+ async function readGitInfo(cwd) {
1938
+ const [branchRes, numstatRes, statusRes] = await Promise.all([
1939
+ runGit(cwd, ["branch", "--show-current"]),
1940
+ runGit(cwd, ["diff", "HEAD", "--numstat"]),
1941
+ runGit(cwd, ["status", "--porcelain"])
1942
+ ]);
1943
+ if (!branchRes.ok || !numstatRes.ok || !statusRes.ok) return null;
1944
+ const branch = branchRes.stdout.trim();
1945
+ const branchLabel = branch || await detachedShortSha(cwd) || "detached";
1946
+ let added = 0;
1947
+ let deleted = 0;
1948
+ for (const line of numstatRes.stdout.split("\n")) {
1949
+ if (!line) continue;
1950
+ const [a, d] = line.split(" ");
1951
+ if (a && a !== "-") added += Number.parseInt(a, 10) || 0;
1952
+ if (d && d !== "-") deleted += Number.parseInt(d, 10) || 0;
1468
1953
  }
1469
- return null;
1470
- }
1471
- async function readPngFile(p) {
1472
- try {
1473
- const buf = await fs2.readFile(p);
1474
- if (buf.length === 0) {
1475
- await fs2.unlink(p).catch(() => void 0);
1476
- return null;
1477
- }
1478
- if (buf.length > MAX_IMAGE_BYTES) {
1479
- await fs2.unlink(p).catch(() => void 0);
1480
- throw new Error(`Clipboard image exceeds ${MAX_IMAGE_BYTES / 1024 / 1024}MB limit`);
1481
- }
1482
- if (buf[0] !== 137 || buf[1] !== 80 || buf[2] !== 78 || buf[3] !== 71) {
1483
- await fs2.unlink(p).catch(() => void 0);
1484
- return null;
1485
- }
1486
- await fs2.unlink(p).catch(() => void 0);
1487
- return { base64: buf.toString("base64"), mediaType: "image/png", bytes: buf.length };
1488
- } catch (err) {
1489
- if (err.code === "ENOENT") return null;
1490
- throw err;
1954
+ let untracked = 0;
1955
+ for (const line of statusRes.stdout.split("\n")) {
1956
+ if (line.startsWith("?? ")) untracked++;
1491
1957
  }
1958
+ return { branch: branchLabel, added, deleted, untracked };
1492
1959
  }
1493
- function runCmd(cmd, args) {
1494
- return new Promise((resolve) => {
1495
- const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
1496
- let out = "";
1497
- child.stdout.on("data", (c) => {
1498
- out += String(c);
1499
- });
1500
- child.on("error", () => resolve(null));
1501
- child.on("exit", (code) => resolve(code === 0 ? out : null));
1502
- });
1960
+ async function detachedShortSha(cwd) {
1961
+ const res = await runGit(cwd, ["rev-parse", "--short", "HEAD"]);
1962
+ return res.ok ? res.stdout.trim() : null;
1503
1963
  }
1504
- function runCmdToFile(cmd, args, outPath) {
1964
+ function runGit(cwd, args) {
1505
1965
  return new Promise((resolve) => {
1506
- const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
1507
- const chunks = [];
1508
- child.stdout.on("data", (c) => chunks.push(c));
1509
- child.on("error", () => resolve(false));
1510
- child.on("exit", async (code) => {
1511
- if (code !== 0 || chunks.length === 0) return resolve(false);
1512
- try {
1513
- await fs2.writeFile(outPath, Buffer.concat(chunks));
1514
- resolve(true);
1515
- } catch {
1516
- resolve(false);
1517
- }
1518
- });
1966
+ let stdout = "";
1967
+ try {
1968
+ const child = spawn("git", args, {
1969
+ cwd,
1970
+ // Inherit stderr (silent) we don't care about git's noise.
1971
+ stdio: ["ignore", "pipe", "ignore"],
1972
+ // Don't let a slow git hang the TUI.
1973
+ timeout: 3e3,
1974
+ windowsHide: true
1975
+ });
1976
+ child.stdout.setEncoding("utf8");
1977
+ child.stdout.on("data", (chunk) => {
1978
+ stdout += chunk;
1979
+ });
1980
+ child.on("error", () => resolve({ ok: false, stdout: "" }));
1981
+ child.on("close", (code) => {
1982
+ resolve({ ok: code === 0, stdout });
1983
+ });
1984
+ } catch {
1985
+ resolve({ ok: false, stdout: "" });
1986
+ }
1519
1987
  });
1520
1988
  }
1521
1989
 
@@ -1567,7 +2035,8 @@ function handleQueueCommand(args, deps) {
1567
2035
  if (uniqueValid.length === 0) {
1568
2036
  const parts2 = ["No valid positions to delete."];
1569
2037
  if (invalid.length > 0) parts2.push(`Invalid: ${invalid.join(", ")}.`);
1570
- if (outOfRange.length > 0) parts2.push(`Out of range (queue has ${queue.length}): ${outOfRange.join(", ")}.`);
2038
+ if (outOfRange.length > 0)
2039
+ parts2.push(`Out of range (queue has ${queue.length}): ${outOfRange.join(", ")}.`);
1571
2040
  return parts2.join(" ");
1572
2041
  }
1573
2042
  deps.deleteAt(uniqueValid);
@@ -1596,65 +2065,10 @@ function oneLine(s, max) {
1596
2065
  const collapsed = s.replace(/\s+/g, " ").trim();
1597
2066
  return collapsed.length <= max ? collapsed : `${collapsed.slice(0, max - 1)}\u2026`;
1598
2067
  }
1599
- async function readGitInfo(cwd) {
1600
- const [branchRes, numstatRes, statusRes] = await Promise.all([
1601
- runGit(cwd, ["branch", "--show-current"]),
1602
- runGit(cwd, ["diff", "HEAD", "--numstat"]),
1603
- runGit(cwd, ["status", "--porcelain"])
1604
- ]);
1605
- if (!branchRes.ok || !numstatRes.ok || !statusRes.ok) return null;
1606
- const branch = branchRes.stdout.trim();
1607
- const branchLabel = branch || await detachedShortSha(cwd) || "detached";
1608
- let added = 0;
1609
- let deleted = 0;
1610
- for (const line of numstatRes.stdout.split("\n")) {
1611
- if (!line) continue;
1612
- const [a, d] = line.split(" ");
1613
- if (a && a !== "-") added += Number.parseInt(a, 10) || 0;
1614
- if (d && d !== "-") deleted += Number.parseInt(d, 10) || 0;
1615
- }
1616
- let untracked = 0;
1617
- for (const line of statusRes.stdout.split("\n")) {
1618
- if (line.startsWith("?? ")) untracked++;
1619
- }
1620
- return { branch: branchLabel, added, deleted, untracked };
1621
- }
1622
- async function detachedShortSha(cwd) {
1623
- const res = await runGit(cwd, ["rev-parse", "--short", "HEAD"]);
1624
- return res.ok ? res.stdout.trim() : null;
1625
- }
1626
- function runGit(cwd, args) {
1627
- return new Promise((resolve) => {
1628
- let stdout = "";
1629
- try {
1630
- const child = spawn("git", args, {
1631
- cwd,
1632
- // Inherit stderr (silent) — we don't care about git's noise.
1633
- stdio: ["ignore", "pipe", "ignore"],
1634
- // Don't let a slow git hang the TUI.
1635
- timeout: 3e3,
1636
- windowsHide: true
1637
- });
1638
- child.stdout.setEncoding("utf8");
1639
- child.stdout.on("data", (chunk) => {
1640
- stdout += chunk;
1641
- });
1642
- child.on("error", () => resolve({ ok: false, stdout: "" }));
1643
- child.on("close", (code) => {
1644
- resolve({ ok: code === 0, stdout });
1645
- });
1646
- } catch {
1647
- resolve({ ok: false, stdout: "" });
1648
- }
1649
- });
1650
- }
1651
2068
  function reducer(state, action) {
1652
2069
  switch (action.type) {
1653
2070
  case "addEntry": {
1654
- const appended = [
1655
- ...state.entries,
1656
- { ...action.entry, id: state.nextId }
1657
- ];
2071
+ const appended = [...state.entries, { ...action.entry, id: state.nextId }];
1658
2072
  return { ...state, entries: appended, nextId: state.nextId + 1 };
1659
2073
  }
1660
2074
  case "setBuffer":
@@ -1687,6 +2101,10 @@ function reducer(state, action) {
1687
2101
  return { ...state, status: action.status };
1688
2102
  case "interrupt":
1689
2103
  return { ...state, interrupts: state.interrupts + 1 };
2104
+ case "steerStart":
2105
+ return { ...state, steeringPending: true, steerSnapshot: action.snapshot };
2106
+ case "steerConsume":
2107
+ return { ...state, steeringPending: false, steerSnapshot: null };
1690
2108
  case "resetInterrupts":
1691
2109
  return { ...state, interrupts: 0 };
1692
2110
  case "hint":
@@ -1748,14 +2166,20 @@ function reducer(state, action) {
1748
2166
  }
1749
2167
  return {
1750
2168
  ...state,
1751
- toolStream: { toolUseId: action.toolUseId, name: action.name, text: action.text }
2169
+ toolStream: {
2170
+ toolUseId: action.toolUseId,
2171
+ name: action.name,
2172
+ text: action.text,
2173
+ startedAt: action.startedAt
2174
+ }
1752
2175
  };
1753
2176
  }
1754
2177
  case "toolStreamClear": {
1755
2178
  if (state.toolStream === null) return state;
1756
2179
  const t = state.toolStream;
1757
2180
  if (action.toolUseId !== void 0 && action.toolUseId !== t.toolUseId) return state;
1758
- if (action.name !== void 0 && action.toolUseId === void 0 && action.name !== t.name) return state;
2181
+ if (action.name !== void 0 && action.toolUseId === void 0 && action.name !== t.name)
2182
+ return state;
1759
2183
  return { ...state, toolStream: null };
1760
2184
  }
1761
2185
  case "enqueue": {
@@ -1875,15 +2299,240 @@ function reducer(state, action) {
1875
2299
  ...state,
1876
2300
  modelPicker: { ...state.modelPicker, hint: action.text }
1877
2301
  };
1878
- case "confirmOpen":
1879
- return { ...state, confirm: action.info };
1880
- case "confirmClose":
1881
- return { ...state, confirm: null };
1882
- case "resetContextChip":
1883
- return { ...state, contextChipVersion: state.contextChipVersion + 1 };
2302
+ case "confirmOpen":
2303
+ return { ...state, confirm: action.info };
2304
+ case "confirmClose":
2305
+ return { ...state, confirm: null };
2306
+ case "resetContextChip":
2307
+ return { ...state, contextChipVersion: state.contextChipVersion + 1 };
2308
+ // --- Fleet ---
2309
+ case "fleetSeed": {
2310
+ const seeded = {};
2311
+ for (const e of action.entries) seeded[e.id] = e;
2312
+ return { ...state, fleet: seeded, fleetCost: action.cost };
2313
+ }
2314
+ case "fleetSpawn": {
2315
+ if (state.fleet[action.id]) return state;
2316
+ const entry = {
2317
+ id: action.id,
2318
+ name: action.name ?? action.id.slice(0, 8),
2319
+ provider: action.provider,
2320
+ model: action.model,
2321
+ status: "idle",
2322
+ streamingText: "",
2323
+ iterations: 0,
2324
+ toolCalls: 0,
2325
+ cost: 0,
2326
+ startedAt: Date.now(),
2327
+ lastEventAt: Date.now(),
2328
+ transcriptPath: action.transcriptPath
2329
+ };
2330
+ return { ...state, fleet: { ...state.fleet, [action.id]: entry } };
2331
+ }
2332
+ case "fleetToolStart": {
2333
+ const cur = state.fleet[action.id];
2334
+ if (!cur) return state;
2335
+ return {
2336
+ ...state,
2337
+ fleet: {
2338
+ ...state.fleet,
2339
+ [action.id]: {
2340
+ ...cur,
2341
+ currentTool: { name: action.name, startedAt: Date.now() },
2342
+ lastEventAt: Date.now()
2343
+ }
2344
+ }
2345
+ };
2346
+ }
2347
+ case "fleetToolEnd": {
2348
+ const cur = state.fleet[action.id];
2349
+ if (!cur) return state;
2350
+ return {
2351
+ ...state,
2352
+ fleet: {
2353
+ ...state.fleet,
2354
+ [action.id]: { ...cur, currentTool: void 0, lastEventAt: Date.now() }
2355
+ }
2356
+ };
2357
+ }
2358
+ case "fleetStart": {
2359
+ const cur = state.fleet[action.id];
2360
+ if (!cur) return state;
2361
+ return {
2362
+ ...state,
2363
+ fleet: {
2364
+ ...state.fleet,
2365
+ [action.id]: {
2366
+ ...cur,
2367
+ status: "running",
2368
+ streamingText: "",
2369
+ startedAt: Date.now()
2370
+ }
2371
+ }
2372
+ };
2373
+ }
2374
+ case "fleetDelta": {
2375
+ const cur = state.fleet[action.id];
2376
+ if (!cur) return state;
2377
+ const appended = (cur.streamingText + action.text).slice(-200);
2378
+ return {
2379
+ ...state,
2380
+ fleet: {
2381
+ ...state.fleet,
2382
+ [action.id]: { ...cur, streamingText: appended, lastEventAt: Date.now() }
2383
+ }
2384
+ };
2385
+ }
2386
+ case "fleetTool": {
2387
+ const cur = state.fleet[action.id];
2388
+ if (!cur) return state;
2389
+ return {
2390
+ ...state,
2391
+ fleet: {
2392
+ ...state.fleet,
2393
+ [action.id]: { ...cur, toolCalls: cur.toolCalls + 1, lastEventAt: Date.now() }
2394
+ }
2395
+ };
2396
+ }
2397
+ case "fleetUsage": {
2398
+ const cur = state.fleet[action.id];
2399
+ if (!cur) return state;
2400
+ const cost = cur.cost;
2401
+ return {
2402
+ ...state,
2403
+ fleet: { ...state.fleet, [action.id]: { ...cur, cost, lastEventAt: Date.now() } }
2404
+ };
2405
+ }
2406
+ case "fleetDone": {
2407
+ const cur = state.fleet[action.id];
2408
+ if (!cur) return state;
2409
+ return {
2410
+ ...state,
2411
+ fleet: {
2412
+ ...state.fleet,
2413
+ [action.id]: {
2414
+ ...cur,
2415
+ status: action.status,
2416
+ iterations: action.iterations,
2417
+ toolCalls: action.toolCalls,
2418
+ streamingText: "",
2419
+ lastEventAt: Date.now()
2420
+ }
2421
+ }
2422
+ };
2423
+ }
2424
+ case "fleetCost": {
2425
+ return { ...state, fleetCost: action.cost };
2426
+ }
2427
+ case "setStreamFleet": {
2428
+ return { ...state, streamFleet: action.enabled };
2429
+ }
1884
2430
  }
1885
2431
  }
1886
2432
  var PASTE_THRESHOLD_CHARS = 200;
2433
+ function buildSteeringPreamble(snapshot, newDirection) {
2434
+ const lines = ["[STEERING \u2014 I pressed Esc to interrupt you mid-task on purpose.", ""];
2435
+ const ctx = [];
2436
+ if (snapshot?.runningTools && snapshot.runningTools.length > 0) {
2437
+ ctx.push(`- in-flight tools (now cancelled): ${snapshot.runningTools.join(", ")}`);
2438
+ }
2439
+ if (snapshot?.subagentsTerminated && snapshot.subagentsTerminated > 0) {
2440
+ const subDetails = snapshot.subagents.map((s) => `${s.label}${s.tool ? ` (was running: ${s.tool})` : ""}`).join(", ");
2441
+ ctx.push(
2442
+ `- subagents (${snapshot.subagentsTerminated} terminated by me, do NOT await them): ${subDetails}`
2443
+ );
2444
+ }
2445
+ if (snapshot?.partialAssistantText && snapshot.partialAssistantText.trim().length > 0) {
2446
+ const tail = snapshot.partialAssistantText.trim().slice(-300);
2447
+ ctx.push(`- your last partial output (truncated, for context only): "${tail}"`);
2448
+ }
2449
+ if (ctx.length > 0) {
2450
+ lines.push("What was happening when I cut you off:");
2451
+ lines.push(...ctx);
2452
+ lines.push("");
2453
+ }
2454
+ lines.push("You have authority to:");
2455
+ lines.push("- Abandon the prior plan entirely if the new direction makes it stale.");
2456
+ lines.push("- Re-spawn fresh subagents (with different roles or tasks) if needed.");
2457
+ lines.push('- Skip a polite "should I continue?" \u2014 just pivot.');
2458
+ lines.push("- Ask me to clarify if the new direction is genuinely ambiguous.");
2459
+ lines.push("");
2460
+ lines.push("New direction:");
2461
+ lines.push("---");
2462
+ lines.push(newDirection);
2463
+ lines.push("---");
2464
+ lines.push("]");
2465
+ return lines.join("\n");
2466
+ }
2467
+ function buildGoalPreamble(goal) {
2468
+ return [
2469
+ "[GOAL \u2014 LOCKED IN. You will work on this until it is verifiably done.",
2470
+ "The user granted you full autonomy. Read these constraints once, then act.",
2471
+ "",
2472
+ "YOUR GOAL:",
2473
+ "---",
2474
+ goal,
2475
+ "---",
2476
+ "",
2477
+ "AUTHORITY YOU HAVE:",
2478
+ "- Spawn as many subagents as the work needs (delegate / spawn_subagent).",
2479
+ " Parallel + recursive fan-out are both fine. There is no spawn budget.",
2480
+ "- Use any provider/model per subagent \u2014 pick the right tool for each",
2481
+ " piece of work. Heavy reasoning model for planning, fast model for",
2482
+ " batch work, specialist model for domain code.",
2483
+ "- Run unlimited tool calls and iterations. There is NO hidden budget.",
2484
+ " The Agent loop auto-extends every 100 iterations forever.",
2485
+ "- Retry failed tools with different inputs, alternative paths, fresh",
2486
+ " subagents. Switch providers mid-run if one is rate-limited.",
2487
+ "- Re-plan freely when an approach hits a dead end. You are not obliged",
2488
+ " to stick with the first plan you proposed.",
2489
+ "",
2490
+ 'WHAT "DONE" MEANS \u2014 non-negotiable:',
2491
+ "- You can name a concrete artifact (a passing test, a written file at",
2492
+ " a specific path, a fixed bug verified by re-running the failing case,",
2493
+ " a clean grep that previously had matches).",
2494
+ "- You can tell the user HOW to verify it themselves in 10 seconds.",
2495
+ '- You have NOT hedged. None of: "looks like it should work", "I',
2496
+ ' believe this fixes it", "the changes appear correct".',
2497
+ "",
2498
+ "WHAT IS NOT DONE \u2014 never report any of these as completion:",
2499
+ "- An error message you didn't recover from.",
2500
+ '- An empty result, a 0-line file, a "no matches found" you accepted',
2501
+ " without questioning the search.",
2502
+ '- "Should I continue?" / "Want me to also...?" / "Let me know if you',
2503
+ ' want X." Those are hedges. The user already told you to finish the',
2504
+ " goal \u2014 just do it.",
2505
+ "- Partial progress dressed up as success. Fixed 3 of 5 bugs = 60%",
2506
+ " done, not done.",
2507
+ "- A subagent's failed/timeout/stopped TaskResult that you didn't",
2508
+ " respond to with a fresh attempt (different role, different model,",
2509
+ " tighter prompt).",
2510
+ "",
2511
+ "PERSISTENCE PROTOCOL:",
2512
+ "- If blocked, try at least 3 different angles before reporting the",
2513
+ " problem to the user. Different tool inputs, different subagent",
2514
+ " roles, different providers, different decomposition of the task.",
2515
+ "- If a tool fails, read its error, alter the input, try again. Do",
2516
+ " not just report the failure back.",
2517
+ "- If a subagent returns useless output, respawn with a tighter prompt",
2518
+ ' or a different role. Do not accept "I could not determine\u2026" as the',
2519
+ " final answer.",
2520
+ "- Use `ask_subagent` for one-shot questions when you don't need a",
2521
+ " full delegated task.",
2522
+ "",
2523
+ "REPORTING:",
2524
+ "- Stream short progress notes between major actions so the user can",
2525
+ " monitor. Do not go silent for 50 tool calls then dump a wall of",
2526
+ " text \u2014 but also do not narrate every tool call.",
2527
+ "- Use the shared scratchpad (if available) to leave breadcrumbs",
2528
+ " subagents can read.",
2529
+ "- Final response must include: (a) what was accomplished, (b) how",
2530
+ " to verify, (c) any caveats (residual TODOs, things the user",
2531
+ " should know about).",
2532
+ "",
2533
+ "BEGIN.]"
2534
+ ].join("\n");
2535
+ }
1887
2536
  function App({
1888
2537
  agent,
1889
2538
  slashRegistry,
@@ -1902,7 +2551,12 @@ function App({
1902
2551
  switchProviderAndModel,
1903
2552
  effectiveMaxContext,
1904
2553
  onExit,
1905
- onClearHistory
2554
+ director,
2555
+ fleetRoster,
2556
+ onClearHistory,
2557
+ fleetStreamController,
2558
+ initialGoal,
2559
+ initialAsk
1906
2560
  }) {
1907
2561
  const { exit } = useApp();
1908
2562
  const [liveModel, setLiveModel] = useState(model);
@@ -1927,6 +2581,8 @@ function App({
1927
2581
  toolStream: null,
1928
2582
  status: "idle",
1929
2583
  interrupts: 0,
2584
+ steeringPending: false,
2585
+ steerSnapshot: null,
1930
2586
  hint: "",
1931
2587
  nextId: 1,
1932
2588
  picker: { open: false, query: "", matches: [], selected: 0 },
@@ -1944,13 +2600,18 @@ function App({
1944
2600
  selected: 0
1945
2601
  },
1946
2602
  confirm: null,
1947
- contextChipVersion: 0
2603
+ contextChipVersion: 0,
2604
+ fleet: {},
2605
+ fleetCost: 0,
2606
+ streamFleet: true
1948
2607
  });
1949
2608
  const builderRef = useRef(null);
1950
2609
  if (builderRef.current === null) {
1951
2610
  builderRef.current = new InputBuilder({ store: attachments });
1952
2611
  }
1953
2612
  const activeCtrlRef = useRef(null);
2613
+ const inputGateRef = useRef(false);
2614
+ const lastEnterAtRef = useRef(0);
1954
2615
  const projectRoot = agent.ctx.projectRoot;
1955
2616
  const projectName = React.useMemo(() => {
1956
2617
  const base = path3.basename(projectRoot);
@@ -1986,20 +2647,18 @@ function App({
1986
2647
  const [lastInputTokens, setLastInputTokens] = React.useState(0);
1987
2648
  useEffect(() => {
1988
2649
  const off = events.on("provider.response", (e) => {
1989
- setLastInputTokens(e.usage.input);
2650
+ const total = (e.usage.input ?? 0) + (e.usage.cacheRead ?? 0) + (e.usage.cacheWrite ?? 0);
2651
+ setLastInputTokens(total);
1990
2652
  });
1991
2653
  return () => {
1992
2654
  off();
1993
2655
  };
1994
2656
  }, [events]);
1995
2657
  const maxContext = effectiveMaxContext ?? agent.ctx.provider.capabilities.maxContext;
1996
- const contextWindow = useMemo(
1997
- () => {
1998
- void state.contextChipVersion;
1999
- return lastInputTokens > 0 && maxContext > 0 ? { used: lastInputTokens, max: maxContext } : void 0;
2000
- },
2001
- [lastInputTokens, maxContext, state.contextChipVersion]
2002
- );
2658
+ const contextWindow = useMemo(() => {
2659
+ void state.contextChipVersion;
2660
+ return lastInputTokens > 0 && maxContext > 0 ? { used: lastInputTokens, max: maxContext } : void 0;
2661
+ }, [lastInputTokens, maxContext, state.contextChipVersion]);
2003
2662
  const todos = useMemo(() => {
2004
2663
  const counts = { pending: 0, inProgress: 0, completed: 0 };
2005
2664
  for (const t of agent.ctx.todos) {
@@ -2009,6 +2668,111 @@ function App({
2009
2668
  }
2010
2669
  return counts;
2011
2670
  }, [nowTick, agent.ctx.todos]);
2671
+ const fleetCounts = useMemo(() => {
2672
+ const entries = Object.values(state.fleet);
2673
+ if (entries.length === 0) return void 0;
2674
+ let running = 0;
2675
+ let idle = 0;
2676
+ let completed = 0;
2677
+ for (const e of entries) {
2678
+ if (e.status === "running") running += 1;
2679
+ else if (e.status === "idle") idle += 1;
2680
+ else completed += 1;
2681
+ }
2682
+ return { running, idle, pending: 0, completed };
2683
+ }, [state.fleet]);
2684
+ const STREAM_COLORS = ["cyan", "magenta", "yellow", "green", "blue"];
2685
+ const labelsRef = useRef(/* @__PURE__ */ new Map());
2686
+ const labelFor = (id, name) => {
2687
+ const m = labelsRef.current;
2688
+ const existing = m.get(id);
2689
+ if (existing) return existing;
2690
+ const n = m.size + 1;
2691
+ const suffix = name && name !== id ? ` ${name}` : "";
2692
+ const v = {
2693
+ label: `AGENT#${n}${suffix}`,
2694
+ color: STREAM_COLORS[(n - 1) % STREAM_COLORS.length]
2695
+ };
2696
+ m.set(id, v);
2697
+ return v;
2698
+ };
2699
+ const fleetAgents = useMemo(() => {
2700
+ const entries = Object.entries(state.fleet);
2701
+ if (entries.length === 0) return void 0;
2702
+ const active = entries.filter(([_id, e]) => e.status === "running" || e.status === "idle");
2703
+ if (active.length === 0) return void 0;
2704
+ active.sort((a, b) => {
2705
+ const sa = a[1].status === "running" ? 0 : 1;
2706
+ const sb = b[1].status === "running" ? 0 : 1;
2707
+ if (sa !== sb) return sa - sb;
2708
+ return a[1].startedAt - b[1].startedAt;
2709
+ });
2710
+ return active.slice(0, 4).map(([id, e]) => {
2711
+ const lbl = labelFor(id, e.name);
2712
+ return {
2713
+ label: lbl.label,
2714
+ color: lbl.color,
2715
+ elapsedMs: Math.max(0, nowTick - e.startedAt),
2716
+ toolCalls: e.toolCalls,
2717
+ running: e.status === "running"
2718
+ };
2719
+ });
2720
+ }, [state.fleet, nowTick]);
2721
+ const [planCounts, setPlanCounts] = useState(null);
2722
+ useEffect(() => {
2723
+ const planPath = agent.ctx.meta["plan.path"];
2724
+ if (typeof planPath !== "string" || !planPath) return;
2725
+ let cancelled = false;
2726
+ const poll = async () => {
2727
+ try {
2728
+ const data = await fs.readFile(planPath, "utf8");
2729
+ const parsed = JSON.parse(data);
2730
+ if (cancelled) return;
2731
+ if (!Array.isArray(parsed.items)) {
2732
+ setPlanCounts(null);
2733
+ return;
2734
+ }
2735
+ let open = 0;
2736
+ let inProgress = 0;
2737
+ let done = 0;
2738
+ for (const it of parsed.items) {
2739
+ if (it?.status === "done") done++;
2740
+ else if (it?.status === "in_progress") inProgress++;
2741
+ else open++;
2742
+ }
2743
+ setPlanCounts(open + inProgress + done > 0 ? { open, inProgress, done } : null);
2744
+ } catch {
2745
+ if (!cancelled) setPlanCounts(null);
2746
+ }
2747
+ };
2748
+ void poll();
2749
+ const id = setInterval(poll, 3e3);
2750
+ return () => {
2751
+ cancelled = true;
2752
+ clearInterval(id);
2753
+ };
2754
+ }, [agent.ctx.meta]);
2755
+ const prevAnyOverlayOpen = useRef(false);
2756
+ const prevEntriesCount = useRef(0);
2757
+ useEffect(() => {
2758
+ const anyOpenNow = state.picker.open || state.slashPicker.open || state.modelPicker.open || !!state.confirm;
2759
+ const overlayClosed = prevAnyOverlayOpen.current && !anyOpenNow;
2760
+ const newEntryCommitted = state.entries.length > prevEntriesCount.current;
2761
+ prevAnyOverlayOpen.current = anyOpenNow;
2762
+ prevEntriesCount.current = state.entries.length;
2763
+ if (overlayClosed || newEntryCommitted) {
2764
+ try {
2765
+ process.stdout.write("\x1B[J");
2766
+ } catch {
2767
+ }
2768
+ }
2769
+ }, [
2770
+ state.picker.open,
2771
+ state.slashPicker.open,
2772
+ state.modelPicker.open,
2773
+ state.confirm,
2774
+ state.entries.length
2775
+ ]);
2012
2776
  useEffect(() => {
2013
2777
  const detected = detectAtToken(state.buffer, state.cursor);
2014
2778
  if (!detected) {
@@ -2095,7 +2859,7 @@ function App({
2095
2859
  }
2096
2860
  const absPath = path3.isAbsolute(picked) ? picked : path3.join(projectRoot, picked);
2097
2861
  try {
2098
- const data = await fs2.readFile(absPath, "utf8");
2862
+ const data = await fs.readFile(absPath, "utf8");
2099
2863
  const placeholder = await builder.appendFile({
2100
2864
  kind: "file",
2101
2865
  data,
@@ -2113,7 +2877,10 @@ function App({
2113
2877
  } catch (err) {
2114
2878
  dispatch({
2115
2879
  type: "addEntry",
2116
- entry: { kind: "error", text: `Attach failed: ${err instanceof Error ? err.message : String(err)}` }
2880
+ entry: {
2881
+ kind: "error",
2882
+ text: `Attach failed: ${err instanceof Error ? err.message : String(err)}`
2883
+ }
2117
2884
  });
2118
2885
  dispatch({ type: "pickerClose" });
2119
2886
  }
@@ -2165,6 +2932,128 @@ function App({
2165
2932
  slashRegistry.unregister("queue");
2166
2933
  };
2167
2934
  }, [slashRegistry]);
2935
+ useEffect(() => {
2936
+ const ALT_OFF = "\x1B[?1049l";
2937
+ const ALT_ON = "\x1B[?1049h";
2938
+ const cmd = {
2939
+ name: "altscreen",
2940
+ description: "Toggle the alt-screen buffer. Default is OFF (native scroll); /altscreen on for full-screen mode.",
2941
+ async run(args) {
2942
+ const arg = args.trim().toLowerCase();
2943
+ if (arg === "off") {
2944
+ try {
2945
+ process.stdout.write(ALT_OFF);
2946
+ } catch {
2947
+ return { message: "Failed to exit alt-screen." };
2948
+ }
2949
+ return {
2950
+ message: "Alt-screen disabled. New entries will land in normal scrollback (mouse wheel / Shift+PgUp work). On-screen history rendered before this command is no longer reachable via terminal scroll. Resize may now leak the live region \u2014 `/altscreen on` to re-enable."
2951
+ };
2952
+ }
2953
+ if (arg === "on") {
2954
+ try {
2955
+ process.stdout.write(ALT_ON);
2956
+ } catch {
2957
+ return { message: "Failed to re-enter alt-screen." };
2958
+ }
2959
+ return { message: "Alt-screen re-enabled. Native scroll is now disabled." };
2960
+ }
2961
+ return { message: "Usage: /altscreen on|off" };
2962
+ }
2963
+ };
2964
+ slashRegistry.register(cmd);
2965
+ return () => {
2966
+ slashRegistry.unregister("altscreen");
2967
+ };
2968
+ }, [slashRegistry]);
2969
+ useEffect(() => {
2970
+ const cmd = {
2971
+ name: "steer",
2972
+ description: "Interrupt the running agent (incl. fleet) and redirect: /steer <new direction>",
2973
+ help: [
2974
+ "Usage: /steer <new direction>",
2975
+ "",
2976
+ "Aborts the active iteration, terminates any running subagents,",
2977
+ "drops queued messages, and sends your text to the model with a",
2978
+ "STEERING preamble explaining what was in flight and what the",
2979
+ "model is authorised to do (pivot hard, respawn subagents, ask",
2980
+ "for clarification). Equivalent to pressing Esc then typing."
2981
+ ].join("\n"),
2982
+ async run(args) {
2983
+ const text = args.trim();
2984
+ if (!text) {
2985
+ return { message: "Usage: /steer <new direction>" };
2986
+ }
2987
+ const s = stateRef.current;
2988
+ const runningTools = Array.from(s.runningTools.values()).map((t) => t.name);
2989
+ const subagents = Object.values(s.fleet).filter((e) => e.status === "running").map((e) => ({ label: e.name, status: e.status, tool: e.currentTool?.name }));
2990
+ const subagentsTerminated = subagents.length;
2991
+ const partialAssistantText = streamingTextRef.current.slice(-1500);
2992
+ activeCtrlRef.current?.abort();
2993
+ dispatch({
2994
+ type: "steerStart",
2995
+ snapshot: { runningTools, subagents, subagentsTerminated, partialAssistantText }
2996
+ });
2997
+ const droppedCount = s.queue.length;
2998
+ if (droppedCount > 0) dispatch({ type: "queueClear" });
2999
+ if (director && subagentsTerminated > 0) {
3000
+ const cap = new Promise((resolve) => {
3001
+ const t = setTimeout(resolve, 1500);
3002
+ t.unref?.();
3003
+ });
3004
+ void Promise.race([director.terminateAll().catch(() => void 0), cap]);
3005
+ }
3006
+ const preamble = buildSteeringPreamble(
3007
+ { runningTools, subagents, subagentsTerminated, partialAssistantText },
3008
+ text
3009
+ );
3010
+ dispatch({ type: "steerConsume" });
3011
+ const droppedTag = droppedCount > 0 ? ` \xB7 dropped ${droppedCount} queued` : "";
3012
+ const fleetTag = subagentsTerminated > 0 ? ` \xB7 stopped ${subagentsTerminated} subagent${subagentsTerminated === 1 ? "" : "s"}` : "";
3013
+ return {
3014
+ message: `\u21AF Steering${droppedTag}${fleetTag}.`,
3015
+ runText: preamble
3016
+ };
3017
+ }
3018
+ };
3019
+ slashRegistry.register(cmd);
3020
+ return () => {
3021
+ slashRegistry.unregister("steer");
3022
+ };
3023
+ }, [slashRegistry, director]);
3024
+ useEffect(() => {
3025
+ const cmd = {
3026
+ name: "goal",
3027
+ description: "Lock in a goal \u2014 no budgets, no hedging, no premature done. /goal <description>",
3028
+ help: [
3029
+ "Usage: /goal <description>",
3030
+ "",
3031
+ "Hands the agent a task it must drive to a verifiable finish.",
3032
+ "Adds a preamble to the next turn that grants full autonomy",
3033
+ "(unlimited subagents, any provider/model, retry-until-it-works),",
3034
+ 'spells out what "done" actually means, and forbids hedge-style',
3035
+ 'completions ("I believe this works", "should I continue?").',
3036
+ "",
3037
+ "Combine with /steer to redirect mid-goal, or Ctrl+C / /fleet kill",
3038
+ "to bail out \u2014 only the user can stop a /goal."
3039
+ ].join("\n"),
3040
+ async run(args) {
3041
+ const goal = args.trim();
3042
+ if (!goal) return { message: "Usage: /goal <description>" };
3043
+ const preamble = buildGoalPreamble(goal);
3044
+ const shortGoal = goal.length > 80 ? `${goal.slice(0, 80)}\u2026` : goal;
3045
+ return {
3046
+ message: `\u{1F3AF} Goal locked: ${shortGoal}
3047
+ Agent will work until verifiably complete. Esc / /steer to redirect, Ctrl+C to stop.`,
3048
+ runText: preamble
3049
+ };
3050
+ }
3051
+ };
3052
+ slashRegistry.register(cmd);
3053
+ return () => {
3054
+ slashRegistry.unregister("goal");
3055
+ };
3056
+ }, [slashRegistry]);
2168
3057
  useEffect(() => {
2169
3058
  if (!getPickableProviders || !switchProviderAndModel) return;
2170
3059
  const cmd = {
@@ -2206,7 +3095,8 @@ function App({
2206
3095
  type: "toolStreamAppend",
2207
3096
  toolUseId: e.id,
2208
3097
  name: e.name,
2209
- text: e.event.text
3098
+ text: e.event.text,
3099
+ startedAt: Date.now()
2210
3100
  });
2211
3101
  });
2212
3102
  const offTool = events.on("tool.executed", (e) => {
@@ -2218,11 +3108,23 @@ function App({
2218
3108
  durationMs: e.durationMs,
2219
3109
  ok: e.ok,
2220
3110
  input: e.input,
2221
- output: e.output
3111
+ output: e.output,
3112
+ // Real model-visible sizes — forwarded so the size chip beside
3113
+ // the tool header can show what the model paid for instead of
3114
+ // the misleading preview-byte count we used to surface.
3115
+ outputBytes: e.outputBytes,
3116
+ outputTokens: e.outputTokens,
3117
+ outputLines: e.outputLines
2222
3118
  }
2223
3119
  });
2224
3120
  dispatch({ type: "toolEnded", name: e.name });
2225
3121
  dispatch({ type: "toolStreamClear", name: e.name });
3122
+ if (e.ok && e.name === "todo") {
3123
+ dispatch({
3124
+ type: "addEntry",
3125
+ entry: { kind: "info", text: formatTodosList(agent.ctx.todos) }
3126
+ });
3127
+ }
2226
3128
  });
2227
3129
  const offRetry = events.on("provider.retry", (e) => {
2228
3130
  const secs = (e.delayMs / 1e3).toFixed(e.delayMs >= 1e3 ? 1 : 2);
@@ -2237,6 +3139,19 @@ function App({
2237
3139
  entry: { kind: "error", text: e.description }
2238
3140
  });
2239
3141
  });
3142
+ const offProvResp = events.on("provider.response", () => {
3143
+ const text = streamingTextRef.current;
3144
+ streamingTextRef.current = "";
3145
+ pendingDeltaRef.current = "";
3146
+ if (flushTimerRef.current) {
3147
+ clearTimeout(flushTimerRef.current);
3148
+ flushTimerRef.current = null;
3149
+ }
3150
+ dispatch({ type: "streamReset" });
3151
+ if (text.trim()) {
3152
+ dispatch({ type: "addEntry", entry: { kind: "assistant", text } });
3153
+ }
3154
+ });
2240
3155
  const offConfirmNeeded = events.on("tool.confirm_needed", (e) => {
2241
3156
  dispatch({
2242
3157
  type: "addEntry",
@@ -2265,21 +3180,302 @@ function App({
2265
3180
  offTool();
2266
3181
  offRetry();
2267
3182
  offProvErr();
3183
+ offProvResp();
2268
3184
  offConfirmNeeded();
2269
3185
  if (flushTimerRef.current) clearTimeout(flushTimerRef.current);
2270
3186
  };
3187
+ }, [events, agent.ctx.todos]);
3188
+ const streamFleetRef = useRef(state.streamFleet);
3189
+ useEffect(() => {
3190
+ streamFleetRef.current = state.streamFleet;
3191
+ }, [state.streamFleet]);
3192
+ useEffect(() => {
3193
+ const offSpawned = events.on("subagent.spawned", (e) => {
3194
+ const lbl = labelFor(e.subagentId, e.name);
3195
+ dispatch({
3196
+ type: "fleetSpawn",
3197
+ id: e.subagentId,
3198
+ name: e.name,
3199
+ provider: e.provider,
3200
+ model: e.model,
3201
+ transcriptPath: e.transcriptPath
3202
+ });
3203
+ const where = e.provider && e.model ? `${e.provider}/${e.model}` : "spawned";
3204
+ const desc = e.description ? ` \u2014 ${e.description.slice(0, 80)}` : "";
3205
+ dispatch({
3206
+ type: "addEntry",
3207
+ entry: {
3208
+ kind: "subagent",
3209
+ agentLabel: lbl.label,
3210
+ agentColor: lbl.color,
3211
+ icon: "\u25B6",
3212
+ text: `${where}${desc}`
3213
+ }
3214
+ });
3215
+ });
3216
+ const offStarted = events.on("subagent.task_started", (e) => {
3217
+ const lbl = labelFor(e.subagentId);
3218
+ dispatch({ type: "fleetStart", id: e.subagentId, taskId: e.taskId });
3219
+ const desc = e.description ? ` \u2014 ${e.description.slice(0, 80)}` : "";
3220
+ dispatch({
3221
+ type: "addEntry",
3222
+ entry: {
3223
+ kind: "subagent",
3224
+ agentLabel: lbl.label,
3225
+ agentColor: lbl.color,
3226
+ icon: "\u25CF",
3227
+ text: `task started${desc}`
3228
+ }
3229
+ });
3230
+ });
3231
+ const offCompleted = events.on("subagent.task_completed", (e) => {
3232
+ const lbl = labelFor(e.subagentId);
3233
+ dispatch({
3234
+ type: "fleetDone",
3235
+ id: e.subagentId,
3236
+ status: e.status,
3237
+ iterations: e.iterations,
3238
+ toolCalls: e.toolCalls
3239
+ });
3240
+ const icon = e.status === "success" ? "\u2713" : e.status === "timeout" ? "\u23F1" : e.status === "stopped" ? "\u2298" : "\u2717";
3241
+ const errKind = e.error?.kind;
3242
+ const errMsg = e.error?.message;
3243
+ const errMsgTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 100)}${errMsg.length > 100 ? "\u2026" : ""}` : "";
3244
+ const errChip = errKind ? ` [${errKind}]` : "";
3245
+ const secs = (e.durationMs / 1e3).toFixed(e.durationMs < 1e4 ? 1 : 0);
3246
+ dispatch({
3247
+ type: "addEntry",
3248
+ entry: {
3249
+ kind: "subagent",
3250
+ agentLabel: lbl.label,
3251
+ agentColor: lbl.color,
3252
+ icon,
3253
+ text: `${e.status} (${e.iterations} iter \xB7 ${e.toolCalls} tools \xB7 ${secs}s)${errChip}${errMsgTail}`
3254
+ }
3255
+ });
3256
+ });
3257
+ const offTool = events.on("subagent.tool_executed", (e) => {
3258
+ const lbl = labelFor(e.subagentId);
3259
+ dispatch({ type: "fleetTool", id: e.subagentId });
3260
+ dispatch({ type: "fleetToolEnd", id: e.subagentId });
3261
+ const bytesTag = typeof e.outputBytes === "number" && e.outputBytes > 0 ? ` \xB7 ${e.outputBytes < 1024 ? `${e.outputBytes}B` : `${(e.outputBytes / 1024).toFixed(1)}KB`}` : "";
3262
+ dispatch({
3263
+ type: "addEntry",
3264
+ entry: {
3265
+ kind: "subagent",
3266
+ agentLabel: lbl.label,
3267
+ agentColor: lbl.color,
3268
+ icon: e.ok === false ? "\u2717" : "\u25CF",
3269
+ text: e.name,
3270
+ detail: `${e.durationMs}ms${bytesTag}`
3271
+ }
3272
+ });
3273
+ });
3274
+ return () => {
3275
+ offSpawned();
3276
+ offStarted();
3277
+ offCompleted();
3278
+ offTool();
3279
+ };
2271
3280
  }, [events]);
3281
+ useEffect(() => {
3282
+ if (!fleetStreamController) return;
3283
+ fleetStreamController.enabled = state.streamFleet;
3284
+ fleetStreamController.setEnabled = (enabled) => {
3285
+ dispatch({ type: "setStreamFleet", enabled });
3286
+ };
3287
+ return () => {
3288
+ fleetStreamController.setEnabled = (enabled) => {
3289
+ fleetStreamController.enabled = enabled;
3290
+ };
3291
+ };
3292
+ }, [fleetStreamController, state.streamFleet]);
3293
+ useEffect(() => {
3294
+ if (fleetStreamController) fleetStreamController.enabled = state.streamFleet;
3295
+ }, [state.streamFleet, fleetStreamController]);
3296
+ useEffect(() => {
3297
+ const d = director;
3298
+ if (!d) return;
3299
+ const FLUSH_MS = 150;
3300
+ const streamBuf = /* @__PURE__ */ new Map();
3301
+ let streamFlushTimer = null;
3302
+ const flushStreamBufs = () => {
3303
+ for (const [id, text] of streamBuf) {
3304
+ if (!text.trim()) continue;
3305
+ const lbl = labelFor(id);
3306
+ dispatch({
3307
+ type: "addEntry",
3308
+ entry: {
3309
+ kind: "subagent",
3310
+ agentLabel: lbl.label,
3311
+ agentColor: lbl.color,
3312
+ icon: "\u{1F4AC}",
3313
+ text: text.trim()
3314
+ }
3315
+ });
3316
+ }
3317
+ streamBuf.clear();
3318
+ streamFlushTimer = null;
3319
+ };
3320
+ const status = d.status();
3321
+ for (const s of status.subagents) {
3322
+ const meta = d.getSubagentMeta(s.id);
3323
+ dispatch({
3324
+ type: "fleetSpawn",
3325
+ id: s.id,
3326
+ name: meta?.name ?? s.name,
3327
+ provider: meta?.provider,
3328
+ model: meta?.model
3329
+ });
3330
+ labelFor(s.id, meta?.name ?? s.name);
3331
+ }
3332
+ dispatch({ type: "fleetCost", cost: d.snapshot().total.cost });
3333
+ const seen = new Set(Object.keys(status.subagents));
3334
+ const pending = /* @__PURE__ */ new Map();
3335
+ let flushTimer = null;
3336
+ const doFlush = () => {
3337
+ for (const [id, text] of pending) {
3338
+ if (text) dispatch({ type: "fleetDelta", id, text });
3339
+ }
3340
+ pending.clear();
3341
+ flushTimer = null;
3342
+ };
3343
+ const offFleet = d.fleet.onAny((e) => {
3344
+ const fresh = !seen.has(e.subagentId);
3345
+ if (fresh) {
3346
+ seen.add(e.subagentId);
3347
+ const meta = d.getSubagentMeta(e.subagentId);
3348
+ dispatch({
3349
+ type: "fleetSpawn",
3350
+ id: e.subagentId,
3351
+ name: meta?.name,
3352
+ provider: meta?.provider,
3353
+ model: meta?.model
3354
+ });
3355
+ const lbl = labelFor(e.subagentId, meta?.name);
3356
+ if (streamFleetRef.current) {
3357
+ const where = meta?.provider && meta?.model ? `${meta.provider}/${meta.model}` : "spawned";
3358
+ dispatch({
3359
+ type: "addEntry",
3360
+ entry: {
3361
+ kind: "subagent",
3362
+ agentLabel: lbl.label,
3363
+ agentColor: lbl.color,
3364
+ icon: "\u25B6",
3365
+ text: where
3366
+ }
3367
+ });
3368
+ }
3369
+ }
3370
+ switch (e.type) {
3371
+ case "iteration.started":
3372
+ dispatch({ type: "fleetStart", id: e.subagentId });
3373
+ break;
3374
+ case "provider.text_delta": {
3375
+ const p = e.payload;
3376
+ if (p?.text) {
3377
+ const cur = pending.get(e.subagentId) ?? "";
3378
+ pending.set(e.subagentId, cur + p.text);
3379
+ if (!flushTimer) flushTimer = setTimeout(doFlush, FLUSH_MS);
3380
+ if (streamFleetRef.current) {
3381
+ streamBuf.set(e.subagentId, (streamBuf.get(e.subagentId) ?? "") + p.text);
3382
+ if (!streamFlushTimer) streamFlushTimer = setTimeout(flushStreamBufs, FLUSH_MS * 4);
3383
+ }
3384
+ }
3385
+ break;
3386
+ }
3387
+ case "tool.started": {
3388
+ const p = e.payload;
3389
+ if (p?.name) {
3390
+ dispatch({ type: "fleetToolStart", id: e.subagentId, name: p.name });
3391
+ }
3392
+ break;
3393
+ }
3394
+ case "tool.executed": {
3395
+ dispatch({ type: "fleetTool", id: e.subagentId });
3396
+ dispatch({ type: "fleetToolEnd", id: e.subagentId });
3397
+ if (streamFleetRef.current) {
3398
+ if (streamFlushTimer) {
3399
+ clearTimeout(streamFlushTimer);
3400
+ flushStreamBufs();
3401
+ }
3402
+ const p = e.payload;
3403
+ const args = p?.input ? formatToolArgs(p.name ?? "", p.input) : "";
3404
+ const lbl = labelFor(e.subagentId);
3405
+ dispatch({
3406
+ type: "addEntry",
3407
+ entry: {
3408
+ kind: "subagent",
3409
+ agentLabel: lbl.label,
3410
+ agentColor: lbl.color,
3411
+ icon: p?.ok === false ? "\u2717" : "\u25CF",
3412
+ text: args ? `${p?.name ?? "tool"} ${args}` : p?.name ?? "tool",
3413
+ detail: typeof p?.durationMs === "number" ? `${p.durationMs}ms` : void 0
3414
+ }
3415
+ });
3416
+ }
3417
+ break;
3418
+ }
3419
+ case "provider.response": {
3420
+ dispatch({ type: "fleetCost", cost: d.snapshot().total.cost });
3421
+ break;
3422
+ }
3423
+ }
3424
+ });
3425
+ const offDone = d.on("task.completed", (payload) => {
3426
+ dispatch({
3427
+ type: "fleetDone",
3428
+ id: payload.result.subagentId,
3429
+ status: payload.result.status,
3430
+ iterations: payload.result.iterations,
3431
+ toolCalls: payload.result.toolCalls
3432
+ });
3433
+ dispatch({ type: "fleetCost", cost: d.snapshot().total.cost });
3434
+ if (streamFleetRef.current && streamFlushTimer) {
3435
+ clearTimeout(streamFlushTimer);
3436
+ flushStreamBufs();
3437
+ }
3438
+ });
3439
+ return () => {
3440
+ offFleet();
3441
+ offDone();
3442
+ if (flushTimer) clearTimeout(flushTimer);
3443
+ doFlush();
3444
+ if (streamFlushTimer) clearTimeout(streamFlushTimer);
3445
+ flushStreamBufs();
3446
+ };
3447
+ }, [director]);
2272
3448
  useEffect(() => {
2273
3449
  const onSigint = () => {
2274
- if (state.interrupts >= 1 && state.status === "idle") {
2275
- exit();
2276
- onExit(130);
3450
+ if (state.interrupts >= 1) {
3451
+ if (state.interrupts >= 2) {
3452
+ process.exit(130);
3453
+ }
3454
+ try {
3455
+ exit();
3456
+ onExit(130);
3457
+ } catch {
3458
+ }
3459
+ setTimeout(() => {
3460
+ try {
3461
+ process.exit(130);
3462
+ } catch {
3463
+ }
3464
+ }, 500).unref?.();
3465
+ dispatch({ type: "interrupt" });
2277
3466
  return;
2278
3467
  }
2279
3468
  dispatch({ type: "interrupt" });
2280
3469
  if (activeCtrlRef.current) {
2281
3470
  activeCtrlRef.current.abort();
2282
3471
  dispatch({ type: "status", status: "aborting" });
3472
+ if (director) {
3473
+ const cap = new Promise((resolve) => {
3474
+ const t = setTimeout(resolve, 1500);
3475
+ t.unref?.();
3476
+ });
3477
+ void Promise.race([director.terminateAll().catch(() => void 0), cap]);
3478
+ }
2283
3479
  const droppedCount = stateRef.current.queue.length;
2284
3480
  if (droppedCount > 0) {
2285
3481
  dispatch({ type: "queueClear" });
@@ -2287,13 +3483,16 @@ function App({
2287
3483
  type: "addEntry",
2288
3484
  entry: {
2289
3485
  kind: "warn",
2290
- text: `Iteration cancelled. Dropped ${droppedCount} queued message${droppedCount === 1 ? "" : "s"}. Press Ctrl+C again to exit.`
3486
+ text: `Iteration cancelled${director ? " + fleet terminated" : ""}. Dropped ${droppedCount} queued message${droppedCount === 1 ? "" : "s"}. Press Ctrl+C again to exit.`
2291
3487
  }
2292
3488
  });
2293
3489
  } else {
2294
3490
  dispatch({
2295
3491
  type: "addEntry",
2296
- entry: { kind: "warn", text: "Iteration cancelled. Press Ctrl+C again to exit." }
3492
+ entry: {
3493
+ kind: "warn",
3494
+ text: `Iteration cancelled${director ? " + fleet terminated" : ""}. Press Ctrl+C again to exit.`
3495
+ }
2297
3496
  });
2298
3497
  }
2299
3498
  } else {
@@ -2307,9 +3506,11 @@ function App({
2307
3506
  return () => {
2308
3507
  process.off("SIGINT", onSigint);
2309
3508
  };
2310
- }, [state.interrupts, state.status, exit, onExit]);
3509
+ }, [state.interrupts, exit, onExit, director]);
2311
3510
  const handleKey = async (input, key) => {
2312
3511
  if (state.status === "aborting") return;
3512
+ if (inputGateRef.current) return;
3513
+ const isEnter = key.return || input === "\r" || input === "\n";
2313
3514
  if (state.modelPicker.open) {
2314
3515
  if (key.escape) {
2315
3516
  if (state.modelPicker.step === "model") {
@@ -2327,33 +3528,38 @@ function App({
2327
3528
  dispatch({ type: "modelPickerMove", delta: 1 });
2328
3529
  return;
2329
3530
  }
2330
- if (key.return) {
2331
- if (state.modelPicker.step === "provider") {
2332
- const opt = state.modelPicker.providerOptions[state.modelPicker.selected];
2333
- if (!opt) return;
3531
+ if (isEnter) {
3532
+ inputGateRef.current = true;
3533
+ try {
3534
+ if (state.modelPicker.step === "provider") {
3535
+ const opt = state.modelPicker.providerOptions[state.modelPicker.selected];
3536
+ if (!opt) return;
3537
+ dispatch({
3538
+ type: "modelPickerPickProvider",
3539
+ providerId: opt.id,
3540
+ models: opt.models
3541
+ });
3542
+ return;
3543
+ }
3544
+ const providerId = state.modelPicker.pickedProviderId;
3545
+ const modelId = state.modelPicker.modelOptions[state.modelPicker.selected];
3546
+ if (!providerId || !modelId) return;
3547
+ const err = switchProviderAndModel?.(providerId, modelId);
3548
+ if (err) {
3549
+ dispatch({ type: "modelPickerHint", text: err });
3550
+ return;
3551
+ }
3552
+ setLiveProvider(providerId);
3553
+ setLiveModel(modelId);
2334
3554
  dispatch({
2335
- type: "modelPickerPickProvider",
2336
- providerId: opt.id,
2337
- models: opt.models
3555
+ type: "addEntry",
3556
+ entry: { kind: "info", text: `Switched to ${providerId} / ${modelId}.` }
2338
3557
  });
3558
+ dispatch({ type: "modelPickerClose" });
2339
3559
  return;
3560
+ } finally {
3561
+ inputGateRef.current = false;
2340
3562
  }
2341
- const providerId = state.modelPicker.pickedProviderId;
2342
- const modelId = state.modelPicker.modelOptions[state.modelPicker.selected];
2343
- if (!providerId || !modelId) return;
2344
- const err = switchProviderAndModel?.(providerId, modelId);
2345
- if (err) {
2346
- dispatch({ type: "modelPickerHint", text: err });
2347
- return;
2348
- }
2349
- setLiveProvider(providerId);
2350
- setLiveModel(modelId);
2351
- dispatch({
2352
- type: "addEntry",
2353
- entry: { kind: "info", text: `Switched to ${providerId} / ${modelId}.` }
2354
- });
2355
- dispatch({ type: "modelPickerClose" });
2356
- return;
2357
3563
  }
2358
3564
  return;
2359
3565
  }
@@ -2370,8 +3576,10 @@ function App({
2370
3576
  dispatch({ type: "slashPickerMove", delta: 1 });
2371
3577
  return;
2372
3578
  }
2373
- if (key.return) {
2374
- await acceptSlashPickerSelection();
3579
+ if (isEnter) {
3580
+ inputGateRef.current = true;
3581
+ acceptSlashPickerSelection();
3582
+ inputGateRef.current = false;
2375
3583
  return;
2376
3584
  }
2377
3585
  if (key.tab && state.slashPicker.matches.length > 0) {
@@ -2396,13 +3604,61 @@ function App({
2396
3604
  dispatch({ type: "pickerMove", delta: 1 });
2397
3605
  return;
2398
3606
  }
2399
- if (key.return) {
2400
- await acceptPickerSelection();
3607
+ if (isEnter) {
3608
+ inputGateRef.current = true;
3609
+ try {
3610
+ await acceptPickerSelection();
3611
+ } finally {
3612
+ inputGateRef.current = false;
3613
+ }
2401
3614
  return;
2402
3615
  }
2403
3616
  }
2404
- if (key.return) {
2405
- await submit();
3617
+ if (key.escape && state.status !== "idle" && !state.confirm) {
3618
+ const runningTools = Array.from(state.runningTools.values()).map((t) => t.name);
3619
+ const subagents = Object.values(state.fleet).filter((e) => e.status === "running").map((e) => ({
3620
+ label: e.name,
3621
+ status: e.status,
3622
+ tool: e.currentTool?.name
3623
+ }));
3624
+ const subagentsTerminated = subagents.length;
3625
+ const partialAssistantText = streamingTextRef.current.slice(-1500);
3626
+ activeCtrlRef.current?.abort();
3627
+ dispatch({ type: "status", status: "aborting" });
3628
+ dispatch({
3629
+ type: "steerStart",
3630
+ snapshot: {
3631
+ runningTools,
3632
+ subagents,
3633
+ subagentsTerminated,
3634
+ partialAssistantText
3635
+ }
3636
+ });
3637
+ if (director && subagentsTerminated > 0) {
3638
+ const cap = new Promise((resolve) => {
3639
+ const t = setTimeout(resolve, 1500);
3640
+ t.unref?.();
3641
+ });
3642
+ void Promise.race([director.terminateAll().catch(() => void 0), cap]);
3643
+ }
3644
+ const droppedCount = state.queue.length;
3645
+ if (droppedCount > 0) dispatch({ type: "queueClear" });
3646
+ const droppedTag = droppedCount > 0 ? ` \xB7 dropped ${droppedCount} queued` : "";
3647
+ const fleetTag = subagentsTerminated > 0 ? ` \xB7 stopped ${subagentsTerminated} subagent${subagentsTerminated === 1 ? "" : "s"}` : "";
3648
+ dispatch({
3649
+ type: "addEntry",
3650
+ entry: {
3651
+ kind: "warn",
3652
+ text: `\u21AF Interrupted${droppedTag}${fleetTag}. Type your new direction.`
3653
+ }
3654
+ });
3655
+ return;
3656
+ }
3657
+ if (isEnter) {
3658
+ const now = Date.now();
3659
+ if (now - lastEnterAtRef.current < 50) return;
3660
+ lastEnterAtRef.current = now;
3661
+ void submit();
2406
3662
  return;
2407
3663
  }
2408
3664
  if (key.backspace || key.delete) {
@@ -2439,7 +3695,8 @@ function App({
2439
3695
  dispatch({ type: "setBuffer", buffer, cursor: target });
2440
3696
  return;
2441
3697
  }
2442
- if (state.cursor > 0) dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor - 1 });
3698
+ if (state.cursor > 0)
3699
+ dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor - 1 });
2443
3700
  return;
2444
3701
  }
2445
3702
  if (key.rightArrow) {
@@ -2452,7 +3709,16 @@ function App({
2452
3709
  dispatch({ type: "setBuffer", buffer, cursor: target });
2453
3710
  return;
2454
3711
  }
2455
- if (state.cursor < state.buffer.length) dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor + 1 });
3712
+ if (state.cursor < state.buffer.length)
3713
+ dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.cursor + 1 });
3714
+ return;
3715
+ }
3716
+ if (key.home) {
3717
+ dispatch({ type: "setBuffer", buffer: state.buffer, cursor: 0 });
3718
+ return;
3719
+ }
3720
+ if (key.end) {
3721
+ dispatch({ type: "setBuffer", buffer: state.buffer, cursor: state.buffer.length });
2456
3722
  return;
2457
3723
  }
2458
3724
  if (key.upArrow) {
@@ -2520,10 +3786,9 @@ function App({
2520
3786
  const before = tokenCounter?.total();
2521
3787
  const costBefore = tokenCounter?.estimateCost().total ?? 0;
2522
3788
  const result = await agent.run(blocks, { signal: ctrl.signal });
2523
- const streamed = streamingTextRef.current;
2524
- const text = result.status === "done" && result.finalText ? result.finalText : streamed;
2525
- if (text?.trim()) {
2526
- dispatch({ type: "addEntry", entry: { kind: "assistant", text } });
3789
+ const lingering = streamingTextRef.current;
3790
+ if (lingering.trim()) {
3791
+ dispatch({ type: "addEntry", entry: { kind: "assistant", text: lingering } });
2527
3792
  }
2528
3793
  streamingTextRef.current = "";
2529
3794
  pendingDeltaRef.current = "";
@@ -2536,10 +3801,10 @@ function App({
2536
3801
  dispatch({ type: "addEntry", entry: { kind: "warn", text: "Aborted." } });
2537
3802
  } else if (result.status === "failed") {
2538
3803
  const err = result.error;
2539
- const text2 = err ? `Failed [${err.severity}${err.recoverable ? ", recoverable" : ""}]: ${err.describe()}` : "Failed.";
3804
+ const text = err ? `Failed [${err.severity}${err.recoverable ? ", recoverable" : ""}]: ${err.describe()}` : "Failed.";
2540
3805
  dispatch({
2541
3806
  type: "addEntry",
2542
- entry: { kind: "error", text: text2 }
3807
+ entry: { kind: "error", text }
2543
3808
  });
2544
3809
  } else if (result.status === "max_iterations") {
2545
3810
  dispatch({
@@ -2554,7 +3819,7 @@ function App({
2554
3819
  type: "addEntry",
2555
3820
  entry: {
2556
3821
  kind: "turn-summary",
2557
- text: `[in: ${fmtTok2(after.input - before.input)} out: ${fmtTok2(after.output - before.output)} iters: ${result.iterations} cost: ${(costAfter - costBefore).toFixed(4)} ${((Date.now() - startedAt) / 1e3).toFixed(1)}s]`
3822
+ text: `[in: ${fmtTok3(after.input - before.input)} out: ${fmtTok3(after.output - before.output)} iters: ${result.iterations} cost: ${(costAfter - costBefore).toFixed(4)} ${((Date.now() - startedAt) / 1e3).toFixed(1)}s]`
2558
3823
  }
2559
3824
  });
2560
3825
  }
@@ -2595,6 +3860,18 @@ function App({
2595
3860
  exit();
2596
3861
  onExit(0);
2597
3862
  }
3863
+ if (res?.runText) {
3864
+ const b = builderRef.current;
3865
+ if (b) {
3866
+ b.appendText(res.runText);
3867
+ const blocks2 = await b.submit();
3868
+ const start = Date.now();
3869
+ while (stateRef.current.status !== "idle" && Date.now() - start < 1500) {
3870
+ await new Promise((r) => setTimeout(r, 25));
3871
+ }
3872
+ await runBlocks(blocks2);
3873
+ }
3874
+ }
2598
3875
  const cmd = trimmed.slice(1).split(/\s+/, 1)[0];
2599
3876
  if (cmd === "clear") {
2600
3877
  onClearHistory?.(dispatch);
@@ -2609,9 +3886,14 @@ function App({
2609
3886
  }
2610
3887
  const builder = builderRef.current;
2611
3888
  if (!builder) return;
2612
- if (trimmed) builder.appendText(trimmed);
3889
+ const steering = state.steeringPending;
3890
+ if (trimmed) {
3891
+ const toAppend = steering ? buildSteeringPreamble(state.steerSnapshot, trimmed) : trimmed;
3892
+ builder.appendText(toAppend);
3893
+ }
3894
+ if (steering) dispatch({ type: "steerConsume" });
2613
3895
  const blocks = await builder.submit();
2614
- const displayText = trimmed || "(attachments only)";
3896
+ const displayText = trimmed ? steering ? `\u21AF ${trimmed}` : trimmed : "(attachments only)";
2615
3897
  dispatch({ type: "clearInput" });
2616
3898
  if (state.status !== "idle") {
2617
3899
  dispatch({
@@ -2626,6 +3908,36 @@ function App({
2626
3908
  if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
2627
3909
  await runBlocks(blocks);
2628
3910
  };
3911
+ const bootInjectedRef = useRef(false);
3912
+ useEffect(() => {
3913
+ if (bootInjectedRef.current) return;
3914
+ bootInjectedRef.current = true;
3915
+ const goal = initialGoal?.trim();
3916
+ const ask = initialAsk?.trim();
3917
+ if (!goal && !ask) return;
3918
+ void (async () => {
3919
+ await new Promise((r) => setTimeout(r, 50));
3920
+ const b = builderRef.current;
3921
+ if (!b) return;
3922
+ if (goal) {
3923
+ const shortGoal = goal.length > 80 ? `${goal.slice(0, 80)}\u2026` : goal;
3924
+ dispatch({
3925
+ type: "addEntry",
3926
+ entry: {
3927
+ kind: "info",
3928
+ text: `\u{1F3AF} Goal locked: ${shortGoal}
3929
+ Agent will work until verifiably complete. Esc / /steer to redirect, Ctrl+C to stop.`
3930
+ }
3931
+ });
3932
+ b.appendText(buildGoalPreamble(goal));
3933
+ } else if (ask) {
3934
+ dispatch({ type: "addEntry", entry: { kind: "user", text: ask } });
3935
+ b.appendText(ask);
3936
+ }
3937
+ const blocks = await b.submit();
3938
+ await runBlocks(blocks);
3939
+ })();
3940
+ }, []);
2629
3941
  const inputHint = useMemo(() => {
2630
3942
  if (state.status !== "idle") return "";
2631
3943
  if (state.buffer.startsWith("/")) return "slash command \u2014 Enter to dispatch";
@@ -2633,7 +3945,15 @@ function App({
2633
3945
  return "";
2634
3946
  }, [state.buffer, state.status, state.picker.open]);
2635
3947
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2636
- /* @__PURE__ */ jsx(History, { entries: state.entries, streamingText: state.streamingText, toolStream: state.toolStream }),
3948
+ /* @__PURE__ */ jsx(
3949
+ History,
3950
+ {
3951
+ entries: state.entries,
3952
+ streamingText: state.streamingText,
3953
+ toolStream: state.toolStream
3954
+ }
3955
+ ),
3956
+ /* @__PURE__ */ jsx(LiveActivityStrip, { entries: state.fleet, nowTick }),
2637
3957
  /* @__PURE__ */ jsx(
2638
3958
  Input,
2639
3959
  {
@@ -2692,11 +4012,16 @@ function App({
2692
4012
  yolo,
2693
4013
  elapsedMs,
2694
4014
  todos,
4015
+ plan: planCounts ?? void 0,
4016
+ fleet: fleetCounts,
4017
+ fleetAgents,
2695
4018
  git: gitInfo,
2696
4019
  context: contextWindow,
2697
- projectName
4020
+ projectName,
4021
+ subagentCount: Object.keys(state.fleet).length
2698
4022
  }
2699
- )
4023
+ ),
4024
+ director ? /* @__PURE__ */ jsx(FleetPanel, { entries: state.fleet, totalCost: state.fleetCost, roster: fleetRoster }) : null
2700
4025
  ] });
2701
4026
  }
2702
4027
  function renderRunningTools(running) {
@@ -2725,7 +4050,7 @@ function detectAtToken(buffer, cursor) {
2725
4050
  }
2726
4051
  return null;
2727
4052
  }
2728
- function fmtTok2(n) {
4053
+ function fmtTok3(n) {
2729
4054
  if (n < 1e3) return String(n);
2730
4055
  if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
2731
4056
  return `${(n / 1e6).toFixed(1)}M`;
@@ -2752,6 +4077,15 @@ async function runTui(opts) {
2752
4077
  stdout.write(CURSOR_HOME);
2753
4078
  }
2754
4079
  stdout.write(BRACKETED_PASTE_ON);
4080
+ const swallowSignals = ["SIGTSTP", "SIGQUIT", "SIGTTIN", "SIGTTOU"];
4081
+ const swallow = () => {
4082
+ };
4083
+ for (const s of swallowSignals) {
4084
+ try {
4085
+ process.on(s, swallow);
4086
+ } catch {
4087
+ }
4088
+ }
2755
4089
  let cleaned = false;
2756
4090
  const cleanup = () => {
2757
4091
  if (cleaned) return;
@@ -2771,6 +4105,12 @@ async function runTui(opts) {
2771
4105
  process.on("exit", exitHandler);
2772
4106
  const detachListeners = () => {
2773
4107
  for (const s of signals) process.off(s, signalHandler);
4108
+ for (const s of swallowSignals) {
4109
+ try {
4110
+ process.off(s, swallow);
4111
+ } catch {
4112
+ }
4113
+ }
2774
4114
  process.off("exit", exitHandler);
2775
4115
  };
2776
4116
  return new Promise((resolve) => {
@@ -2810,7 +4150,12 @@ async function runTui(opts) {
2810
4150
  switchProviderAndModel: opts.switchProviderAndModel,
2811
4151
  effectiveMaxContext: opts.effectiveMaxContext,
2812
4152
  onExit,
2813
- onClearHistory: opts.onClearHistory ? (dispatch) => opts.onClearHistory(dispatch) : void 0
4153
+ director: opts.director ?? null,
4154
+ fleetRoster: opts.fleetRoster,
4155
+ onClearHistory: opts.onClearHistory ? (dispatch) => opts.onClearHistory(dispatch) : void 0,
4156
+ fleetStreamController: opts.fleetStreamController,
4157
+ initialGoal: opts.initialGoal,
4158
+ initialAsk: opts.initialAsk
2814
4159
  }),
2815
4160
  { exitOnCtrlC: false }
2816
4161
  );
@@ -2822,7 +4167,24 @@ async function runTui(opts) {
2822
4167
  settle(1);
2823
4168
  return;
2824
4169
  }
2825
- instance.waitUntilExit().then(() => settle(exitCode)).catch(() => settle(1));
4170
+ let detachResize = null;
4171
+ if (!useAltScreen) {
4172
+ const onResize = () => {
4173
+ try {
4174
+ stdout.write("\x1B[J");
4175
+ } catch {
4176
+ }
4177
+ };
4178
+ stdout.on("resize", onResize);
4179
+ detachResize = () => stdout.off("resize", onResize);
4180
+ }
4181
+ instance.waitUntilExit().then(() => {
4182
+ detachResize?.();
4183
+ settle(exitCode);
4184
+ }).catch(() => {
4185
+ detachResize?.();
4186
+ settle(1);
4187
+ });
2826
4188
  });
2827
4189
  }
2828
4190