@wrongstack/tui 0.2.0 → 0.3.2
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.d.ts +4 -0
- package/dist/index.js +235 -194
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,117 +1,14 @@
|
|
|
1
1
|
import { render, useApp, Box, useStdout, Static, Text, useInput, useStdin } from 'ink';
|
|
2
2
|
import React, { useState, useReducer, useRef, useEffect, useMemo } from 'react';
|
|
3
|
-
import * as
|
|
4
|
-
import * as
|
|
3
|
+
import * as fs2 from 'fs/promises';
|
|
4
|
+
import * as path2 from 'path';
|
|
5
5
|
import { InputBuilder, formatTodosList } from '@wrongstack/core';
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
6
|
+
import { routeImagesForModel } from '@wrongstack/runtime/vision';
|
|
7
|
+
import { readClipboardImage } from '@wrongstack/runtime/clipboard';
|
|
8
8
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
9
|
+
import { spawn } from 'child_process';
|
|
9
10
|
|
|
10
11
|
// 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
12
|
function stringifyInput(input) {
|
|
116
13
|
if (!input || typeof input !== "object") return "";
|
|
117
14
|
const obj = input;
|
|
@@ -214,8 +111,8 @@ function FilePicker({ query, matches, selected }) {
|
|
|
214
111
|
] }, m))
|
|
215
112
|
] });
|
|
216
113
|
}
|
|
217
|
-
function highlight(
|
|
218
|
-
return
|
|
114
|
+
function highlight(path3, query) {
|
|
115
|
+
return path3;
|
|
219
116
|
}
|
|
220
117
|
var STATUS_ICON = {
|
|
221
118
|
idle: { icon: "\u25CB", color: "gray" },
|
|
@@ -233,6 +130,27 @@ function fmtCount(n) {
|
|
|
233
130
|
if (n === 0) return "\u2014";
|
|
234
131
|
return String(n);
|
|
235
132
|
}
|
|
133
|
+
function fmtDuration(ms) {
|
|
134
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
135
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
136
|
+
}
|
|
137
|
+
function fmtBytes(n) {
|
|
138
|
+
if (n < 1024) return `${n}B`;
|
|
139
|
+
return `${(n / 1024).toFixed(1)}KB`;
|
|
140
|
+
}
|
|
141
|
+
function fmtRecentTool(tool) {
|
|
142
|
+
const status = tool.ok === false ? "fail" : "ok";
|
|
143
|
+
const name = tool.name.length > 24 ? `${tool.name.slice(0, 23)}...` : tool.name;
|
|
144
|
+
const parts = [status, name];
|
|
145
|
+
if (typeof tool.durationMs === "number") parts.push(fmtDuration(tool.durationMs));
|
|
146
|
+
if (typeof tool.outputBytes === "number" && tool.outputBytes > 0) parts.push(fmtBytes(tool.outputBytes));
|
|
147
|
+
if (typeof tool.outputLines === "number" && tool.outputLines > 0) parts.push(`${tool.outputLines}L`);
|
|
148
|
+
return parts.join(" ");
|
|
149
|
+
}
|
|
150
|
+
function fmtRecentMessage(message) {
|
|
151
|
+
const text = message.text.replace(/\s+/g, " ");
|
|
152
|
+
return text.length > 80 ? `${text.slice(0, 79)}...` : text;
|
|
153
|
+
}
|
|
236
154
|
function fmtModel(provider, model) {
|
|
237
155
|
if (!provider && !model) return "";
|
|
238
156
|
const p = provider ?? "";
|
|
@@ -282,6 +200,8 @@ function FleetPanel({ entries, totalCost, roster }) {
|
|
|
282
200
|
const si = STATUS_ICON[entry.status];
|
|
283
201
|
const modelTag = fmtModel(entry.provider, entry.model);
|
|
284
202
|
const name = resolveName(entry, roster);
|
|
203
|
+
const recentTools = (entry.recentTools ?? []).slice(-2).map(fmtRecentTool).join(" | ");
|
|
204
|
+
const recentMessages = (entry.recentMessages ?? []).slice(-2).map(fmtRecentMessage);
|
|
285
205
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
286
206
|
/* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
|
|
287
207
|
/* @__PURE__ */ jsx(Text, { color: si.color, children: si.icon }),
|
|
@@ -313,6 +233,14 @@ function FleetPanel({ entries, totalCost, roster }) {
|
|
|
313
233
|
"ms)"
|
|
314
234
|
] })
|
|
315
235
|
] }) : null,
|
|
236
|
+
recentTools ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
237
|
+
"tools: ",
|
|
238
|
+
recentTools
|
|
239
|
+
] }) }) : null,
|
|
240
|
+
recentMessages.map((message, index) => /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
241
|
+
"msg: ",
|
|
242
|
+
message
|
|
243
|
+
] }) }, `${entry.id}-msg-${index}-${message}`)),
|
|
316
244
|
entry.status === "running" && entry.streamingText ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
317
245
|
">",
|
|
318
246
|
" ",
|
|
@@ -335,6 +263,23 @@ function fmtElapsed(ms) {
|
|
|
335
263
|
const s = Math.floor(ms % 6e4 / 1e3);
|
|
336
264
|
return `${m}m${s.toString().padStart(2, "0")}s`;
|
|
337
265
|
}
|
|
266
|
+
function fmtBytes2(n) {
|
|
267
|
+
if (n < 1024) return `${n}B`;
|
|
268
|
+
return `${(n / 1024).toFixed(1)}KB`;
|
|
269
|
+
}
|
|
270
|
+
function fmtRecentTool2(tool) {
|
|
271
|
+
const status = tool.ok === false ? "fail" : "ok";
|
|
272
|
+
const name = tool.name.length > 18 ? `${tool.name.slice(0, 17)}...` : tool.name;
|
|
273
|
+
const parts = [status, name];
|
|
274
|
+
if (typeof tool.durationMs === "number") parts.push(fmtElapsed(tool.durationMs));
|
|
275
|
+
if (typeof tool.outputBytes === "number" && tool.outputBytes > 0) parts.push(fmtBytes2(tool.outputBytes));
|
|
276
|
+
if (typeof tool.outputLines === "number" && tool.outputLines > 0) parts.push(`${tool.outputLines}L`);
|
|
277
|
+
return parts.join(" ");
|
|
278
|
+
}
|
|
279
|
+
function fmtRecentMessage2(message) {
|
|
280
|
+
const text = message.text.replace(/\s+/g, " ");
|
|
281
|
+
return text.length > 48 ? `${text.slice(0, 47)}...` : text;
|
|
282
|
+
}
|
|
338
283
|
function LiveActivityStrip({
|
|
339
284
|
entries,
|
|
340
285
|
nowTick,
|
|
@@ -348,6 +293,8 @@ function LiveActivityStrip({
|
|
|
348
293
|
const toolElapsed = e.currentTool ? now - e.currentTool.startedAt : 0;
|
|
349
294
|
const taskElapsed = now - e.startedAt;
|
|
350
295
|
const toolSeg = e.currentTool ? `\u2192 ${e.currentTool.name} (${fmtElapsed(toolElapsed)})` : "idle between tools";
|
|
296
|
+
const recentTools = (e.recentTools ?? []).slice(-2).map(fmtRecentTool2).join(" | ");
|
|
297
|
+
const messageText = e.streamingText.trim() || (e.recentMessages ?? []).slice(-1).map(fmtRecentMessage2).join("");
|
|
351
298
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
|
|
352
299
|
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u25CF" }),
|
|
353
300
|
/* @__PURE__ */ jsx(Text, { children: e.name.slice(0, 14).padEnd(14) }),
|
|
@@ -360,7 +307,21 @@ function LiveActivityStrip({
|
|
|
360
307
|
e.toolCalls,
|
|
361
308
|
"tc \xB7 ",
|
|
362
309
|
fmtElapsed(taskElapsed)
|
|
363
|
-
] })
|
|
310
|
+
] }),
|
|
311
|
+
recentTools ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
312
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "|" }),
|
|
313
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
314
|
+
"last: ",
|
|
315
|
+
recentTools
|
|
316
|
+
] })
|
|
317
|
+
] }) : null,
|
|
318
|
+
messageText ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
319
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "|" }),
|
|
320
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
321
|
+
"msg: ",
|
|
322
|
+
fmtRecentMessage2({ text: messageText})
|
|
323
|
+
] })
|
|
324
|
+
] }) : null
|
|
364
325
|
] }, e.id);
|
|
365
326
|
}),
|
|
366
327
|
Object.values(entries).filter((e) => e.status === "running").length > maxRows ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
@@ -594,7 +555,7 @@ function ToolStreamBox({
|
|
|
594
555
|
/* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
|
|
595
556
|
/* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u25C6 " }),
|
|
596
557
|
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: name }),
|
|
597
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u23F1 ${
|
|
558
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u23F1 ${fmtDuration2(elapsedMs)}` }),
|
|
598
559
|
hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` (${totalLines} lines, showing last ${MAX_STREAM_LINES})` }) : null
|
|
599
560
|
] }),
|
|
600
561
|
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [
|
|
@@ -617,6 +578,15 @@ function tailForDisplay(text, maxChars) {
|
|
|
617
578
|
return `\u2026 ${text.slice(cut)}`;
|
|
618
579
|
}
|
|
619
580
|
function DiffBlock({ rows, hidden }) {
|
|
581
|
+
let gutterWidth = 1;
|
|
582
|
+
for (const r of rows) {
|
|
583
|
+
const n = r.kind === "del" ? r.oldLine : r.newLine;
|
|
584
|
+
if (typeof n === "number") {
|
|
585
|
+
const w = String(n).length;
|
|
586
|
+
if (w > gutterWidth) gutterWidth = w;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const blank = " ".repeat(gutterWidth);
|
|
620
590
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 4, marginTop: 0, children: [
|
|
621
591
|
rows.map((row, i) => {
|
|
622
592
|
const key = i;
|
|
@@ -624,16 +594,20 @@ function DiffBlock({ rows, hidden }) {
|
|
|
624
594
|
return /* @__PURE__ */ jsx(Text, { color: "cyan", dimColor: true, children: row.text }, key);
|
|
625
595
|
}
|
|
626
596
|
if (row.kind === "meta") {
|
|
627
|
-
return /* @__PURE__ */ jsx(Text, { dimColor: true, children: row.text }, key);
|
|
597
|
+
return /* @__PURE__ */ jsx(Text, { dimColor: true, children: `${blank} ${row.text}` }, key);
|
|
628
598
|
}
|
|
599
|
+
const lnNumber = row.kind === "del" ? row.oldLine : row.newLine;
|
|
600
|
+
const lnText = typeof lnNumber === "number" ? String(lnNumber).padStart(gutterWidth, " ") : blank;
|
|
629
601
|
if (row.kind === "ctx") {
|
|
630
|
-
return /* @__PURE__ */ jsx(Text, { dimColor: true, children: row.text }, key);
|
|
602
|
+
return /* @__PURE__ */ jsx(Text, { dimColor: true, children: `${lnText} ${row.text}` }, key);
|
|
631
603
|
}
|
|
632
|
-
const bg = row.kind === "add" ? "
|
|
633
|
-
|
|
634
|
-
|
|
604
|
+
const bg = row.kind === "add" ? "greenBright" : "redBright";
|
|
605
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
606
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: `${lnText} ` }),
|
|
607
|
+
/* @__PURE__ */ jsx(Text, { backgroundColor: bg, color: "black", children: row.text })
|
|
608
|
+
] }, key);
|
|
635
609
|
}),
|
|
636
|
-
hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children:
|
|
610
|
+
hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: `${blank} \u2026 ${hidden} more line${hidden === 1 ? "" : "s"}` }) : null
|
|
637
611
|
] });
|
|
638
612
|
}
|
|
639
613
|
function Entry({
|
|
@@ -667,7 +641,7 @@ function Entry({
|
|
|
667
641
|
parts.push(`${entry.outputLines} L`);
|
|
668
642
|
}
|
|
669
643
|
if (entry.outputBytes && entry.outputBytes > 0) {
|
|
670
|
-
parts.push(
|
|
644
|
+
parts.push(fmtBytes3(entry.outputBytes));
|
|
671
645
|
}
|
|
672
646
|
if (entry.outputTokens && entry.outputTokens > 0) {
|
|
673
647
|
parts.push(`\u2248${fmtTok(entry.outputTokens)} tok`);
|
|
@@ -683,7 +657,7 @@ function Entry({
|
|
|
683
657
|
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
684
658
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: argSummary })
|
|
685
659
|
] }) : null,
|
|
686
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${
|
|
660
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${fmtDuration2(entry.durationMs)}` }),
|
|
687
661
|
sizeChip ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \xB7 ${sizeChip}` }) : null
|
|
688
662
|
] }),
|
|
689
663
|
outLines.map((line, i) => (
|
|
@@ -823,7 +797,7 @@ function fmtTok(n) {
|
|
|
823
797
|
if (n >= 1e3) return `${(n / 1e3).toFixed(n >= 1e4 ? 0 : 1)}k`;
|
|
824
798
|
return String(n);
|
|
825
799
|
}
|
|
826
|
-
function
|
|
800
|
+
function fmtDuration2(ms) {
|
|
827
801
|
if (ms < 1e3) return `${ms}ms`;
|
|
828
802
|
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
829
803
|
const totalSec = Math.floor(ms / 1e3);
|
|
@@ -951,7 +925,7 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
|
|
|
951
925
|
const bytes = numOf(o["bytes_written"]) ?? numOf(o["bytes"]);
|
|
952
926
|
const created = o["created"] === true;
|
|
953
927
|
const tag = created ? "created" : "updated";
|
|
954
|
-
if (bytes !== void 0) return [`${tag} \xB7 ${
|
|
928
|
+
if (bytes !== void 0) return [`${tag} \xB7 ${fmtBytes3(bytes)}`];
|
|
955
929
|
return [tag];
|
|
956
930
|
}
|
|
957
931
|
}
|
|
@@ -1016,17 +990,17 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
|
|
|
1016
990
|
if (json && typeof json === "object") {
|
|
1017
991
|
const o = json;
|
|
1018
992
|
const bytes = numOf(o["bytes"]);
|
|
1019
|
-
if (bytes !== void 0) return [`${
|
|
993
|
+
if (bytes !== void 0) return [`${fmtBytes3(bytes)} read`];
|
|
1020
994
|
}
|
|
1021
995
|
const range = scanNumberedRange(text);
|
|
1022
996
|
if (range.count > 0 && range.first !== void 0 && range.last !== void 0) {
|
|
1023
997
|
if (range.first === range.last) {
|
|
1024
|
-
return [`L${range.first} \xB7 ${
|
|
998
|
+
return [`L${range.first} \xB7 ${fmtBytes3(text.length)}`];
|
|
1025
999
|
}
|
|
1026
1000
|
const contiguous = range.count === range.last - range.first + 1;
|
|
1027
1001
|
const head = `L${range.first}\u2013${range.last}`;
|
|
1028
1002
|
const tail = contiguous ? `${range.count} line${range.count === 1 ? "" : "s"}` : `${range.count} lines (gaps)`;
|
|
1029
|
-
return [`${head} \xB7 ${tail} \xB7 ${
|
|
1003
|
+
return [`${head} \xB7 ${tail} \xB7 ${fmtBytes3(text.length)}`];
|
|
1030
1004
|
}
|
|
1031
1005
|
}
|
|
1032
1006
|
if (toolName === "grep" || toolName === "glob") {
|
|
@@ -1084,7 +1058,7 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
|
|
|
1084
1058
|
const head = [];
|
|
1085
1059
|
if (status !== void 0) head.push(`HTTP ${status}`);
|
|
1086
1060
|
if (ct) head.push(ct.split(";")[0] ?? ct);
|
|
1087
|
-
if (content) head.push(
|
|
1061
|
+
if (content) head.push(fmtBytes3(Buffer.byteLength(content, "utf8")));
|
|
1088
1062
|
const lines = [];
|
|
1089
1063
|
if (head.length > 0) lines.push(head.join(" \xB7 "));
|
|
1090
1064
|
if (url && status !== void 0 && (status < 200 || status >= 400)) {
|
|
@@ -1175,7 +1149,7 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
|
|
|
1175
1149
|
if (runner && runner !== "none") head.push(runner);
|
|
1176
1150
|
head.push(`${passed}/${total} passed`);
|
|
1177
1151
|
if (failed > 0) head.push(`${failed} failed`);
|
|
1178
|
-
if (duration !== void 0) head.push(
|
|
1152
|
+
if (duration !== void 0) head.push(fmtDuration2(duration));
|
|
1179
1153
|
return [head.join(" \xB7 ")];
|
|
1180
1154
|
}
|
|
1181
1155
|
}
|
|
@@ -1386,20 +1360,29 @@ function extractDiffPreview(toolName, output) {
|
|
|
1386
1360
|
}
|
|
1387
1361
|
function parseUnifiedDiff(diff, maxLines) {
|
|
1388
1362
|
const all = [];
|
|
1363
|
+
let oldLn = 0;
|
|
1364
|
+
let newLn = 0;
|
|
1389
1365
|
for (const raw of diff.split("\n")) {
|
|
1390
1366
|
const line = raw.replace(/\r$/, "");
|
|
1391
1367
|
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
1392
1368
|
if (line.startsWith("diff --git") || line.startsWith("index ")) continue;
|
|
1393
1369
|
if (line.startsWith("@@")) {
|
|
1370
|
+
const m = line.match(/^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
|
|
1371
|
+
if (m) {
|
|
1372
|
+
oldLn = Number.parseInt(m[1] ?? "0", 10) || 0;
|
|
1373
|
+
newLn = Number.parseInt(m[2] ?? "0", 10) || 0;
|
|
1374
|
+
}
|
|
1394
1375
|
all.push({ kind: "hunk", text: truncMid(line, 60) });
|
|
1395
1376
|
continue;
|
|
1396
1377
|
}
|
|
1397
1378
|
if (line.startsWith("+")) {
|
|
1398
|
-
all.push({ kind: "add", text: truncMid(line, 100) });
|
|
1379
|
+
all.push({ kind: "add", text: truncMid(line, 100), newLine: newLn });
|
|
1380
|
+
newLn++;
|
|
1399
1381
|
continue;
|
|
1400
1382
|
}
|
|
1401
1383
|
if (line.startsWith("-")) {
|
|
1402
|
-
all.push({ kind: "del", text: truncMid(line, 100) });
|
|
1384
|
+
all.push({ kind: "del", text: truncMid(line, 100), oldLine: oldLn });
|
|
1385
|
+
oldLn++;
|
|
1403
1386
|
continue;
|
|
1404
1387
|
}
|
|
1405
1388
|
if (line.startsWith("\\ No newline")) {
|
|
@@ -1407,7 +1390,9 @@ function parseUnifiedDiff(diff, maxLines) {
|
|
|
1407
1390
|
continue;
|
|
1408
1391
|
}
|
|
1409
1392
|
if (line.length === 0) continue;
|
|
1410
|
-
all.push({ kind: "ctx", text: truncMid(line, 100) });
|
|
1393
|
+
all.push({ kind: "ctx", text: truncMid(line, 100), oldLine: oldLn, newLine: newLn });
|
|
1394
|
+
oldLn++;
|
|
1395
|
+
newLn++;
|
|
1411
1396
|
}
|
|
1412
1397
|
if (all.length === 0) return { rows: [], hidden: 0 };
|
|
1413
1398
|
if (all.length <= maxLines) return { rows: all, hidden: 0 };
|
|
@@ -1449,7 +1434,7 @@ function countLines(text) {
|
|
|
1449
1434
|
if (!text) return 0;
|
|
1450
1435
|
return text.replace(/\n$/, "").split("\n").length;
|
|
1451
1436
|
}
|
|
1452
|
-
function
|
|
1437
|
+
function fmtBytes3(n) {
|
|
1453
1438
|
if (n < 1024) return `${n}B`;
|
|
1454
1439
|
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
|
|
1455
1440
|
return `${(n / (1024 * 1024)).toFixed(1)}MB`;
|
|
@@ -1886,10 +1871,10 @@ async function loadIndex(root) {
|
|
|
1886
1871
|
async function walk(root, rel, depth, out) {
|
|
1887
1872
|
if (out.length >= MAX_FILES_INDEXED) return;
|
|
1888
1873
|
if (depth > MAX_DEPTH) return;
|
|
1889
|
-
const dir = rel ?
|
|
1874
|
+
const dir = rel ? path2.join(root, rel) : root;
|
|
1890
1875
|
let entries;
|
|
1891
1876
|
try {
|
|
1892
|
-
entries = await
|
|
1877
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
1893
1878
|
} catch {
|
|
1894
1879
|
return;
|
|
1895
1880
|
}
|
|
@@ -2308,7 +2293,13 @@ function reducer(state, action) {
|
|
|
2308
2293
|
// --- Fleet ---
|
|
2309
2294
|
case "fleetSeed": {
|
|
2310
2295
|
const seeded = {};
|
|
2311
|
-
for (const e of action.entries)
|
|
2296
|
+
for (const e of action.entries) {
|
|
2297
|
+
seeded[e.id] = {
|
|
2298
|
+
...e,
|
|
2299
|
+
recentTools: e.recentTools ?? [],
|
|
2300
|
+
recentMessages: e.recentMessages ?? []
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2312
2303
|
return { ...state, fleet: seeded, fleetCost: action.cost };
|
|
2313
2304
|
}
|
|
2314
2305
|
case "fleetSpawn": {
|
|
@@ -2322,6 +2313,8 @@ function reducer(state, action) {
|
|
|
2322
2313
|
streamingText: "",
|
|
2323
2314
|
iterations: 0,
|
|
2324
2315
|
toolCalls: 0,
|
|
2316
|
+
recentTools: [],
|
|
2317
|
+
recentMessages: [],
|
|
2325
2318
|
cost: 0,
|
|
2326
2319
|
startedAt: Date.now(),
|
|
2327
2320
|
lastEventAt: Date.now(),
|
|
@@ -2383,14 +2376,45 @@ function reducer(state, action) {
|
|
|
2383
2376
|
}
|
|
2384
2377
|
};
|
|
2385
2378
|
}
|
|
2379
|
+
case "fleetMessage": {
|
|
2380
|
+
const cur = state.fleet[action.id];
|
|
2381
|
+
const text = action.text.trim().replace(/\s+/g, " ");
|
|
2382
|
+
if (!cur || !text) return state;
|
|
2383
|
+
const now = Date.now();
|
|
2384
|
+
const recentMessages = [...cur.recentMessages ?? [], { text, at: now }].slice(-2);
|
|
2385
|
+
return {
|
|
2386
|
+
...state,
|
|
2387
|
+
fleet: {
|
|
2388
|
+
...state.fleet,
|
|
2389
|
+
[action.id]: { ...cur, recentMessages, lastEventAt: now }
|
|
2390
|
+
}
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2386
2393
|
case "fleetTool": {
|
|
2387
2394
|
const cur = state.fleet[action.id];
|
|
2388
2395
|
if (!cur) return state;
|
|
2396
|
+
const now = Date.now();
|
|
2397
|
+
const recentTools = action.name !== void 0 ? [
|
|
2398
|
+
...cur.recentTools ?? [],
|
|
2399
|
+
{
|
|
2400
|
+
name: action.name,
|
|
2401
|
+
ok: action.ok,
|
|
2402
|
+
durationMs: action.durationMs,
|
|
2403
|
+
outputBytes: action.outputBytes,
|
|
2404
|
+
outputLines: action.outputLines,
|
|
2405
|
+
at: now
|
|
2406
|
+
}
|
|
2407
|
+
].slice(-2) : cur.recentTools ?? [];
|
|
2389
2408
|
return {
|
|
2390
2409
|
...state,
|
|
2391
2410
|
fleet: {
|
|
2392
2411
|
...state.fleet,
|
|
2393
|
-
[action.id]: {
|
|
2412
|
+
[action.id]: {
|
|
2413
|
+
...cur,
|
|
2414
|
+
toolCalls: cur.toolCalls + 1,
|
|
2415
|
+
recentTools,
|
|
2416
|
+
lastEventAt: now
|
|
2417
|
+
}
|
|
2394
2418
|
}
|
|
2395
2419
|
};
|
|
2396
2420
|
}
|
|
@@ -2416,6 +2440,7 @@ function reducer(state, action) {
|
|
|
2416
2440
|
iterations: action.iterations,
|
|
2417
2441
|
toolCalls: action.toolCalls,
|
|
2418
2442
|
streamingText: "",
|
|
2443
|
+
currentTool: void 0,
|
|
2419
2444
|
lastEventAt: Date.now()
|
|
2420
2445
|
}
|
|
2421
2446
|
}
|
|
@@ -2539,6 +2564,8 @@ function App({
|
|
|
2539
2564
|
attachments,
|
|
2540
2565
|
events,
|
|
2541
2566
|
tokenCounter,
|
|
2567
|
+
visionAdapters = [],
|
|
2568
|
+
supportsVision,
|
|
2542
2569
|
model,
|
|
2543
2570
|
banner = true,
|
|
2544
2571
|
queueStore,
|
|
@@ -2614,8 +2641,8 @@ function App({
|
|
|
2614
2641
|
const lastEnterAtRef = useRef(0);
|
|
2615
2642
|
const projectRoot = agent.ctx.projectRoot;
|
|
2616
2643
|
const projectName = React.useMemo(() => {
|
|
2617
|
-
const base =
|
|
2618
|
-
return base && base !==
|
|
2644
|
+
const base = path2.basename(projectRoot);
|
|
2645
|
+
return base && base !== path2.sep ? base : void 0;
|
|
2619
2646
|
}, [projectRoot]);
|
|
2620
2647
|
const streamingTextRef = useRef("");
|
|
2621
2648
|
const pendingDeltaRef = useRef("");
|
|
@@ -2725,7 +2752,7 @@ function App({
|
|
|
2725
2752
|
let cancelled = false;
|
|
2726
2753
|
const poll = async () => {
|
|
2727
2754
|
try {
|
|
2728
|
-
const data = await
|
|
2755
|
+
const data = await fs2.readFile(planPath, "utf8");
|
|
2729
2756
|
const parsed = JSON.parse(data);
|
|
2730
2757
|
if (cancelled) return;
|
|
2731
2758
|
if (!Array.isArray(parsed.items)) {
|
|
@@ -2857,9 +2884,9 @@ function App({
|
|
|
2857
2884
|
dispatch({ type: "pickerClose" });
|
|
2858
2885
|
return;
|
|
2859
2886
|
}
|
|
2860
|
-
const absPath =
|
|
2887
|
+
const absPath = path2.isAbsolute(picked) ? picked : path2.join(projectRoot, picked);
|
|
2861
2888
|
try {
|
|
2862
|
-
const data = await
|
|
2889
|
+
const data = await fs2.readFile(absPath, "utf8");
|
|
2863
2890
|
const placeholder = await builder.appendFile({
|
|
2864
2891
|
kind: "file",
|
|
2865
2892
|
data,
|
|
@@ -3255,21 +3282,16 @@ function App({
|
|
|
3255
3282
|
});
|
|
3256
3283
|
});
|
|
3257
3284
|
const offTool = events.on("subagent.tool_executed", (e) => {
|
|
3258
|
-
|
|
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`}` : "";
|
|
3285
|
+
if (director) return;
|
|
3262
3286
|
dispatch({
|
|
3263
|
-
type: "
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
text: e.name,
|
|
3270
|
-
detail: `${e.durationMs}ms${bytesTag}`
|
|
3271
|
-
}
|
|
3287
|
+
type: "fleetTool",
|
|
3288
|
+
id: e.subagentId,
|
|
3289
|
+
name: e.name,
|
|
3290
|
+
ok: e.ok,
|
|
3291
|
+
durationMs: e.durationMs,
|
|
3292
|
+
outputBytes: e.outputBytes
|
|
3272
3293
|
});
|
|
3294
|
+
dispatch({ type: "fleetToolEnd", id: e.subagentId });
|
|
3273
3295
|
});
|
|
3274
3296
|
return () => {
|
|
3275
3297
|
offSpawned();
|
|
@@ -3277,7 +3299,7 @@ function App({
|
|
|
3277
3299
|
offCompleted();
|
|
3278
3300
|
offTool();
|
|
3279
3301
|
};
|
|
3280
|
-
}, [events]);
|
|
3302
|
+
}, [events, director]);
|
|
3281
3303
|
useEffect(() => {
|
|
3282
3304
|
if (!fleetStreamController) return;
|
|
3283
3305
|
fleetStreamController.enabled = state.streamFleet;
|
|
@@ -3301,18 +3323,22 @@ function App({
|
|
|
3301
3323
|
let streamFlushTimer = null;
|
|
3302
3324
|
const flushStreamBufs = () => {
|
|
3303
3325
|
for (const [id, text] of streamBuf) {
|
|
3304
|
-
|
|
3326
|
+
const trimmed = text.trim();
|
|
3327
|
+
if (!trimmed) continue;
|
|
3305
3328
|
const lbl = labelFor(id);
|
|
3306
|
-
dispatch({
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3329
|
+
dispatch({ type: "fleetMessage", id, text: trimmed });
|
|
3330
|
+
if (streamFleetRef.current) {
|
|
3331
|
+
dispatch({
|
|
3332
|
+
type: "addEntry",
|
|
3333
|
+
entry: {
|
|
3334
|
+
kind: "subagent",
|
|
3335
|
+
agentLabel: lbl.label,
|
|
3336
|
+
agentColor: lbl.color,
|
|
3337
|
+
icon: "\u{1F4AC}",
|
|
3338
|
+
text: trimmed
|
|
3339
|
+
}
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3316
3342
|
}
|
|
3317
3343
|
streamBuf.clear();
|
|
3318
3344
|
streamFlushTimer = null;
|
|
@@ -3377,10 +3403,9 @@ function App({
|
|
|
3377
3403
|
const cur = pending.get(e.subagentId) ?? "";
|
|
3378
3404
|
pending.set(e.subagentId, cur + p.text);
|
|
3379
3405
|
if (!flushTimer) flushTimer = setTimeout(doFlush, FLUSH_MS);
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
}
|
|
3406
|
+
streamBuf.set(e.subagentId, (streamBuf.get(e.subagentId) ?? "") + p.text);
|
|
3407
|
+
if (streamFlushTimer) clearTimeout(streamFlushTimer);
|
|
3408
|
+
streamFlushTimer = setTimeout(flushStreamBufs, FLUSH_MS * 4);
|
|
3384
3409
|
}
|
|
3385
3410
|
break;
|
|
3386
3411
|
}
|
|
@@ -3392,28 +3417,17 @@ function App({
|
|
|
3392
3417
|
break;
|
|
3393
3418
|
}
|
|
3394
3419
|
case "tool.executed": {
|
|
3395
|
-
|
|
3420
|
+
const p = e.payload;
|
|
3421
|
+
dispatch({
|
|
3422
|
+
type: "fleetTool",
|
|
3423
|
+
id: e.subagentId,
|
|
3424
|
+
name: p?.name,
|
|
3425
|
+
ok: p?.ok,
|
|
3426
|
+
durationMs: p?.durationMs,
|
|
3427
|
+
outputBytes: p?.outputBytes,
|
|
3428
|
+
outputLines: p?.outputLines
|
|
3429
|
+
});
|
|
3396
3430
|
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
3431
|
break;
|
|
3418
3432
|
}
|
|
3419
3433
|
case "provider.response": {
|
|
@@ -3431,7 +3445,7 @@ function App({
|
|
|
3431
3445
|
toolCalls: payload.result.toolCalls
|
|
3432
3446
|
});
|
|
3433
3447
|
dispatch({ type: "fleetCost", cost: d.snapshot().total.cost });
|
|
3434
|
-
if (
|
|
3448
|
+
if (streamFlushTimer) {
|
|
3435
3449
|
clearTimeout(streamFlushTimer);
|
|
3436
3450
|
flushStreamBufs();
|
|
3437
3451
|
}
|
|
@@ -3785,7 +3799,24 @@ function App({
|
|
|
3785
3799
|
const startedAt = Date.now();
|
|
3786
3800
|
const before = tokenCounter?.total();
|
|
3787
3801
|
const costBefore = tokenCounter?.estimateCost().total ?? 0;
|
|
3788
|
-
const
|
|
3802
|
+
const routed = blocks.some((block) => block.type === "image") ? await routeImagesForModel(blocks, {
|
|
3803
|
+
supportsVision: supportsVision ? await supportsVision() : agent.ctx.provider.capabilities.vision,
|
|
3804
|
+
adapters: visionAdapters,
|
|
3805
|
+
ctx: agent.ctx,
|
|
3806
|
+
signal: ctrl.signal,
|
|
3807
|
+
providerId: agent.ctx.provider.id,
|
|
3808
|
+
model: agent.ctx.model
|
|
3809
|
+
}) : { blocks, route: "none", convertedImages: 0 };
|
|
3810
|
+
if (routed.route === "adapter") {
|
|
3811
|
+
dispatch({
|
|
3812
|
+
type: "addEntry",
|
|
3813
|
+
entry: {
|
|
3814
|
+
kind: "info",
|
|
3815
|
+
text: `Image input analyzed via ${routed.adapterName ?? "vision adapter"} (${routed.convertedImages} image${routed.convertedImages === 1 ? "" : "s"}).`
|
|
3816
|
+
}
|
|
3817
|
+
});
|
|
3818
|
+
}
|
|
3819
|
+
const result = await agent.run(routed.blocks, { signal: ctrl.signal });
|
|
3789
3820
|
const lingering = streamingTextRef.current;
|
|
3790
3821
|
if (lingering.trim()) {
|
|
3791
3822
|
dispatch({ type: "addEntry", entry: { kind: "assistant", text: lingering } });
|
|
@@ -3838,11 +3869,19 @@ function App({
|
|
|
3838
3869
|
await runBlocks(head.blocks);
|
|
3839
3870
|
}
|
|
3840
3871
|
};
|
|
3872
|
+
const runBlocksRef = useRef(runBlocks);
|
|
3873
|
+
runBlocksRef.current = runBlocks;
|
|
3841
3874
|
const submit = async () => {
|
|
3842
3875
|
const raw = state.buffer;
|
|
3843
3876
|
const trimmed = raw.trim();
|
|
3844
3877
|
if (!trimmed && state.placeholders.length === 0) return;
|
|
3845
3878
|
dispatch({ type: "resetInterrupts" });
|
|
3879
|
+
if (trimmed === "/image" || trimmed === "/paste-image") {
|
|
3880
|
+
dispatch({ type: "clearInput" });
|
|
3881
|
+
await pasteClipboardImage();
|
|
3882
|
+
if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3846
3885
|
if (trimmed.startsWith("/")) {
|
|
3847
3886
|
dispatch({ type: "addEntry", entry: { kind: "user", text: trimmed } });
|
|
3848
3887
|
if (state.historyIndex > 0) dispatch({ type: "historyPush", text: trimmed });
|
|
@@ -3935,9 +3974,9 @@ function App({
|
|
|
3935
3974
|
b.appendText(ask);
|
|
3936
3975
|
}
|
|
3937
3976
|
const blocks = await b.submit();
|
|
3938
|
-
await
|
|
3977
|
+
await runBlocksRef.current(blocks);
|
|
3939
3978
|
})();
|
|
3940
|
-
}, []);
|
|
3979
|
+
}, [initialAsk, initialGoal]);
|
|
3941
3980
|
const inputHint = useMemo(() => {
|
|
3942
3981
|
if (state.status !== "idle") return "";
|
|
3943
3982
|
if (state.buffer.startsWith("/")) return "slash command \u2014 Enter to dispatch";
|
|
@@ -4138,6 +4177,8 @@ async function runTui(opts) {
|
|
|
4138
4177
|
attachments: opts.attachments,
|
|
4139
4178
|
events: opts.events,
|
|
4140
4179
|
tokenCounter: opts.tokenCounter,
|
|
4180
|
+
visionAdapters: opts.visionAdapters,
|
|
4181
|
+
supportsVision: opts.supportsVision,
|
|
4141
4182
|
model: opts.model,
|
|
4142
4183
|
banner: opts.banner ?? true,
|
|
4143
4184
|
queueStore: opts.queueStore,
|