@heyhuynhgiabuu/pi-pretty 0.6.25 → 0.6.27
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/CHANGELOG.md +34 -0
- package/README.md +47 -17
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +30 -0
- package/dist/config.js.map +1 -1
- package/dist/fff.d.ts +16 -1
- package/dist/fff.d.ts.map +1 -1
- package/dist/fff.js +139 -33
- package/dist/fff.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +192 -43
- package/dist/index.js.map +1 -1
- package/dist/prompt-editor.d.ts +11 -0
- package/dist/prompt-editor.d.ts.map +1 -0
- package/dist/prompt-editor.js +78 -0
- package/dist/prompt-editor.js.map +1 -0
- package/dist/render.d.ts +9 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +25 -9
- package/dist/render.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +73 -30
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +29 -10
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +20 -10
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +10 -5
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/metrics.d.ts +6 -1
- package/dist/tools/metrics.d.ts.map +1 -1
- package/dist/tools/metrics.js +21 -8
- package/dist/tools/metrics.js.map +1 -1
- package/dist/tools/read.d.ts +2 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +93 -30
- package/dist/tools/read.js.map +1 -1
- package/dist/types.d.ts +13 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/working-indicator.d.ts +91 -26
- package/dist/working-indicator.d.ts.map +1 -1
- package/dist/working-indicator.js +259 -77
- package/dist/working-indicator.js.map +1 -1
- package/package.json +7 -6
- package/src/config.ts +51 -0
- package/src/fff.ts +158 -29
- package/src/index.ts +207 -47
- package/src/prompt-editor.ts +102 -0
- package/src/render.ts +30 -11
- package/src/tools/bash.ts +94 -32
- package/src/tools/find.ts +39 -17
- package/src/tools/grep.ts +26 -15
- package/src/tools/ls.ts +19 -10
- package/src/tools/metrics.ts +28 -8
- package/src/tools/read.ts +122 -30
- package/src/types.ts +14 -3
- package/src/working-indicator.ts +309 -88
package/src/tools/bash.ts
CHANGED
|
@@ -2,14 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { resolveBaseBackground, TOOL_RESULT_INDENT, termWidth } from "../config.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
CHARS_KEY,
|
|
7
|
+
compactErrorLines,
|
|
8
|
+
ELAPSED_KEY,
|
|
9
|
+
formatCharCount,
|
|
10
|
+
inferBashExitCode,
|
|
11
|
+
stripBashExitStatusLine,
|
|
12
|
+
} from "../helpers.js";
|
|
13
|
+
import {
|
|
14
|
+
fillToolBackground,
|
|
15
|
+
fillToolBody,
|
|
16
|
+
rememberToolTitle,
|
|
17
|
+
renderToolDuration,
|
|
18
|
+
renderToolError,
|
|
19
|
+
setCollapsedToolTitle,
|
|
20
|
+
} from "../render.js";
|
|
7
21
|
import { resolveTextCtor } from "../tui-text.js";
|
|
8
22
|
import type { BashDetails, ComponentLike, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
|
|
9
|
-
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
23
|
+
import { type RejectedExecutionMetrics, wrapExecuteWithMetrics } from "./metrics.js";
|
|
10
24
|
|
|
11
25
|
type Result = AgentToolResult<Record<string, unknown>>;
|
|
12
26
|
|
|
27
|
+
const BASH_REJECTED_METRICS_KEY = "__piPrettyBashRejectedMetrics";
|
|
28
|
+
const BASH_RESULT_RENDER_KEY = "__piPrettyBashResultRender";
|
|
29
|
+
const REJECTED_METRICS_TTL_MS = 60_000;
|
|
30
|
+
const rejectedBashMetrics = new Map<string, RejectedExecutionMetrics>();
|
|
31
|
+
|
|
32
|
+
function rememberRejectedBashMetrics(toolCallId: string, metrics: RejectedExecutionMetrics): void {
|
|
33
|
+
rejectedBashMetrics.set(toolCallId, metrics);
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
if (rejectedBashMetrics.get(toolCallId) === metrics) rejectedBashMetrics.delete(toolCallId);
|
|
36
|
+
}, REJECTED_METRICS_TTL_MS);
|
|
37
|
+
timer.unref?.();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function takeRejectedBashMetrics(ctx: RenderCtxLike): RejectedExecutionMetrics | undefined {
|
|
41
|
+
const state = ctx.state;
|
|
42
|
+
const cached = state[BASH_REJECTED_METRICS_KEY] as RejectedExecutionMetrics | undefined;
|
|
43
|
+
if (cached) return cached;
|
|
44
|
+
if (!ctx.toolCallId) return undefined;
|
|
45
|
+
const metrics = rejectedBashMetrics.get(ctx.toolCallId);
|
|
46
|
+
if (metrics) {
|
|
47
|
+
rejectedBashMetrics.delete(ctx.toolCallId);
|
|
48
|
+
state[BASH_REJECTED_METRICS_KEY] = metrics;
|
|
49
|
+
}
|
|
50
|
+
return metrics;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function addRejectedMetrics(result: Result, metrics: RejectedExecutionMetrics): Result {
|
|
54
|
+
const details = {
|
|
55
|
+
...((result.details ?? {}) as Record<string, unknown>),
|
|
56
|
+
[ELAPSED_KEY]: metrics.elapsedMs,
|
|
57
|
+
[CHARS_KEY]: metrics.chars,
|
|
58
|
+
};
|
|
59
|
+
return { ...result, details } as Result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function restoreBashResultRender(ctx: RenderCtxLike, text: ComponentLike): void {
|
|
63
|
+
const state = ctx.state;
|
|
64
|
+
const original = state[BASH_RESULT_RENDER_KEY] as ((width: number) => string[]) | undefined;
|
|
65
|
+
if (!original) return;
|
|
66
|
+
(text as unknown as { render: (width: number) => string[] }).render = original;
|
|
67
|
+
delete state[BASH_RESULT_RENDER_KEY];
|
|
68
|
+
}
|
|
69
|
+
|
|
13
70
|
export function registerBashTool(
|
|
14
71
|
pi: ExtensionAPI,
|
|
15
72
|
_cwd: string,
|
|
@@ -33,23 +90,12 @@ export function registerBashTool(
|
|
|
33
90
|
parameters: sdkTool.parameters,
|
|
34
91
|
renderShell: "self",
|
|
35
92
|
|
|
36
|
-
execute: wrapExecuteWithMetrics(async (tid, params, sig,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
content: [{ type: "text" as const, text: msg }],
|
|
43
|
-
isError: true,
|
|
44
|
-
details: {
|
|
45
|
-
_type: "bashResult",
|
|
46
|
-
text: msg,
|
|
47
|
-
exitCode: 1,
|
|
48
|
-
command: String((params as any).command ?? ""),
|
|
49
|
-
} as BashDetails,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
}),
|
|
93
|
+
execute: wrapExecuteWithMetrics(async (tid, params, sig, upd, ctx: ExtensionContext) => {
|
|
94
|
+
// Let the host derive the tool-error flag from the rejected execution.
|
|
95
|
+
// AgentToolResult has no portable `isError` field, so converting a bash
|
|
96
|
+
// failure into a successful-looking result hides the failure from the model.
|
|
97
|
+
return (await sdkTool.execute(tid, params, sig, upd, ctx)) as Result;
|
|
98
|
+
}, rememberRejectedBashMetrics),
|
|
53
99
|
|
|
54
100
|
renderCall(args: any, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
55
101
|
resolveBaseBackground(theme);
|
|
@@ -65,9 +111,14 @@ export function registerBashTool(
|
|
|
65
111
|
? `${rawCmd.slice(0, Math.max(1, headerBudget))}…`
|
|
66
112
|
: rawCmd;
|
|
67
113
|
const commandLabel = theme.fg(ctx.isError ? "error" : "toolTitle", theme.bold(`$ ${cmd}`));
|
|
68
|
-
|
|
69
|
-
fillToolBackground(
|
|
70
|
-
|
|
114
|
+
const renderTitle = (suffix = ""): string =>
|
|
115
|
+
fillToolBackground(
|
|
116
|
+
`\n${TOOL_RESULT_INDENT}${commandLabel}${t}${suffix}\n`,
|
|
117
|
+
undefined,
|
|
118
|
+
ctx.expanded ? undefined : tw,
|
|
119
|
+
);
|
|
120
|
+
rememberToolTitle(ctx, text, renderTitle);
|
|
121
|
+
text.setText(renderTitle());
|
|
71
122
|
return text;
|
|
72
123
|
},
|
|
73
124
|
|
|
@@ -75,9 +126,12 @@ export function registerBashTool(
|
|
|
75
126
|
resolveBaseBackground(theme);
|
|
76
127
|
|
|
77
128
|
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
129
|
+
restoreBashResultRender(ctx, text as ComponentLike);
|
|
130
|
+
const rejectedMetrics = takeRejectedBashMetrics(ctx);
|
|
131
|
+
const displayResult = rejectedMetrics ? addRejectedMetrics(result, rejectedMetrics) : result;
|
|
78
132
|
|
|
79
|
-
const details =
|
|
80
|
-
const tc = getText(
|
|
133
|
+
const details = displayResult.details;
|
|
134
|
+
const tc = getText(displayResult);
|
|
81
135
|
const d: BashDetails | undefined =
|
|
82
136
|
(details as BashDetails)?._type === "bashResult"
|
|
83
137
|
? (details as BashDetails)
|
|
@@ -95,29 +149,37 @@ export function registerBashTool(
|
|
|
95
149
|
const cleaned = stripBashExitStatusLine(d.text);
|
|
96
150
|
const output = isErr ? compactErrorLines(cleaned).join("\n") : cleaned;
|
|
97
151
|
const lineCount = output.split("\n").length;
|
|
98
|
-
const info = [
|
|
152
|
+
const info = [
|
|
153
|
+
`${lineCount} lines`,
|
|
154
|
+
renderToolDuration(displayResult),
|
|
155
|
+
rejectedMetrics ? formatCharCount(rejectedMetrics.chars) : "",
|
|
156
|
+
!ctx.expanded ? "ctrl+o to expand" : "",
|
|
157
|
+
]
|
|
99
158
|
.filter(Boolean)
|
|
100
159
|
.map((part) => theme.fg("dim", part))
|
|
101
160
|
.join(theme.fg("dim", " · "));
|
|
102
161
|
const header = `${TOOL_RESULT_INDENT}${info}`;
|
|
103
162
|
const rw = termWidth();
|
|
104
163
|
|
|
164
|
+
if (setCollapsedToolTitle(ctx, text, ` ${info}`)) return text;
|
|
165
|
+
|
|
105
166
|
const renderFn = (w: number) => {
|
|
106
|
-
if (!ctx.expanded) return
|
|
107
|
-
if (!output.trim()) return
|
|
167
|
+
if (!ctx.expanded) return fillToolBody(header, undefined, w);
|
|
168
|
+
if (!output.trim()) return fillToolBody(header, undefined, w);
|
|
108
169
|
const show = output.split("\n");
|
|
109
170
|
const out = [header, "", ...show.map((line: string) => `${TOOL_RESULT_INDENT}${line}`)];
|
|
110
|
-
return
|
|
171
|
+
return fillToolBody(out.join("\n"), undefined, w);
|
|
111
172
|
};
|
|
112
173
|
|
|
113
174
|
text.setText(renderFn(rw));
|
|
114
175
|
const baseRender =
|
|
115
176
|
typeof (text as ComponentLike).render === "function" ? (text as ComponentLike).render.bind(text) : null;
|
|
116
177
|
if (baseRender) {
|
|
178
|
+
ctx.state[BASH_RESULT_RENDER_KEY] = baseRender;
|
|
117
179
|
let key: string | undefined;
|
|
118
180
|
(text as unknown as Record<string, unknown>).render = (w: number) => {
|
|
119
181
|
const width = Math.max(1, Math.floor(w || termWidth()));
|
|
120
|
-
const k = `bash:${ctx.expanded ? "1" : "0"}:${width}:${d.exitCode ?? "killed"}:${output.length}:${renderToolDuration(
|
|
182
|
+
const k = `bash:${ctx.expanded ? "1" : "0"}:${width}:${d.exitCode ?? "killed"}:${output.length}:${renderToolDuration(displayResult)}`;
|
|
121
183
|
if (key !== k) {
|
|
122
184
|
text.setText(renderFn(width));
|
|
123
185
|
key = k;
|
|
@@ -132,9 +194,9 @@ export function registerBashTool(
|
|
|
132
194
|
text.setText(renderToolError(tc || "Error", theme));
|
|
133
195
|
return text;
|
|
134
196
|
}
|
|
135
|
-
const fc =
|
|
197
|
+
const fc = displayResult.content?.[0];
|
|
136
198
|
text.setText(
|
|
137
|
-
|
|
199
|
+
fillToolBody(
|
|
138
200
|
`${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
|
|
139
201
|
),
|
|
140
202
|
);
|
package/src/tools/find.ts
CHANGED
|
@@ -6,7 +6,15 @@ import { BG_ERROR, FG_DIM, RST, resolveBaseBackground, TOOL_RESULT_INDENT } from
|
|
|
6
6
|
import { isLikelyGlobPattern, normalizeFindGlobPattern } from "../find-glob.js";
|
|
7
7
|
import { shortPath } from "../helpers.js";
|
|
8
8
|
import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
fillToolBackground,
|
|
11
|
+
fillToolBody,
|
|
12
|
+
rememberToolTitle,
|
|
13
|
+
renderFindResults,
|
|
14
|
+
renderToolDuration,
|
|
15
|
+
renderToolError,
|
|
16
|
+
setCollapsedToolTitle,
|
|
17
|
+
} from "../render.js";
|
|
10
18
|
import { resolveTextCtor } from "../tui-text.js";
|
|
11
19
|
import type { FffServiceWithCursor, FindDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
|
|
12
20
|
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
@@ -40,6 +48,19 @@ function buildGlobPattern(pattern: string, path: string | undefined, basePath: s
|
|
|
40
48
|
return normalized.startsWith("**/") || normalized.includes("/") ? normalized : `**/${normalized}`;
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
function normalizeSdkFindOutput(text: string): { text: string; matchCount: number; notices: string[] } {
|
|
52
|
+
const trimmed = text.trim();
|
|
53
|
+
if (!trimmed || trimmed === "No files found matching pattern") {
|
|
54
|
+
return { text: "", matchCount: 0, notices: [] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const noticeMatch = /\n\n\[([^\r\n]*)\]\s*$/.exec(text);
|
|
58
|
+
const body = noticeMatch ? text.slice(0, noticeMatch.index) : text;
|
|
59
|
+
const notices = noticeMatch ? noticeMatch[1].split(". ").filter(Boolean) : [];
|
|
60
|
+
const matchCount = body.trim() ? body.trim().split("\n").filter(Boolean).length : 0;
|
|
61
|
+
return { text: body, matchCount, notices };
|
|
62
|
+
}
|
|
63
|
+
|
|
43
64
|
async function sdkFindAsFindResult(
|
|
44
65
|
sdkTool: SdkToolDef,
|
|
45
66
|
tid: string,
|
|
@@ -50,14 +71,14 @@ async function sdkFindAsFindResult(
|
|
|
50
71
|
extraNotices: string[],
|
|
51
72
|
): Promise<Result> {
|
|
52
73
|
const result = (await sdkTool.execute(tid, params, sig, undefined, ctx)) as Result;
|
|
53
|
-
const
|
|
74
|
+
const parsed = normalizeSdkFindOutput(getText(result));
|
|
54
75
|
const prev = (result.details as FindDetails | undefined)?.notices ?? [];
|
|
55
|
-
const notices = [...(Array.isArray(prev) ? prev : []), ...extraNotices];
|
|
76
|
+
const notices = [...parsed.notices, ...(Array.isArray(prev) ? prev : []), ...extraNotices];
|
|
56
77
|
result.details = {
|
|
57
78
|
_type: "findResult",
|
|
58
|
-
text:
|
|
79
|
+
text: parsed.text,
|
|
59
80
|
pattern,
|
|
60
|
-
matchCount:
|
|
81
|
+
matchCount: parsed.matchCount,
|
|
61
82
|
notices,
|
|
62
83
|
};
|
|
63
84
|
return result;
|
|
@@ -150,7 +171,10 @@ export function registerFindTool(
|
|
|
150
171
|
const pathPart = theme.fg("toolOutput", pathArg);
|
|
151
172
|
const limitPart = limit !== undefined && limit !== null ? theme.fg("dim", ` limit ${limit}`) : "";
|
|
152
173
|
const out = `${findLabel} ${patternPart}${inPart}${pathPart}${limitPart}`;
|
|
153
|
-
|
|
174
|
+
const renderTitle = (suffix = ""): string =>
|
|
175
|
+
fillToolBackground(`\n${TOOL_RESULT_INDENT}${out}${suffix}\n`, ctx.isError ? BG_ERROR : undefined);
|
|
176
|
+
rememberToolTitle(ctx, text, renderTitle);
|
|
177
|
+
text.setText(renderTitle());
|
|
154
178
|
return text;
|
|
155
179
|
},
|
|
156
180
|
|
|
@@ -165,19 +189,19 @@ export function registerFindTool(
|
|
|
165
189
|
const d = r.details as FindDetails | undefined;
|
|
166
190
|
if (d?._type === "findResult") {
|
|
167
191
|
if (!d.text.trim()) {
|
|
192
|
+
const notice = d.notices?.length ? ` ${theme.fg("warning", `[${d.notices.join(". ")}]`)}` : "";
|
|
193
|
+
if (setCollapsedToolTitle(ctx, text, ` ${FG_DIM}0 files${RST}${notice}`)) return text;
|
|
168
194
|
const noticeStr = d.notices?.length
|
|
169
195
|
? `\n${TOOL_RESULT_INDENT}${theme.fg("warning", `[${d.notices.join(". ")}]`)}`
|
|
170
196
|
: "";
|
|
171
|
-
text.setText(
|
|
197
|
+
text.setText(fillToolBody(`${TOOL_RESULT_INDENT}${theme.fg("dim", "0 files")}${noticeStr}`));
|
|
172
198
|
return text;
|
|
173
199
|
}
|
|
174
200
|
if (!ctx.expanded) {
|
|
175
201
|
const duration = renderToolDuration(r);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
),
|
|
180
|
-
);
|
|
202
|
+
const summary = `${FG_DIM}${d.matchCount} files — ctrl+o to expand${RST}${duration ? `${FG_DIM}· ${duration}${RST}` : ""}`;
|
|
203
|
+
if (setCollapsedToolTitle(ctx, text, ` ${summary}`)) return text;
|
|
204
|
+
text.setText(fillToolBody(`${TOOL_RESULT_INDENT}${summary}`));
|
|
181
205
|
return text;
|
|
182
206
|
}
|
|
183
207
|
const rendered = renderFindResults(d.text, theme)
|
|
@@ -189,16 +213,14 @@ export function registerFindTool(
|
|
|
189
213
|
: "";
|
|
190
214
|
const duration = renderToolDuration(r);
|
|
191
215
|
text.setText(
|
|
192
|
-
|
|
193
|
-
|
|
216
|
+
fillToolBody(
|
|
217
|
+
`${TOOL_RESULT_INDENT}${theme.fg("dim", `${d.matchCount} files`)}${duration ? `${FG_DIM}· ${duration}${RST}` : ""}\n${rendered}${noticeStr}`,
|
|
194
218
|
),
|
|
195
219
|
);
|
|
196
220
|
return text;
|
|
197
221
|
}
|
|
198
222
|
const fc = r.content?.[0] as TextContent | undefined;
|
|
199
|
-
text.setText(
|
|
200
|
-
fillToolBackground(`\n${TOOL_RESULT_INDENT}${theme.fg("dim", fc?.text?.slice(0, 120) ?? "0 files")}\n`),
|
|
201
|
-
);
|
|
223
|
+
text.setText(fillToolBody(`${TOOL_RESULT_INDENT}${theme.fg("dim", fc?.text?.slice(0, 120) ?? "0 files")}`));
|
|
202
224
|
return text;
|
|
203
225
|
},
|
|
204
226
|
} as unknown as ToolDefinition);
|
package/src/tools/grep.ts
CHANGED
|
@@ -5,7 +5,13 @@ import { BG_ERROR, FG_DIM, RST, resolveBaseBackground, TOOL_RESULT_INDENT } from
|
|
|
5
5
|
import { fffFormatGrepText } from "../fff-helpers.js";
|
|
6
6
|
import { normalizeLineEndings, shortPath } from "../helpers.js";
|
|
7
7
|
import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
fillToolBackground,
|
|
10
|
+
fillToolBody,
|
|
11
|
+
rememberToolTitle,
|
|
12
|
+
renderToolError,
|
|
13
|
+
setCollapsedToolTitle,
|
|
14
|
+
} from "../render.js";
|
|
9
15
|
import { resolveTextCtor } from "../tui-text.js";
|
|
10
16
|
import type { FffServiceWithCursor, GrepDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
|
|
11
17
|
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
@@ -40,7 +46,10 @@ export function registerGrepTool(
|
|
|
40
46
|
const limit = typeof p.limit === "number" ? p.limit : 200;
|
|
41
47
|
const literal = p.literal === true;
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
// FFF exposes smart-case, not the SDK's explicit ignoreCase mode.
|
|
50
|
+
// Fall back when callers request case-insensitive matching so the SDK
|
|
51
|
+
// remains the semantic source of truth for that option.
|
|
52
|
+
if (fffService?.isAvailable && !path && !glob && p.ignoreCase !== true) {
|
|
44
53
|
try {
|
|
45
54
|
const fff = fffService.getFinder();
|
|
46
55
|
if (!fff) throw new Error("FFF finder not available");
|
|
@@ -48,6 +57,7 @@ export function registerGrepTool(
|
|
|
48
57
|
const grepResult = fff.grep(pattern, {
|
|
49
58
|
pageSize: effectiveLimit,
|
|
50
59
|
mode: literal ? "plain" : "regex",
|
|
60
|
+
smartCase: false,
|
|
51
61
|
beforeContext: context,
|
|
52
62
|
afterContext: context,
|
|
53
63
|
});
|
|
@@ -68,7 +78,7 @@ export function registerGrepTool(
|
|
|
68
78
|
content: [{ type: "text" as const, text }],
|
|
69
79
|
details: {
|
|
70
80
|
_type: "grepResult",
|
|
71
|
-
text,
|
|
81
|
+
text: items.length > 0 ? text : "",
|
|
72
82
|
pattern,
|
|
73
83
|
matchCount: items.length,
|
|
74
84
|
} as GrepDetails,
|
|
@@ -88,11 +98,12 @@ export function registerGrepTool(
|
|
|
88
98
|
.filter((c) => c.type === "text")
|
|
89
99
|
.map((c) => c.text)
|
|
90
100
|
.join("\n") ?? "";
|
|
101
|
+
const normalizedText = tc.trim() === "No matches found" ? "" : tc;
|
|
91
102
|
result.details = {
|
|
92
103
|
_type: "grepResult",
|
|
93
|
-
text:
|
|
104
|
+
text: normalizedText,
|
|
94
105
|
pattern,
|
|
95
|
-
matchCount:
|
|
106
|
+
matchCount: normalizedText ? normalizedText.trim().split("\n").filter(Boolean).length : 0,
|
|
96
107
|
} as GrepDetails;
|
|
97
108
|
return result;
|
|
98
109
|
}),
|
|
@@ -111,7 +122,10 @@ export function registerGrepTool(
|
|
|
111
122
|
if (limit !== undefined && limit !== null) out += theme.fg("dim", ` limit ${limit}`);
|
|
112
123
|
if (literal) out += theme.fg("dim", ` (literal)`);
|
|
113
124
|
if (caseInsensitive) out += theme.fg("dim", ` (case-insensitive)`);
|
|
114
|
-
|
|
125
|
+
const renderTitle = (suffix = ""): string =>
|
|
126
|
+
fillToolBackground(`\n${TOOL_RESULT_INDENT}${out}${suffix}\n`, ctx.isError ? BG_ERROR : undefined);
|
|
127
|
+
rememberToolTitle(ctx, text, renderTitle);
|
|
128
|
+
text.setText(renderTitle());
|
|
115
129
|
return text;
|
|
116
130
|
},
|
|
117
131
|
|
|
@@ -134,12 +148,9 @@ export function registerGrepTool(
|
|
|
134
148
|
if (d?._type === "grepResult" && d.text) {
|
|
135
149
|
const lines = d.text.split("\n");
|
|
136
150
|
if (!ctx.expanded) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
ctx.isError ? BG_ERROR : undefined,
|
|
141
|
-
),
|
|
142
|
-
);
|
|
151
|
+
const summary = `${FG_DIM}${lines.length} lines — ctrl+o to expand${RST}`;
|
|
152
|
+
if (setCollapsedToolTitle(ctx, text, ` ${summary}`)) return text;
|
|
153
|
+
text.setText(fillToolBody(`${TOOL_RESULT_INDENT}${summary}`, ctx.isError ? BG_ERROR : undefined));
|
|
143
154
|
return text;
|
|
144
155
|
}
|
|
145
156
|
const maxShow = lines.length;
|
|
@@ -153,14 +164,14 @@ export function registerGrepTool(
|
|
|
153
164
|
if (remaining > 0) {
|
|
154
165
|
out.push(theme.fg("muted", `… (${remaining} more ${remaining === 1 ? "line" : "lines"}, to expand)`));
|
|
155
166
|
}
|
|
156
|
-
const body =
|
|
157
|
-
text.setText(
|
|
167
|
+
const body = out.map((l) => `${TOOL_RESULT_INDENT}${l}`).join("\n");
|
|
168
|
+
text.setText(fillToolBody(body, ctx.isError ? BG_ERROR : undefined));
|
|
158
169
|
return text;
|
|
159
170
|
}
|
|
160
171
|
const fc = result.content?.[0];
|
|
161
172
|
const fallback = fc && "text" in fc ? String(fc.text).slice(0, 120) : "no matches";
|
|
162
173
|
text.setText(
|
|
163
|
-
|
|
174
|
+
fillToolBody(`${TOOL_RESULT_INDENT}${theme.fg("dim", fallback)}`, ctx.isError ? BG_ERROR : undefined),
|
|
164
175
|
);
|
|
165
176
|
return text;
|
|
166
177
|
},
|
package/src/tools/ls.ts
CHANGED
|
@@ -3,7 +3,15 @@
|
|
|
3
3
|
import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { BG_ERROR, FG_DIM, RST, resolveBaseBackground, TOOL_RESULT_INDENT } from "../config.js";
|
|
5
5
|
import { shortPath } from "../helpers.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
fillToolBackground,
|
|
8
|
+
fillToolBody,
|
|
9
|
+
rememberToolTitle,
|
|
10
|
+
renderToolError,
|
|
11
|
+
renderToolMetrics,
|
|
12
|
+
renderTree,
|
|
13
|
+
setCollapsedToolTitle,
|
|
14
|
+
} from "../render.js";
|
|
7
15
|
import { resolveTextCtor } from "../tui-text.js";
|
|
8
16
|
import type { LsDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
|
|
9
17
|
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
@@ -51,7 +59,10 @@ export function registerLsTool(
|
|
|
51
59
|
let out = theme.fg("toolTitle", theme.bold("ls"));
|
|
52
60
|
if (path) out += ` ${theme.fg("accent", path)}`;
|
|
53
61
|
if (limit !== undefined && limit !== null) out += theme.fg("toolOutput", ` (limit ${limit})`);
|
|
54
|
-
|
|
62
|
+
const renderTitle = (suffix = ""): string =>
|
|
63
|
+
fillToolBackground(`\n${TOOL_RESULT_INDENT}${out}${suffix}\n`, ctx.isError ? BG_ERROR : undefined);
|
|
64
|
+
rememberToolTitle(ctx, text, renderTitle);
|
|
65
|
+
text.setText(renderTitle());
|
|
55
66
|
return text;
|
|
56
67
|
},
|
|
57
68
|
|
|
@@ -66,11 +77,9 @@ export function registerLsTool(
|
|
|
66
77
|
const d = result.details as LsDetails | undefined;
|
|
67
78
|
if (d?._type === "lsResult" && d.text) {
|
|
68
79
|
if (!ctx.expanded) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
),
|
|
73
|
-
);
|
|
80
|
+
const summary = `${FG_DIM}${d.entryCount} entries — ctrl+o to expand${RST}${renderToolMetrics(result)}`;
|
|
81
|
+
if (setCollapsedToolTitle(ctx, text, ` ${summary}`)) return text;
|
|
82
|
+
text.setText(fillToolBody(`${TOOL_RESULT_INDENT}${summary}`));
|
|
74
83
|
return text;
|
|
75
84
|
}
|
|
76
85
|
const rendered = renderTree(d.text, d.path)
|
|
@@ -78,15 +87,15 @@ export function registerLsTool(
|
|
|
78
87
|
.map((l) => `${TOOL_RESULT_INDENT}${l}`)
|
|
79
88
|
.join("\n");
|
|
80
89
|
text.setText(
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
fillToolBody(
|
|
91
|
+
`${TOOL_RESULT_INDENT}${FG_DIM}${d.entryCount} entries${RST}${renderToolMetrics(result)}\n${rendered}`,
|
|
83
92
|
),
|
|
84
93
|
);
|
|
85
94
|
return text;
|
|
86
95
|
}
|
|
87
96
|
const fc = result.content?.[0];
|
|
88
97
|
text.setText(
|
|
89
|
-
|
|
98
|
+
fillToolBody(
|
|
90
99
|
`${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
|
|
91
100
|
),
|
|
92
101
|
);
|
package/src/tools/metrics.ts
CHANGED
|
@@ -15,19 +15,39 @@ type ExecuteFn = (
|
|
|
15
15
|
ctx: ExtensionContext,
|
|
16
16
|
) => Promise<AgentToolResult<Record<string, unknown>>>;
|
|
17
17
|
|
|
18
|
-
export
|
|
18
|
+
export interface RejectedExecutionMetrics {
|
|
19
|
+
elapsedMs: number;
|
|
20
|
+
chars: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type RejectedExecutionHandler = (toolCallId: string, metrics: RejectedExecutionMetrics) => void;
|
|
24
|
+
|
|
25
|
+
export function wrapExecuteWithMetrics(execute: ExecuteFn, onRejected?: RejectedExecutionHandler): ExecuteFn {
|
|
19
26
|
return async (tid, params, sig, upd, ctx) => {
|
|
20
27
|
const start = performance.now();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
try {
|
|
29
|
+
const result = await execute(tid, params, sig, upd, ctx);
|
|
30
|
+
const elapsedMs = performance.now() - start;
|
|
31
|
+
const details = (result.details ?? {}) as Record<string, unknown>;
|
|
32
|
+
details[ELAPSED_KEY] = elapsedMs;
|
|
33
|
+
details[CHARS_KEY] = getOutputCharCount(result);
|
|
34
|
+
(result as { details: Record<string, unknown> }).details = details;
|
|
35
|
+
return result;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
onRejected?.(tid, {
|
|
38
|
+
elapsedMs: performance.now() - start,
|
|
39
|
+
chars: getErrorCharCount(error),
|
|
40
|
+
});
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
28
43
|
};
|
|
29
44
|
}
|
|
30
45
|
|
|
46
|
+
function getErrorCharCount(error: unknown): number {
|
|
47
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
48
|
+
return text.replace(/\r/g, "").length;
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
function getOutputCharCount(result: AgentToolResult<unknown>): number {
|
|
32
52
|
const content = result.content;
|
|
33
53
|
if (!Array.isArray(content)) return 0;
|