@heyhuynhgiabuu/pi-pretty 0.6.26 → 0.6.28
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 +25 -0
- package/README.md +34 -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 +70 -24
- 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 +79 -32
- 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 +11 -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 +97 -31
- package/dist/tools/read.js.map +1 -1
- package/dist/types.d.ts +13 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/working-indicator.d.ts +39 -31
- package/dist/working-indicator.d.ts.map +1 -1
- package/dist/working-indicator.js +114 -87
- package/dist/working-indicator.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +51 -0
- package/src/fff.ts +158 -29
- package/src/index.ts +77 -24
- package/src/prompt-editor.ts +102 -0
- package/src/render.ts +30 -11
- package/src/tools/bash.ts +100 -34
- package/src/tools/find.ts +39 -17
- package/src/tools/grep.ts +26 -15
- package/src/tools/ls.ts +20 -10
- package/src/tools/metrics.ts +28 -8
- package/src/tools/read.ts +126 -31
- package/src/types.ts +14 -4
- package/src/working-indicator.ts +123 -95
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,
|
|
@@ -27,29 +84,22 @@ export function registerBashTool(
|
|
|
27
84
|
: "Execute shell commands. For text search: `rg -n`.",
|
|
28
85
|
promptSnippet: "Execute commands via bash. For text search: `rg -n`.",
|
|
29
86
|
promptGuidelines: [
|
|
30
|
-
|
|
31
|
-
|
|
87
|
+
// Re-registering by name drops the host's built-in bash guidelines; keep them.
|
|
88
|
+
...(sdkTool.promptGuidelines ?? []),
|
|
89
|
+
"rg skips .gitignored and hidden files by default. On no results use `--hidden` for dotfiles (add `-g '!.git'`), `--no-ignore` for ignored files, or name the path directly; `-u` = `--no-ignore`, `-uu` adds hidden.",
|
|
90
|
+
"Quote rg patterns: `rg -n 'foo|bar'`. `|` is alternation, `\\|` is a literal pipe (unlike GNU grep); use `-F` for literal text.",
|
|
91
|
+
"Keep output small: `-l` lists files only, `-m N` caps matches per file.",
|
|
32
92
|
],
|
|
33
93
|
parameters: sdkTool.parameters,
|
|
94
|
+
constrainedSampling: sdkTool.constrainedSampling,
|
|
34
95
|
renderShell: "self",
|
|
35
96
|
|
|
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
|
-
}),
|
|
97
|
+
execute: wrapExecuteWithMetrics(async (tid, params, sig, upd, ctx: ExtensionContext) => {
|
|
98
|
+
// Let the host derive the tool-error flag from the rejected execution.
|
|
99
|
+
// AgentToolResult has no portable `isError` field, so converting a bash
|
|
100
|
+
// failure into a successful-looking result hides the failure from the model.
|
|
101
|
+
return (await sdkTool.execute(tid, params, sig, upd, ctx)) as Result;
|
|
102
|
+
}, rememberRejectedBashMetrics),
|
|
53
103
|
|
|
54
104
|
renderCall(args: any, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
55
105
|
resolveBaseBackground(theme);
|
|
@@ -65,9 +115,14 @@ export function registerBashTool(
|
|
|
65
115
|
? `${rawCmd.slice(0, Math.max(1, headerBudget))}…`
|
|
66
116
|
: rawCmd;
|
|
67
117
|
const commandLabel = theme.fg(ctx.isError ? "error" : "toolTitle", theme.bold(`$ ${cmd}`));
|
|
68
|
-
|
|
69
|
-
fillToolBackground(
|
|
70
|
-
|
|
118
|
+
const renderTitle = (suffix = ""): string =>
|
|
119
|
+
fillToolBackground(
|
|
120
|
+
`\n${TOOL_RESULT_INDENT}${commandLabel}${t}${suffix}\n`,
|
|
121
|
+
undefined,
|
|
122
|
+
ctx.expanded ? undefined : tw,
|
|
123
|
+
);
|
|
124
|
+
rememberToolTitle(ctx, text, renderTitle);
|
|
125
|
+
text.setText(renderTitle());
|
|
71
126
|
return text;
|
|
72
127
|
},
|
|
73
128
|
|
|
@@ -75,9 +130,12 @@ export function registerBashTool(
|
|
|
75
130
|
resolveBaseBackground(theme);
|
|
76
131
|
|
|
77
132
|
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
133
|
+
restoreBashResultRender(ctx, text as ComponentLike);
|
|
134
|
+
const rejectedMetrics = takeRejectedBashMetrics(ctx);
|
|
135
|
+
const displayResult = rejectedMetrics ? addRejectedMetrics(result, rejectedMetrics) : result;
|
|
78
136
|
|
|
79
|
-
const details =
|
|
80
|
-
const tc = getText(
|
|
137
|
+
const details = displayResult.details;
|
|
138
|
+
const tc = getText(displayResult);
|
|
81
139
|
const d: BashDetails | undefined =
|
|
82
140
|
(details as BashDetails)?._type === "bashResult"
|
|
83
141
|
? (details as BashDetails)
|
|
@@ -95,29 +153,37 @@ export function registerBashTool(
|
|
|
95
153
|
const cleaned = stripBashExitStatusLine(d.text);
|
|
96
154
|
const output = isErr ? compactErrorLines(cleaned).join("\n") : cleaned;
|
|
97
155
|
const lineCount = output.split("\n").length;
|
|
98
|
-
const info = [
|
|
156
|
+
const info = [
|
|
157
|
+
`${lineCount} lines`,
|
|
158
|
+
renderToolDuration(displayResult),
|
|
159
|
+
rejectedMetrics ? formatCharCount(rejectedMetrics.chars) : "",
|
|
160
|
+
!ctx.expanded ? "ctrl+o to expand" : "",
|
|
161
|
+
]
|
|
99
162
|
.filter(Boolean)
|
|
100
163
|
.map((part) => theme.fg("dim", part))
|
|
101
164
|
.join(theme.fg("dim", " · "));
|
|
102
165
|
const header = `${TOOL_RESULT_INDENT}${info}`;
|
|
103
166
|
const rw = termWidth();
|
|
104
167
|
|
|
168
|
+
if (setCollapsedToolTitle(ctx, text, ` ${info}`)) return text;
|
|
169
|
+
|
|
105
170
|
const renderFn = (w: number) => {
|
|
106
|
-
if (!ctx.expanded) return
|
|
107
|
-
if (!output.trim()) return
|
|
171
|
+
if (!ctx.expanded) return fillToolBody(header, undefined, w);
|
|
172
|
+
if (!output.trim()) return fillToolBody(header, undefined, w);
|
|
108
173
|
const show = output.split("\n");
|
|
109
174
|
const out = [header, "", ...show.map((line: string) => `${TOOL_RESULT_INDENT}${line}`)];
|
|
110
|
-
return
|
|
175
|
+
return fillToolBody(out.join("\n"), undefined, w);
|
|
111
176
|
};
|
|
112
177
|
|
|
113
178
|
text.setText(renderFn(rw));
|
|
114
179
|
const baseRender =
|
|
115
180
|
typeof (text as ComponentLike).render === "function" ? (text as ComponentLike).render.bind(text) : null;
|
|
116
181
|
if (baseRender) {
|
|
182
|
+
ctx.state[BASH_RESULT_RENDER_KEY] = baseRender;
|
|
117
183
|
let key: string | undefined;
|
|
118
184
|
(text as unknown as Record<string, unknown>).render = (w: number) => {
|
|
119
185
|
const width = Math.max(1, Math.floor(w || termWidth()));
|
|
120
|
-
const k = `bash:${ctx.expanded ? "1" : "0"}:${width}:${d.exitCode ?? "killed"}:${output.length}:${renderToolDuration(
|
|
186
|
+
const k = `bash:${ctx.expanded ? "1" : "0"}:${width}:${d.exitCode ?? "killed"}:${output.length}:${renderToolDuration(displayResult)}`;
|
|
121
187
|
if (key !== k) {
|
|
122
188
|
text.setText(renderFn(width));
|
|
123
189
|
key = k;
|
|
@@ -132,9 +198,9 @@ export function registerBashTool(
|
|
|
132
198
|
text.setText(renderToolError(tc || "Error", theme));
|
|
133
199
|
return text;
|
|
134
200
|
}
|
|
135
|
-
const fc =
|
|
201
|
+
const fc = displayResult.content?.[0];
|
|
136
202
|
text.setText(
|
|
137
|
-
|
|
203
|
+
fillToolBody(
|
|
138
204
|
`${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
|
|
139
205
|
),
|
|
140
206
|
);
|
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
|
-
`${TOOL_RESULT_INDENT}${theme.fg("dim", `${d.matchCount} files`)}${duration ? `${FG_DIM}· ${duration}${RST}` : ""}\n${rendered}${noticeStr}
|
|
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(`${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";
|
|
@@ -25,6 +33,7 @@ export function registerLsTool(
|
|
|
25
33
|
label: "List",
|
|
26
34
|
description: sdkTool.description ?? "List directory contents",
|
|
27
35
|
parameters: sdkTool.parameters,
|
|
36
|
+
promptSnippet: sdkTool.promptSnippet,
|
|
28
37
|
renderShell: "self",
|
|
29
38
|
|
|
30
39
|
execute: wrapExecuteWithMetrics(async (tid, params, sig, _upd, ctx: ExtensionContext) => {
|
|
@@ -51,7 +60,10 @@ export function registerLsTool(
|
|
|
51
60
|
let out = theme.fg("toolTitle", theme.bold("ls"));
|
|
52
61
|
if (path) out += ` ${theme.fg("accent", path)}`;
|
|
53
62
|
if (limit !== undefined && limit !== null) out += theme.fg("toolOutput", ` (limit ${limit})`);
|
|
54
|
-
|
|
63
|
+
const renderTitle = (suffix = ""): string =>
|
|
64
|
+
fillToolBackground(`\n${TOOL_RESULT_INDENT}${out}${suffix}\n`, ctx.isError ? BG_ERROR : undefined);
|
|
65
|
+
rememberToolTitle(ctx, text, renderTitle);
|
|
66
|
+
text.setText(renderTitle());
|
|
55
67
|
return text;
|
|
56
68
|
},
|
|
57
69
|
|
|
@@ -66,11 +78,9 @@ export function registerLsTool(
|
|
|
66
78
|
const d = result.details as LsDetails | undefined;
|
|
67
79
|
if (d?._type === "lsResult" && d.text) {
|
|
68
80
|
if (!ctx.expanded) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
),
|
|
73
|
-
);
|
|
81
|
+
const summary = `${FG_DIM}${d.entryCount} entries — ctrl+o to expand${RST}${renderToolMetrics(result)}`;
|
|
82
|
+
if (setCollapsedToolTitle(ctx, text, ` ${summary}`)) return text;
|
|
83
|
+
text.setText(fillToolBody(`${TOOL_RESULT_INDENT}${summary}`));
|
|
74
84
|
return text;
|
|
75
85
|
}
|
|
76
86
|
const rendered = renderTree(d.text, d.path)
|
|
@@ -78,15 +88,15 @@ export function registerLsTool(
|
|
|
78
88
|
.map((l) => `${TOOL_RESULT_INDENT}${l}`)
|
|
79
89
|
.join("\n");
|
|
80
90
|
text.setText(
|
|
81
|
-
|
|
82
|
-
`${TOOL_RESULT_INDENT}${FG_DIM}${d.entryCount} entries${RST}${renderToolMetrics(result)}\n${rendered}
|
|
91
|
+
fillToolBody(
|
|
92
|
+
`${TOOL_RESULT_INDENT}${FG_DIM}${d.entryCount} entries${RST}${renderToolMetrics(result)}\n${rendered}`,
|
|
83
93
|
),
|
|
84
94
|
);
|
|
85
95
|
return text;
|
|
86
96
|
}
|
|
87
97
|
const fc = result.content?.[0];
|
|
88
98
|
text.setText(
|
|
89
|
-
|
|
99
|
+
fillToolBody(
|
|
90
100
|
`${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
|
|
91
101
|
),
|
|
92
102
|
);
|
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;
|