@zhijiewang/openharness 2.25.0 → 2.26.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/Tool.d.ts +1 -0
- package/dist/renderer/json-tree.d.ts +11 -0
- package/dist/renderer/json-tree.js +194 -0
- package/dist/renderer/layout-sections.js +7 -21
- package/dist/renderer/layout.d.ts +1 -0
- package/dist/renderer/output-renderer.d.ts +27 -0
- package/dist/renderer/output-renderer.js +89 -0
- package/dist/repl.js +6 -1
- package/dist/tools/FileReadTool/index.js +12 -3
- package/dist/tools/WebFetchTool/index.js +5 -2
- package/dist/types/events.d.ts +1 -0
- package/package.json +1 -1
package/dist/Tool.d.ts
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static JSON tree renderer for tool output.
|
|
3
|
+
* Theme-colored, indented, depth-truncated, line-truncated.
|
|
4
|
+
*/
|
|
5
|
+
import type { CellGrid } from "./cells.js";
|
|
6
|
+
export declare function resetJsonStyleCache(): void;
|
|
7
|
+
export declare function renderJsonTree(grid: CellGrid, row: number, col: number, value: unknown, width: number, opts: {
|
|
8
|
+
maxLines: number;
|
|
9
|
+
limit: number;
|
|
10
|
+
}): number;
|
|
11
|
+
//# sourceMappingURL=json-tree.d.ts.map
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static JSON tree renderer for tool output.
|
|
3
|
+
* Theme-colored, indented, depth-truncated, line-truncated.
|
|
4
|
+
*/
|
|
5
|
+
import { getTheme } from "../utils/theme-data.js";
|
|
6
|
+
const s = (fg, bold = false, dim = false) => ({ fg, bg: null, bold, dim, underline: false });
|
|
7
|
+
const MAX_DEPTH = 3;
|
|
8
|
+
let S_KEY;
|
|
9
|
+
let S_STRING;
|
|
10
|
+
let S_NUMBER;
|
|
11
|
+
let S_PUNCT;
|
|
12
|
+
let S_TRUNC;
|
|
13
|
+
let _stylesInit = false;
|
|
14
|
+
export function resetJsonStyleCache() {
|
|
15
|
+
_stylesInit = false;
|
|
16
|
+
}
|
|
17
|
+
function ensureStyles() {
|
|
18
|
+
if (_stylesInit)
|
|
19
|
+
return;
|
|
20
|
+
_stylesInit = true;
|
|
21
|
+
const t = getTheme();
|
|
22
|
+
S_KEY = s(t.user);
|
|
23
|
+
S_STRING = s(t.success);
|
|
24
|
+
S_NUMBER = s(t.tool);
|
|
25
|
+
S_PUNCT = s(null, false, true);
|
|
26
|
+
S_TRUNC = s(null, false, true);
|
|
27
|
+
}
|
|
28
|
+
export function renderJsonTree(grid, row, col, value, width, opts) {
|
|
29
|
+
ensureStyles();
|
|
30
|
+
const lines = [];
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
emitValue(lines, value, 0, 0, seen);
|
|
33
|
+
const maxRows = Math.min(opts.limit - row, opts.maxLines);
|
|
34
|
+
if (maxRows <= 0)
|
|
35
|
+
return 0;
|
|
36
|
+
const truncated = lines.length > maxRows;
|
|
37
|
+
const visible = truncated ? lines.slice(0, maxRows - 1) : lines;
|
|
38
|
+
let r = row;
|
|
39
|
+
for (const line of visible) {
|
|
40
|
+
if (r >= opts.limit)
|
|
41
|
+
break;
|
|
42
|
+
let c = col + line.indent;
|
|
43
|
+
for (const tok of line.tokens) {
|
|
44
|
+
for (let i = 0; i < tok.text.length; i++) {
|
|
45
|
+
if (c >= col + width)
|
|
46
|
+
break;
|
|
47
|
+
grid.setCell(r, c, tok.text[i], tok.style);
|
|
48
|
+
c++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
r++;
|
|
52
|
+
}
|
|
53
|
+
if (truncated && r < opts.limit) {
|
|
54
|
+
const footer = `… (${lines.length} lines total)`;
|
|
55
|
+
grid.writeText(r, col, footer.slice(0, width), S_TRUNC);
|
|
56
|
+
r++;
|
|
57
|
+
}
|
|
58
|
+
return r - row;
|
|
59
|
+
}
|
|
60
|
+
function emitValue(out, value, indent, depth, seen) {
|
|
61
|
+
if (value === null) {
|
|
62
|
+
out.push({ indent, tokens: [{ text: "null", style: S_NUMBER }] });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === "string") {
|
|
66
|
+
out.push({ indent, tokens: [{ text: JSON.stringify(value), style: S_STRING }] });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
70
|
+
out.push({ indent, tokens: [{ text: String(value), style: S_NUMBER }] });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
if (value.length === 0) {
|
|
75
|
+
out.push({ indent, tokens: [{ text: "[]", style: S_PUNCT }] });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (depth >= MAX_DEPTH) {
|
|
79
|
+
out.push({ indent, tokens: [{ text: `[${value.length} items]`, style: S_TRUNC }] });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (seen.has(value)) {
|
|
83
|
+
out.push({ indent, tokens: [{ text: "[Circular]", style: S_TRUNC }] });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
seen.add(value);
|
|
87
|
+
out.push({ indent, tokens: [{ text: "[", style: S_PUNCT }] });
|
|
88
|
+
for (let i = 0; i < value.length; i++) {
|
|
89
|
+
emitValueAsItem(out, value[i], indent + 2, depth + 1, seen, i < value.length - 1);
|
|
90
|
+
}
|
|
91
|
+
out.push({ indent, tokens: [{ text: "]", style: S_PUNCT }] });
|
|
92
|
+
seen.delete(value);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (typeof value === "object") {
|
|
96
|
+
const entries = Object.entries(value);
|
|
97
|
+
if (entries.length === 0) {
|
|
98
|
+
out.push({ indent, tokens: [{ text: "{}", style: S_PUNCT }] });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (depth >= MAX_DEPTH) {
|
|
102
|
+
out.push({ indent, tokens: [{ text: "{…}", style: S_TRUNC }] });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (seen.has(value)) {
|
|
106
|
+
out.push({ indent, tokens: [{ text: "[Circular]", style: S_TRUNC }] });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
seen.add(value);
|
|
110
|
+
out.push({ indent, tokens: [{ text: "{", style: S_PUNCT }] });
|
|
111
|
+
for (let i = 0; i < entries.length; i++) {
|
|
112
|
+
const [k, v] = entries[i];
|
|
113
|
+
emitObjectEntry(out, k, v, indent + 2, depth + 1, seen, i < entries.length - 1);
|
|
114
|
+
}
|
|
115
|
+
out.push({ indent, tokens: [{ text: "}", style: S_PUNCT }] });
|
|
116
|
+
seen.delete(value);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
out.push({ indent, tokens: [{ text: String(value), style: S_TRUNC }] });
|
|
120
|
+
}
|
|
121
|
+
function emitValueAsItem(out, value, indent, depth, seen, trailingComma) {
|
|
122
|
+
const before = out.length;
|
|
123
|
+
emitValue(out, value, indent, depth, seen);
|
|
124
|
+
if (trailingComma && out.length > before) {
|
|
125
|
+
const last = out[out.length - 1];
|
|
126
|
+
last.tokens.push({ text: ",", style: S_PUNCT });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function emitObjectEntry(out, key, value, indent, depth, seen, trailingComma) {
|
|
130
|
+
const keyTok = { text: JSON.stringify(key), style: S_KEY };
|
|
131
|
+
const colonTok = { text: ": ", style: S_PUNCT };
|
|
132
|
+
// Inline primitives onto the same line as the key.
|
|
133
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
134
|
+
const valStyle = typeof value === "string" ? S_STRING : S_NUMBER;
|
|
135
|
+
const valText = typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
136
|
+
const tokens = [keyTok, colonTok, { text: valText, style: valStyle }];
|
|
137
|
+
if (trailingComma)
|
|
138
|
+
tokens.push({ text: ",", style: S_PUNCT });
|
|
139
|
+
out.push({ indent, tokens });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Empty container inlines too: "key": {} / "key": []
|
|
143
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
144
|
+
const tokens = [keyTok, colonTok, { text: "[]", style: S_PUNCT }];
|
|
145
|
+
if (trailingComma)
|
|
146
|
+
tokens.push({ text: ",", style: S_PUNCT });
|
|
147
|
+
out.push({ indent, tokens });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) {
|
|
151
|
+
const tokens = [keyTok, colonTok, { text: "{}", style: S_PUNCT }];
|
|
152
|
+
if (trailingComma)
|
|
153
|
+
tokens.push({ text: ",", style: S_PUNCT });
|
|
154
|
+
out.push({ indent, tokens });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Depth-collapsed container also inlines.
|
|
158
|
+
if (depth >= MAX_DEPTH) {
|
|
159
|
+
const collapsed = Array.isArray(value) ? `[${value.length} items]` : "{…}";
|
|
160
|
+
out.push({ indent, tokens: [keyTok, colonTok, { text: collapsed, style: S_TRUNC }] });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Circular reference: render inline, no descent.
|
|
164
|
+
if (seen.has(value)) {
|
|
165
|
+
const circ = [keyTok, colonTok, { text: "[Circular]", style: S_TRUNC }];
|
|
166
|
+
if (trailingComma)
|
|
167
|
+
circ.push({ text: ",", style: S_PUNCT });
|
|
168
|
+
out.push({ indent, tokens: circ });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Non-empty container: open bracket on key line, body indented, close bracket on its own line.
|
|
172
|
+
const opener = Array.isArray(value) ? "[" : "{";
|
|
173
|
+
const closer = Array.isArray(value) ? "]" : "}";
|
|
174
|
+
out.push({ indent, tokens: [keyTok, colonTok, { text: opener, style: S_PUNCT }] });
|
|
175
|
+
seen.add(value);
|
|
176
|
+
if (Array.isArray(value)) {
|
|
177
|
+
for (let i = 0; i < value.length; i++) {
|
|
178
|
+
emitValueAsItem(out, value[i], indent + 2, depth + 1, seen, i < value.length - 1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
const entries = Object.entries(value);
|
|
183
|
+
for (let i = 0; i < entries.length; i++) {
|
|
184
|
+
const [ck, cv] = entries[i];
|
|
185
|
+
emitObjectEntry(out, ck, cv, indent + 2, depth + 1, seen, i < entries.length - 1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
seen.delete(value);
|
|
189
|
+
const closerTokens = [{ text: closer, style: S_PUNCT }];
|
|
190
|
+
if (trailingComma)
|
|
191
|
+
closerTokens.push({ text: ",", style: S_PUNCT });
|
|
192
|
+
out.push({ indent, tokens: closerTokens });
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=json-tree.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getTheme } from "../utils/theme-data.js";
|
|
6
6
|
import { renderDiff } from "./diff.js";
|
|
7
|
-
import {
|
|
7
|
+
import { renderToolOutput } from "./output-renderer.js";
|
|
8
8
|
import { deriveSpinnerLabel } from "./spinner-label.js";
|
|
9
9
|
import { toolColor } from "./tool-color.js";
|
|
10
10
|
// ── Style constants ──
|
|
@@ -195,26 +195,12 @@ export function renderToolCallsSection(state, grid, r, limit, opts) {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
if (tc.output && tc.status !== "running" && isExpanded && r < limit) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const outLines = tc.output.split("\n");
|
|
205
|
-
const maxOut = 20;
|
|
206
|
-
const showLines = outLines.slice(0, maxOut);
|
|
207
|
-
for (const line of showLines) {
|
|
208
|
-
if (r >= limit)
|
|
209
|
-
break;
|
|
210
|
-
const lineStyle = tc.status === "error" ? S_ERROR : S_DIM;
|
|
211
|
-
grid.writeTextWithLinks(r, 6, line.slice(0, w - 8), lineStyle, w - 2);
|
|
212
|
-
r++;
|
|
213
|
-
}
|
|
214
|
-
if (outLines.length > maxOut && r < limit) {
|
|
215
|
-
grid.writeText(r, 6, `… (${outLines.length} lines total)`, S_DIM);
|
|
216
|
-
r++;
|
|
217
|
-
}
|
|
198
|
+
const consumed = renderToolOutput(grid, r, 6, tc.output, tc.outputType, w - 8, {
|
|
199
|
+
status: tc.status,
|
|
200
|
+
maxLines: 20,
|
|
201
|
+
limit,
|
|
202
|
+
});
|
|
203
|
+
r += consumed;
|
|
218
204
|
}
|
|
219
205
|
}
|
|
220
206
|
return r;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool output dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Detection chain (stops at first hit):
|
|
5
|
+
* 1. __IMAGE__: sentinel -> renderImageInline
|
|
6
|
+
* 2. outputType="json" -> renderJsonTree (fallback to plain on parse fail)
|
|
7
|
+
* 3. outputType="markdown" -> renderMarkdown
|
|
8
|
+
* 4. outputType="plain"|"image" -> renderPlain (image without sentinel is malformed)
|
|
9
|
+
* 5. heuristic JSON parse -> renderJsonTree
|
|
10
|
+
* 6. heuristic markdown -> renderMarkdown
|
|
11
|
+
* 7. fallback -> renderPlain
|
|
12
|
+
*/
|
|
13
|
+
import type { CellGrid } from "./cells.js";
|
|
14
|
+
export type OutputType = "json" | "markdown" | "image" | "plain";
|
|
15
|
+
export declare function renderToolOutput(grid: CellGrid, row: number, col: number, output: string, outputType: OutputType | undefined, width: number, opts: {
|
|
16
|
+
status: "running" | "done" | "error";
|
|
17
|
+
maxLines: number;
|
|
18
|
+
limit: number;
|
|
19
|
+
}): number;
|
|
20
|
+
export declare function tryParseJson(s: string): {
|
|
21
|
+
ok: true;
|
|
22
|
+
value: unknown;
|
|
23
|
+
} | {
|
|
24
|
+
ok: false;
|
|
25
|
+
};
|
|
26
|
+
export declare function looksLikeMarkdown(s: string): boolean;
|
|
27
|
+
//# sourceMappingURL=output-renderer.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool output dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Detection chain (stops at first hit):
|
|
5
|
+
* 1. __IMAGE__: sentinel -> renderImageInline
|
|
6
|
+
* 2. outputType="json" -> renderJsonTree (fallback to plain on parse fail)
|
|
7
|
+
* 3. outputType="markdown" -> renderMarkdown
|
|
8
|
+
* 4. outputType="plain"|"image" -> renderPlain (image without sentinel is malformed)
|
|
9
|
+
* 5. heuristic JSON parse -> renderJsonTree
|
|
10
|
+
* 6. heuristic markdown -> renderMarkdown
|
|
11
|
+
* 7. fallback -> renderPlain
|
|
12
|
+
*/
|
|
13
|
+
import { isImageOutput, renderImageInline } from "./image.js";
|
|
14
|
+
import { renderJsonTree } from "./json-tree.js";
|
|
15
|
+
import { renderMarkdown } from "./markdown.js";
|
|
16
|
+
const S_DIM = { fg: null, bg: null, bold: false, dim: true, underline: false };
|
|
17
|
+
const S_ERROR = { fg: "red", bg: null, bold: false, dim: false, underline: false };
|
|
18
|
+
export function renderToolOutput(grid, row, col, output, outputType, width, opts) {
|
|
19
|
+
// 1. Image sentinel always wins.
|
|
20
|
+
if (isImageOutput(output)) {
|
|
21
|
+
if (row >= opts.limit)
|
|
22
|
+
return 0;
|
|
23
|
+
const label = renderImageInline(output);
|
|
24
|
+
grid.writeText(row, col, label.slice(0, width), S_DIM);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
// 2-4. Typed dispatch.
|
|
28
|
+
if (outputType === "json") {
|
|
29
|
+
const parsed = tryParseJson(output);
|
|
30
|
+
if (parsed.ok)
|
|
31
|
+
return renderJsonTree(grid, row, col, parsed.value, width, { maxLines: opts.maxLines, limit: opts.limit });
|
|
32
|
+
return renderPlain(grid, row, col, output, width, opts);
|
|
33
|
+
}
|
|
34
|
+
if (outputType === "markdown") {
|
|
35
|
+
return renderMarkdown(grid, row, col, output, width, false, opts.limit);
|
|
36
|
+
}
|
|
37
|
+
if (outputType === "plain" || outputType === "image") {
|
|
38
|
+
return renderPlain(grid, row, col, output, width, opts);
|
|
39
|
+
}
|
|
40
|
+
// 5-7. Heuristic fallback (outputType undefined).
|
|
41
|
+
const json = tryParseJson(output);
|
|
42
|
+
if (json.ok)
|
|
43
|
+
return renderJsonTree(grid, row, col, json.value, width, { maxLines: opts.maxLines, limit: opts.limit });
|
|
44
|
+
if (looksLikeMarkdown(output))
|
|
45
|
+
return renderMarkdown(grid, row, col, output, width, false, opts.limit);
|
|
46
|
+
return renderPlain(grid, row, col, output, width, opts);
|
|
47
|
+
}
|
|
48
|
+
export function tryParseJson(s) {
|
|
49
|
+
const t = s.trimStart();
|
|
50
|
+
if (t[0] !== "{" && t[0] !== "[")
|
|
51
|
+
return { ok: false };
|
|
52
|
+
try {
|
|
53
|
+
return { ok: true, value: JSON.parse(t) };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return { ok: false };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const FENCED_RE = /```[\w]*\r?\n/;
|
|
60
|
+
const TABLE_RE = /^\|.+\|\s*\n\|[\s:|-]+\|/m;
|
|
61
|
+
const HEADING_RE = /^#{1,6}\s+\S/gm;
|
|
62
|
+
export function looksLikeMarkdown(s) {
|
|
63
|
+
if (FENCED_RE.test(s))
|
|
64
|
+
return true;
|
|
65
|
+
if (TABLE_RE.test(s))
|
|
66
|
+
return true;
|
|
67
|
+
const headings = s.match(HEADING_RE);
|
|
68
|
+
if (headings && headings.length >= 2)
|
|
69
|
+
return true;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
function renderPlain(grid, row, col, output, width, opts) {
|
|
73
|
+
const outLines = output.split("\n");
|
|
74
|
+
const showLines = outLines.slice(0, opts.maxLines);
|
|
75
|
+
const lineStyle = opts.status === "error" ? S_ERROR : S_DIM;
|
|
76
|
+
let r = row;
|
|
77
|
+
for (const line of showLines) {
|
|
78
|
+
if (r >= opts.limit)
|
|
79
|
+
break;
|
|
80
|
+
grid.writeTextWithLinks(r, col, line.slice(0, width), lineStyle, col + width);
|
|
81
|
+
r++;
|
|
82
|
+
}
|
|
83
|
+
if (outLines.length > opts.maxLines && r < opts.limit) {
|
|
84
|
+
grid.writeText(r, col, `… (${outLines.length} lines total)`.slice(0, width), S_DIM);
|
|
85
|
+
r++;
|
|
86
|
+
}
|
|
87
|
+
return r - row;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=output-renderer.js.map
|
package/dist/repl.js
CHANGED
|
@@ -21,6 +21,7 @@ import { isTrusted, trustSystemActive } from "./harness/trust.js";
|
|
|
21
21
|
import { query } from "./query/index.js";
|
|
22
22
|
import { resetDiffStyleCache } from "./renderer/diff.js";
|
|
23
23
|
import { TerminalRenderer } from "./renderer/index.js";
|
|
24
|
+
import { resetJsonStyleCache } from "./renderer/json-tree.js";
|
|
24
25
|
import { resetStyleCache } from "./renderer/layout.js";
|
|
25
26
|
import { resetMdStyleCache } from "./renderer/markdown.js";
|
|
26
27
|
import { createAssistantMessage, createInfoMessage, createMessage } from "./types/message.js";
|
|
@@ -28,6 +29,8 @@ import { formatTokenCount } from "./utils/format.js";
|
|
|
28
29
|
import { fuzzyFilter } from "./utils/fuzzy.js";
|
|
29
30
|
import { setActiveTheme } from "./utils/theme-data.js";
|
|
30
31
|
import { formatToolArgs, summarizeToolOutput } from "./utils/tool-summary.js";
|
|
32
|
+
/** Per-call cap on rendered tool output in renderer state. Sized to fit typical JSON/markdown files (16 KiB) so JSON.parse / markdown detection works on real content; larger outputs render truncated. */
|
|
33
|
+
const TOOL_OUTPUT_RENDER_CAP = 16384;
|
|
31
34
|
export async function startREPL(config) {
|
|
32
35
|
if (config.theme)
|
|
33
36
|
setActiveTheme(config.theme);
|
|
@@ -814,6 +817,7 @@ export async function startREPL(config) {
|
|
|
814
817
|
resetStyleCache();
|
|
815
818
|
resetMdStyleCache();
|
|
816
819
|
resetDiffStyleCache();
|
|
820
|
+
resetJsonStyleCache();
|
|
817
821
|
// Persist theme to config
|
|
818
822
|
try {
|
|
819
823
|
const cfg = cachedConfig ?? {
|
|
@@ -980,7 +984,8 @@ export async function startREPL(config) {
|
|
|
980
984
|
renderer.setToolCall(event.callId, {
|
|
981
985
|
toolName,
|
|
982
986
|
status: event.isError ? "error" : "done",
|
|
983
|
-
output: event.output?.slice(0,
|
|
987
|
+
output: event.output?.slice(0, TOOL_OUTPUT_RENDER_CAP),
|
|
988
|
+
outputType: event.outputType,
|
|
984
989
|
args: prevTc?.args,
|
|
985
990
|
resultSummary: event.output ? summarizeToolOutput(event.output) : undefined,
|
|
986
991
|
startedAt: prevTc?.startedAt,
|
|
@@ -10,6 +10,15 @@ const inputSchema = z.object({
|
|
|
10
10
|
const DEFAULT_LIMIT = 2000;
|
|
11
11
|
const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"]);
|
|
12
12
|
const NOTEBOOK_EXTENSION = ".ipynb";
|
|
13
|
+
const MARKDOWN_EXTENSIONS = new Set([".md", ".markdown"]);
|
|
14
|
+
const JSON_EXTENSIONS = new Set([".json"]);
|
|
15
|
+
function outputTypeFromExt(ext) {
|
|
16
|
+
if (JSON_EXTENSIONS.has(ext))
|
|
17
|
+
return "json";
|
|
18
|
+
if (MARKDOWN_EXTENSIONS.has(ext))
|
|
19
|
+
return "markdown";
|
|
20
|
+
return "plain";
|
|
21
|
+
}
|
|
13
22
|
function parsePageRange(pages) {
|
|
14
23
|
const result = [];
|
|
15
24
|
for (const part of pages.split(",")) {
|
|
@@ -101,7 +110,7 @@ export const FileReadTool = {
|
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
if (pageTexts.length > 0) {
|
|
104
|
-
return { output: pageTexts.join("\n\n"), isError: false };
|
|
113
|
+
return { output: pageTexts.join("\n\n"), isError: false, outputType: "plain" };
|
|
105
114
|
}
|
|
106
115
|
// Fallback: return as base64 for multimodal analysis
|
|
107
116
|
const base64 = buffer.toString("base64");
|
|
@@ -128,7 +137,7 @@ export const FileReadTool = {
|
|
|
128
137
|
}
|
|
129
138
|
}
|
|
130
139
|
}
|
|
131
|
-
return { output: parts.join("\n\n"), isError: false };
|
|
140
|
+
return { output: parts.join("\n\n"), isError: false, outputType: "plain" };
|
|
132
141
|
}
|
|
133
142
|
// Default: text file
|
|
134
143
|
const content = await fs.readFile(filePath, "utf-8");
|
|
@@ -143,7 +152,7 @@ export const FileReadTool = {
|
|
|
143
152
|
if (shown < total) {
|
|
144
153
|
result += `\n\n(Showing lines ${offset + 1}-${offset + shown} of ${total})`;
|
|
145
154
|
}
|
|
146
|
-
return { output: result, isError: false };
|
|
155
|
+
return { output: result, isError: false, outputType: outputTypeFromExt(ext) };
|
|
147
156
|
}
|
|
148
157
|
catch (err) {
|
|
149
158
|
if (err.code === "ENOENT") {
|
|
@@ -70,7 +70,7 @@ export const WebFetchTool = {
|
|
|
70
70
|
signal: AbortSignal.timeout(30_000),
|
|
71
71
|
});
|
|
72
72
|
// Re-check host after redirect to prevent SSRF via open redirects
|
|
73
|
-
const finalUrl = new URL(response.url);
|
|
73
|
+
const finalUrl = new URL(response.url || input.url);
|
|
74
74
|
if (isBlockedHost(finalUrl.hostname)) {
|
|
75
75
|
return { output: "Error: Redirect to private/internal host blocked.", isError: true };
|
|
76
76
|
}
|
|
@@ -88,7 +88,10 @@ export const WebFetchTool = {
|
|
|
88
88
|
if (text.length > MAX_OUTPUT) {
|
|
89
89
|
text = `${text.slice(0, MAX_OUTPUT)}\n... [truncated]`;
|
|
90
90
|
}
|
|
91
|
-
|
|
91
|
+
const ct = contentType.toLowerCase();
|
|
92
|
+
const isJson = /^application\/(?:[a-z0-9.+-]+\+)?json\b/.test(ct);
|
|
93
|
+
const outputType = isJson ? "json" : ct.includes("text/markdown") ? "markdown" : "plain";
|
|
94
|
+
return { output: text, isError: false, outputType };
|
|
92
95
|
}
|
|
93
96
|
catch (err) {
|
|
94
97
|
return { output: `Error fetching URL: ${err.message}`, isError: true };
|
package/dist/types/events.d.ts
CHANGED