@heyhuynhgiabuu/pi-pretty 0.5.0 → 0.5.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/README.md +1 -0
- package/package.json +1 -1
- package/src/index.ts +189 -45
- package/test/bash-rendering.test.ts +2 -2
- package/test/fff-integration.test.ts +38 -0
package/README.md
CHANGED
|
@@ -140,6 +140,7 @@ Optional environment variables:
|
|
|
140
140
|
- `PRETTY_MAX_PREVIEW_LINES` (default: `80`)
|
|
141
141
|
- `PRETTY_CACHE_LIMIT` (default: `128`)
|
|
142
142
|
- `PRETTY_ICONS` (`nerd` by default, set to `none` to disable icons)
|
|
143
|
+
- `PRETTY_DISABLE_TOOLS` — comma-separated list of tool names to skip during registration (e.g. `read,grep`). Useful when another extension already owns one of these tool names. All tools (read, bash, ls, find, grep, multi_grep) are registered by default.
|
|
143
144
|
|
|
144
145
|
## Development
|
|
145
146
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyhuynhgiabuu/pi-pretty",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Pretty terminal output for pi — syntax-highlighted file reads, colored bash output, tree-view directory listings, and more.",
|
|
5
5
|
"author": "huynhgiabuu",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -185,8 +185,8 @@ function preserveToolBackground(ansi: string, bg: string): string {
|
|
|
185
185
|
});
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
function fillToolBackground(text: string, bg = BG_BASE): string {
|
|
189
|
-
|
|
188
|
+
function fillToolBackground(text: string, bg = BG_BASE, width?: number): string {
|
|
189
|
+
if (width === undefined) width = termW();
|
|
190
190
|
return text
|
|
191
191
|
.split("\n")
|
|
192
192
|
.map((line) => {
|
|
@@ -199,9 +199,16 @@ function fillToolBackground(text: string, bg = BG_BASE): string {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
function termW(): number {
|
|
202
|
-
|
|
202
|
+
// When process.stdout.columns is available (real terminal or compositor override),
|
|
203
|
+
// use it directly — the TUI/compositor already provides the exact content width.
|
|
204
|
+
// The -4 safety margin only applies to fallback values (stderr.columns, env.COLUMNS, default).
|
|
205
|
+
if (process.stdout.columns) {
|
|
206
|
+
return Math.max(1, Math.min(process.stdout.columns, 210));
|
|
207
|
+
}
|
|
203
208
|
const raw =
|
|
204
|
-
process.
|
|
209
|
+
(process.stderr as NodeJS.WriteStream & { columns?: number }).columns ||
|
|
210
|
+
Number.parseInt(process.env.COLUMNS ?? "", 10) ||
|
|
211
|
+
200;
|
|
205
212
|
return Math.max(1, Math.min(raw - 4, 210));
|
|
206
213
|
}
|
|
207
214
|
|
|
@@ -605,6 +612,7 @@ async function renderFileContent(
|
|
|
605
612
|
filePath: string,
|
|
606
613
|
offset = 1,
|
|
607
614
|
maxLines = MAX_PREVIEW_LINES,
|
|
615
|
+
width?: number,
|
|
608
616
|
): Promise<string> {
|
|
609
617
|
const normalizedContent = normalizeLineEndings(content);
|
|
610
618
|
const lines = normalizedContent.split("\n");
|
|
@@ -613,7 +621,7 @@ async function renderFileContent(
|
|
|
613
621
|
const lg = lang(filePath);
|
|
614
622
|
const hl = await hlBlock(show.join("\n"), lg);
|
|
615
623
|
|
|
616
|
-
const tw = termW();
|
|
624
|
+
const tw = width ?? termW();
|
|
617
625
|
const startLine = offset;
|
|
618
626
|
const endLine = startLine + show.length - 1;
|
|
619
627
|
const nw = Math.max(3, String(endLine).length);
|
|
@@ -780,6 +788,77 @@ async function renderGrepResults(text: string, pattern: string): Promise<string>
|
|
|
780
788
|
return out.join("\n");
|
|
781
789
|
}
|
|
782
790
|
|
|
791
|
+
// ---------------------------------------------------------------------------
|
|
792
|
+
// Tool metrics — elapsed time + output size
|
|
793
|
+
// pi-droid-styling-inspired: wrap execute to record performance, display in footer.
|
|
794
|
+
// ---------------------------------------------------------------------------
|
|
795
|
+
|
|
796
|
+
const ELAPSED_KEY = "__prettyElapsedMs";
|
|
797
|
+
const CHARS_KEY = "__prettyOutputChars";
|
|
798
|
+
|
|
799
|
+
/** Format milliseconds for display. */
|
|
800
|
+
function formatElapsedMs(ms: number | undefined): string {
|
|
801
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return "";
|
|
802
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
803
|
+
const s = ms / 1000;
|
|
804
|
+
return s < 10 ? `${s.toFixed(1)}s` : `${Math.round(s)}s`;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** Format character count for display. */
|
|
808
|
+
function formatCharCount(chars: number | undefined): string {
|
|
809
|
+
if (typeof chars !== "number" || !Number.isFinite(chars) || chars <= 0) return "";
|
|
810
|
+
if (chars < 1000) return `${chars} chars`;
|
|
811
|
+
if (chars < 10_000) return `${(chars / 1000).toFixed(1)}k chars`;
|
|
812
|
+
return `${Math.round(chars / 1000)}k chars`;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** Compute text output length from a tool result. */
|
|
816
|
+
function getOutputCharCount(result: ToolResultLike): number {
|
|
817
|
+
const content = result.content;
|
|
818
|
+
if (!Array.isArray(content)) return 0;
|
|
819
|
+
let length = 0;
|
|
820
|
+
for (const block of content) {
|
|
821
|
+
if (block.type !== "text") continue;
|
|
822
|
+
length += String(block.text ?? "").replace(/\r/g, "").length;
|
|
823
|
+
}
|
|
824
|
+
return length;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Wrap a tool's execute function to measure elapsed time and output size.
|
|
829
|
+
* Annotates result.details with __prettyElapsedMs and __prettyOutputChars.
|
|
830
|
+
*/
|
|
831
|
+
function wrapExecuteWithMetrics<TParams, TDetails>(
|
|
832
|
+
execute: (...args: any[]) => Promise<ToolResultLike<TDetails>>,
|
|
833
|
+
): ToolExecutor<TParams, TDetails> {
|
|
834
|
+
return async (
|
|
835
|
+
tid: string,
|
|
836
|
+
params: TParams,
|
|
837
|
+
sig?: AbortSignal,
|
|
838
|
+
onUpdate?: AgentToolUpdateCallback<TDetails | undefined>,
|
|
839
|
+
ctx?: ExtensionContext,
|
|
840
|
+
) => {
|
|
841
|
+
const start = performance.now();
|
|
842
|
+
const result = await execute(tid, params, sig, onUpdate, ctx);
|
|
843
|
+
const elapsedMs = performance.now() - start;
|
|
844
|
+
const details = (result.details ?? {}) as Record<string, unknown>;
|
|
845
|
+
details[ELAPSED_KEY] = elapsedMs;
|
|
846
|
+
details[CHARS_KEY] = getOutputCharCount(result);
|
|
847
|
+
(result as { details: Record<string, unknown> }).details = details;
|
|
848
|
+
return result;
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** Render a tool metrics line: "3.2s · 14.2k chars" */
|
|
853
|
+
function renderToolMetrics(result: ToolResultLike): string {
|
|
854
|
+
const details = result.details as Record<string, unknown> | undefined;
|
|
855
|
+
if (!details) return "";
|
|
856
|
+
const elapsed = formatElapsedMs(details[ELAPSED_KEY] as number | undefined);
|
|
857
|
+
const chars = formatCharCount(details[CHARS_KEY] as number | undefined);
|
|
858
|
+
if (!elapsed && !chars) return "";
|
|
859
|
+
return `${FG_DIM}· ${[elapsed, chars].filter(Boolean).join(" · ")}${RST}`;
|
|
860
|
+
}
|
|
861
|
+
|
|
783
862
|
// ---------------------------------------------------------------------------
|
|
784
863
|
// FFF integration (optional) — Fast File Finder with frecency & SIMD search
|
|
785
864
|
//
|
|
@@ -1035,6 +1114,64 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1035
1114
|
const sp = (p: string) => shortPath(cwd, home, p);
|
|
1036
1115
|
const multiGrepRipgrepFallback = deps?.multiGrepRipgrepFallback ?? runMultiGrepRipgrepFallback;
|
|
1037
1116
|
|
|
1117
|
+
// Parse PRETTY_DISABLE_TOOLS — comma-separated tool names to skip
|
|
1118
|
+
const disabledTools = new Set(
|
|
1119
|
+
(process.env.PRETTY_DISABLE_TOOLS ?? "")
|
|
1120
|
+
.split(",")
|
|
1121
|
+
.map((s) => s.trim().toLowerCase())
|
|
1122
|
+
.filter(Boolean),
|
|
1123
|
+
);
|
|
1124
|
+
function isToolEnabled(name: string): boolean {
|
|
1125
|
+
return !disabledTools.has(name.toLowerCase());
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// ===================================================================
|
|
1129
|
+
// Generic renderResult for custom tools (no custom renderer)
|
|
1130
|
+
// ===================================================================
|
|
1131
|
+
|
|
1132
|
+
const origRegisterTool = pi.registerTool.bind(pi);
|
|
1133
|
+
pi.registerTool = (tool: any) => {
|
|
1134
|
+
if (!tool.renderResult && !tool.renderCall) {
|
|
1135
|
+
const toolName = tool.label ?? tool.name ?? "tool";
|
|
1136
|
+
tool.renderResult = (result: any, _opt: unknown, theme: ThemeLike, ctx: RenderContextLike) => {
|
|
1137
|
+
resolveBaseBackground(theme);
|
|
1138
|
+
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1139
|
+
|
|
1140
|
+
if (ctx.isError) {
|
|
1141
|
+
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
1142
|
+
return text;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const content = getTextContent(result);
|
|
1146
|
+
if (content) {
|
|
1147
|
+
const renderWidth = termW();
|
|
1148
|
+
const lines = content.split("\n");
|
|
1149
|
+
const maxShow = ctx.expanded ? lines.length : Math.min(lines.length, MAX_PREVIEW_LINES);
|
|
1150
|
+
const preview = lines.slice(0, maxShow).join("\n");
|
|
1151
|
+
const more = lines.length > maxShow ? `\n${FG_DIM}... ${lines.length - maxShow} more lines${RST}` : "";
|
|
1152
|
+
const metrics = renderToolMetrics(result);
|
|
1153
|
+
text.setText(
|
|
1154
|
+
fillToolBackground(` ${preview}${more}${metrics ? `\n ${metrics}` : ""}`, undefined, renderWidth),
|
|
1155
|
+
);
|
|
1156
|
+
} else {
|
|
1157
|
+
text.setText(fillToolBackground(` ${theme.fg("dim", "(no text output)")}`));
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
return text;
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
tool.renderCall = (args: any, theme: ThemeLike, ctx: RenderContextLike) => {
|
|
1164
|
+
resolveBaseBackground(theme);
|
|
1165
|
+
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1166
|
+
text.setText(
|
|
1167
|
+
fillToolBackground(`${theme.fg("toolTitle", theme.bold(toolName))}`),
|
|
1168
|
+
);
|
|
1169
|
+
return text;
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
origRegisterTool(tool);
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1038
1175
|
// ===================================================================
|
|
1039
1176
|
// FFF initialization (optional — graceful fallback to SDK)
|
|
1040
1177
|
// ===================================================================
|
|
@@ -1111,17 +1248,18 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1111
1248
|
|
|
1112
1249
|
const origRead = createReadTool(cwd);
|
|
1113
1250
|
|
|
1114
|
-
|
|
1251
|
+
if (isToolEnabled("read")) {
|
|
1252
|
+
pi.registerTool({
|
|
1115
1253
|
...origRead,
|
|
1116
1254
|
name: "read",
|
|
1117
1255
|
|
|
1118
|
-
async
|
|
1256
|
+
execute: wrapExecuteWithMetrics(async (
|
|
1119
1257
|
tid: string,
|
|
1120
1258
|
params: ReadParams,
|
|
1121
1259
|
sig: AbortSignal | undefined,
|
|
1122
1260
|
upd: AgentToolUpdateCallback<unknown> | undefined,
|
|
1123
1261
|
ctx: ExtensionContext,
|
|
1124
|
-
) {
|
|
1262
|
+
) => {
|
|
1125
1263
|
const result = (await origRead.execute(tid, params, sig, upd, ctx)) as ToolResultLike;
|
|
1126
1264
|
|
|
1127
1265
|
const fp = params.path ?? "";
|
|
@@ -1152,7 +1290,7 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1152
1290
|
}
|
|
1153
1291
|
|
|
1154
1292
|
return result;
|
|
1155
|
-
},
|
|
1293
|
+
}),
|
|
1156
1294
|
|
|
1157
1295
|
renderCall(args: ReadParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
1158
1296
|
resolveBaseBackground(theme);
|
|
@@ -1192,22 +1330,24 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1192
1330
|
}
|
|
1193
1331
|
|
|
1194
1332
|
if (d?._type === "readFile" && d.content) {
|
|
1195
|
-
const
|
|
1333
|
+
const renderWidth = termW();
|
|
1334
|
+
const key = `read:${d.filePath}:${d.offset}:${d.lineCount}:${renderWidth}`;
|
|
1196
1335
|
if (ctx.state._rk !== key) {
|
|
1197
1336
|
ctx.state._rk = key;
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1337
|
+
const metrics = renderToolMetrics(result);
|
|
1338
|
+
const info = `${FG_DIM}${d.lineCount} lines${RST}${metrics}`;
|
|
1339
|
+
ctx.state._rt = fillToolBackground(` ${info}`, undefined, renderWidth);
|
|
1200
1340
|
|
|
1201
1341
|
const maxShow = ctx.expanded ? d.lineCount : MAX_PREVIEW_LINES;
|
|
1202
|
-
renderFileContent(d.content, d.filePath, d.offset, maxShow)
|
|
1342
|
+
renderFileContent(d.content, d.filePath, d.offset, maxShow, renderWidth)
|
|
1203
1343
|
.then((rendered: string) => {
|
|
1204
1344
|
if (ctx.state._rk !== key) return;
|
|
1205
|
-
ctx.state._rt = fillToolBackground(` ${info}\n${rendered}
|
|
1345
|
+
ctx.state._rt = fillToolBackground(` ${info}\n${rendered}`, undefined, renderWidth);
|
|
1206
1346
|
ctx.invalidate();
|
|
1207
1347
|
})
|
|
1208
1348
|
.catch(() => {});
|
|
1209
1349
|
}
|
|
1210
|
-
text.setText(ctx.state._rt ?? fillToolBackground(` ${FG_DIM}${d.lineCount} lines${RST}
|
|
1350
|
+
text.setText(ctx.state._rt ?? fillToolBackground(` ${FG_DIM}${d.lineCount} lines${RST}${renderToolMetrics(result)}`, undefined, renderWidth));
|
|
1211
1351
|
return text;
|
|
1212
1352
|
}
|
|
1213
1353
|
|
|
@@ -1218,25 +1358,26 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1218
1358
|
return text;
|
|
1219
1359
|
},
|
|
1220
1360
|
});
|
|
1361
|
+
}
|
|
1221
1362
|
|
|
1222
1363
|
// ===================================================================
|
|
1223
1364
|
// bash — colored exit status
|
|
1224
1365
|
// ===================================================================
|
|
1225
1366
|
|
|
1226
|
-
if (createBashTool) {
|
|
1367
|
+
if (createBashTool && isToolEnabled("bash")) {
|
|
1227
1368
|
const origBash = createBashTool(cwd);
|
|
1228
1369
|
|
|
1229
1370
|
pi.registerTool({
|
|
1230
1371
|
...origBash,
|
|
1231
1372
|
name: "bash",
|
|
1232
1373
|
|
|
1233
|
-
async
|
|
1374
|
+
execute: wrapExecuteWithMetrics(async (
|
|
1234
1375
|
tid: string,
|
|
1235
1376
|
params: BashParams,
|
|
1236
1377
|
sig: AbortSignal | undefined,
|
|
1237
1378
|
upd: AgentToolUpdateCallback<unknown> | undefined,
|
|
1238
1379
|
ctx: ExtensionContext,
|
|
1239
|
-
) {
|
|
1380
|
+
) => {
|
|
1240
1381
|
const result = (await origBash.execute(tid, params, sig, upd, ctx)) as ToolResultLike;
|
|
1241
1382
|
const textContent = getTextContent(result);
|
|
1242
1383
|
|
|
@@ -1257,7 +1398,7 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1257
1398
|
});
|
|
1258
1399
|
|
|
1259
1400
|
return result;
|
|
1260
|
-
},
|
|
1401
|
+
}),
|
|
1261
1402
|
|
|
1262
1403
|
renderCall(args: BashParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
1263
1404
|
resolveBaseBackground(theme);
|
|
@@ -1287,7 +1428,7 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1287
1428
|
const { summary } = renderBashOutput(d.text, d.exitCode);
|
|
1288
1429
|
const lines = d.text.split("\n");
|
|
1289
1430
|
const lineCount = lines.length;
|
|
1290
|
-
const lineInfo = lineCount > 1 ? ` ${FG_DIM}(${lineCount} lines)${RST}` :
|
|
1431
|
+
const lineInfo = lineCount > 1 ? ` ${FG_DIM}(${lineCount} lines)${RST} ${renderToolMetrics(result)}` : ` ${renderToolMetrics(result)}`;
|
|
1291
1432
|
const header = ` ${summary}${lineInfo}`;
|
|
1292
1433
|
|
|
1293
1434
|
if (d.text.trim()) {
|
|
@@ -1321,20 +1462,20 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1321
1462
|
// ls — tree view with icons
|
|
1322
1463
|
// ===================================================================
|
|
1323
1464
|
|
|
1324
|
-
if (createLsTool) {
|
|
1465
|
+
if (createLsTool && isToolEnabled("ls")) {
|
|
1325
1466
|
const origLs = createLsTool(cwd);
|
|
1326
1467
|
|
|
1327
1468
|
pi.registerTool({
|
|
1328
1469
|
...origLs,
|
|
1329
1470
|
name: "ls",
|
|
1330
1471
|
|
|
1331
|
-
async
|
|
1472
|
+
execute: wrapExecuteWithMetrics(async (
|
|
1332
1473
|
tid: string,
|
|
1333
1474
|
params: LsParams,
|
|
1334
1475
|
sig: AbortSignal | undefined,
|
|
1335
1476
|
upd: AgentToolUpdateCallback<unknown> | undefined,
|
|
1336
1477
|
ctx: ExtensionContext,
|
|
1337
|
-
) {
|
|
1478
|
+
) => {
|
|
1338
1479
|
const result = (await origLs.execute(tid, params, sig, upd, ctx)) as ToolResultLike;
|
|
1339
1480
|
const textContent = getTextContent(result);
|
|
1340
1481
|
const fp = params.path ?? cwd;
|
|
@@ -1348,7 +1489,7 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1348
1489
|
});
|
|
1349
1490
|
|
|
1350
1491
|
return result;
|
|
1351
|
-
},
|
|
1492
|
+
}),
|
|
1352
1493
|
|
|
1353
1494
|
renderCall(args: LsParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
1354
1495
|
resolveBaseBackground(theme);
|
|
@@ -1370,7 +1511,7 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1370
1511
|
const d = result.details as RenderDetails | undefined;
|
|
1371
1512
|
if (d?._type === "lsResult" && d.text) {
|
|
1372
1513
|
const tree = renderTree(d.text, d.path);
|
|
1373
|
-
const info = `${FG_DIM}${d.entryCount} entries${RST}`;
|
|
1514
|
+
const info = `${FG_DIM}${d.entryCount} entries${RST}${renderToolMetrics(result)}`;
|
|
1374
1515
|
text.setText(fillToolBackground(` ${info}\n${tree}`));
|
|
1375
1516
|
return text;
|
|
1376
1517
|
}
|
|
@@ -1387,20 +1528,20 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1387
1528
|
// find — grouped file list with icons
|
|
1388
1529
|
// ===================================================================
|
|
1389
1530
|
|
|
1390
|
-
if (createFindTool) {
|
|
1531
|
+
if (createFindTool && isToolEnabled("find")) {
|
|
1391
1532
|
const origFind = createFindTool(cwd);
|
|
1392
1533
|
|
|
1393
1534
|
pi.registerTool({
|
|
1394
1535
|
...origFind,
|
|
1395
1536
|
name: "find",
|
|
1396
1537
|
|
|
1397
|
-
async
|
|
1538
|
+
execute: wrapExecuteWithMetrics(async (
|
|
1398
1539
|
tid: string,
|
|
1399
1540
|
params: FindParams,
|
|
1400
1541
|
sig: AbortSignal | undefined,
|
|
1401
1542
|
upd: unknown,
|
|
1402
1543
|
ctx: ExtensionContext,
|
|
1403
|
-
) {
|
|
1544
|
+
) => {
|
|
1404
1545
|
// Try FFF first (frecency-ranked, SIMD-accelerated)
|
|
1405
1546
|
if (_fffFinder && !_fffFinder.isDestroyed) {
|
|
1406
1547
|
try {
|
|
@@ -1443,7 +1584,7 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1443
1584
|
});
|
|
1444
1585
|
|
|
1445
1586
|
return result;
|
|
1446
|
-
},
|
|
1587
|
+
}),
|
|
1447
1588
|
|
|
1448
1589
|
renderCall(args: FindParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
1449
1590
|
resolveBaseBackground(theme);
|
|
@@ -1473,7 +1614,7 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1473
1614
|
const d = result.details;
|
|
1474
1615
|
if (d?._type === "findResult" && d.text) {
|
|
1475
1616
|
const rendered = renderFindResults(d.text);
|
|
1476
|
-
const info = `${FG_DIM}${d.matchCount} files${RST}`;
|
|
1617
|
+
const info = `${FG_DIM}${d.matchCount} files${RST}${renderToolMetrics(result)}`;
|
|
1477
1618
|
text.setText(fillToolBackground(` ${info}\n${rendered}`));
|
|
1478
1619
|
return text;
|
|
1479
1620
|
}
|
|
@@ -1490,20 +1631,20 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1490
1631
|
// grep — highlighted matches with line numbers
|
|
1491
1632
|
// ===================================================================
|
|
1492
1633
|
|
|
1493
|
-
if (createGrepTool) {
|
|
1634
|
+
if (createGrepTool && isToolEnabled("grep")) {
|
|
1494
1635
|
const origGrep = createGrepTool(cwd);
|
|
1495
1636
|
|
|
1496
1637
|
pi.registerTool({
|
|
1497
1638
|
...origGrep,
|
|
1498
1639
|
name: "grep",
|
|
1499
1640
|
|
|
1500
|
-
async
|
|
1641
|
+
execute: wrapExecuteWithMetrics(async (
|
|
1501
1642
|
tid: string,
|
|
1502
1643
|
params: GrepParams,
|
|
1503
1644
|
sig: AbortSignal | undefined,
|
|
1504
1645
|
upd: unknown,
|
|
1505
1646
|
ctx: ExtensionContext,
|
|
1506
|
-
) {
|
|
1647
|
+
) => {
|
|
1507
1648
|
// Try FFF first (SIMD-accelerated, frecency-ranked).
|
|
1508
1649
|
// FFF 0.5.2 can abort the process when path/glob constraints meet
|
|
1509
1650
|
// Unicode filenames, so constrained searches use the SDK fallback.
|
|
@@ -1563,7 +1704,7 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1563
1704
|
});
|
|
1564
1705
|
|
|
1565
1706
|
return result;
|
|
1566
|
-
},
|
|
1707
|
+
}),
|
|
1567
1708
|
|
|
1568
1709
|
renderCall(args: GrepParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
1569
1710
|
resolveBaseBackground(theme);
|
|
@@ -1595,21 +1736,23 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1595
1736
|
|
|
1596
1737
|
const d = result.details;
|
|
1597
1738
|
if (d?._type === "grepResult" && d.text) {
|
|
1598
|
-
const
|
|
1739
|
+
const renderWidth = termW();
|
|
1740
|
+
const key = `grep:${d.pattern}:${d.matchCount}:${renderWidth}`;
|
|
1599
1741
|
if (ctx.state._gk !== key) {
|
|
1600
1742
|
ctx.state._gk = key;
|
|
1601
|
-
const
|
|
1602
|
-
|
|
1743
|
+
const metrics = renderToolMetrics(result);
|
|
1744
|
+
const info = `${FG_DIM}${d.matchCount} matches${RST}${metrics}`;
|
|
1745
|
+
ctx.state._gt = fillToolBackground(` ${info}`, undefined, renderWidth);
|
|
1603
1746
|
|
|
1604
1747
|
renderGrepResults(d.text, d.pattern)
|
|
1605
1748
|
.then((rendered: string) => {
|
|
1606
1749
|
if (ctx.state._gk !== key) return;
|
|
1607
|
-
ctx.state._gt = fillToolBackground(` ${info}\n${rendered}
|
|
1750
|
+
ctx.state._gt = fillToolBackground(` ${info}\n${rendered}`, undefined, renderWidth);
|
|
1608
1751
|
ctx.invalidate();
|
|
1609
1752
|
})
|
|
1610
1753
|
.catch(() => {});
|
|
1611
1754
|
}
|
|
1612
|
-
text.setText(ctx.state._gt ?? fillToolBackground(` ${FG_DIM}${d.matchCount} matches${RST}
|
|
1755
|
+
text.setText(ctx.state._gt ?? fillToolBackground(` ${FG_DIM}${d.matchCount} matches${RST}${renderToolMetrics(result)}`, undefined, renderWidth));
|
|
1613
1756
|
return text;
|
|
1614
1757
|
}
|
|
1615
1758
|
|
|
@@ -1626,7 +1769,7 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1626
1769
|
// SDK grep fallback otherwise)
|
|
1627
1770
|
// ===================================================================
|
|
1628
1771
|
|
|
1629
|
-
if (_fffModule || createGrepTool) {
|
|
1772
|
+
if ((_fffModule || createGrepTool) && isToolEnabled("multi_grep")) {
|
|
1630
1773
|
const multiGrepFallback = createGrepTool ? createGrepTool(cwd) : null;
|
|
1631
1774
|
|
|
1632
1775
|
pi.registerTool({
|
|
@@ -1676,13 +1819,13 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1676
1819
|
required: ["patterns"],
|
|
1677
1820
|
},
|
|
1678
1821
|
|
|
1679
|
-
async
|
|
1822
|
+
execute: wrapExecuteWithMetrics(async (
|
|
1680
1823
|
tid: string,
|
|
1681
1824
|
params: MultiGrepParams,
|
|
1682
1825
|
sig: AbortSignal | undefined,
|
|
1683
1826
|
upd: unknown,
|
|
1684
1827
|
ctx: ExtensionContext,
|
|
1685
|
-
) {
|
|
1828
|
+
) => {
|
|
1686
1829
|
if (sig?.aborted) return makeTextResult("Aborted", {});
|
|
1687
1830
|
|
|
1688
1831
|
if (!params.patterns || params.patterns.length === 0) {
|
|
@@ -1800,7 +1943,7 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1800
1943
|
const message = getErrorMessage(error);
|
|
1801
1944
|
return makeTextResult(`multi_grep error: ${message}`, { error: message });
|
|
1802
1945
|
}
|
|
1803
|
-
},
|
|
1946
|
+
}),
|
|
1804
1947
|
|
|
1805
1948
|
renderCall(args: MultiGrepParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
1806
1949
|
resolveBaseBackground(theme);
|
|
@@ -1837,7 +1980,8 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1837
1980
|
const key = `mgrep:${d.pattern}:${d.matchCount}:${termW()}`;
|
|
1838
1981
|
if (ctx.state._mgk !== key) {
|
|
1839
1982
|
ctx.state._mgk = key;
|
|
1840
|
-
const
|
|
1983
|
+
const metrics = renderToolMetrics(result);
|
|
1984
|
+
const info = `${FG_DIM}${d.matchCount} matches${RST}${metrics}`;
|
|
1841
1985
|
ctx.state._mgt = ` ${info}`;
|
|
1842
1986
|
|
|
1843
1987
|
renderGrepResults(d.text, d.pattern)
|
|
@@ -1848,7 +1992,7 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
|
|
|
1848
1992
|
})
|
|
1849
1993
|
.catch(() => {});
|
|
1850
1994
|
}
|
|
1851
|
-
text.setText(ctx.state._mgt ?? ` ${FG_DIM}${d.matchCount} matches${RST}`);
|
|
1995
|
+
text.setText(ctx.state._mgt ?? ` ${FG_DIM}${d.matchCount} matches${RST}${renderToolMetrics(result)}`);
|
|
1852
1996
|
return text;
|
|
1853
1997
|
}
|
|
1854
1998
|
|
|
@@ -141,7 +141,7 @@ describe("bash renderCall expansion", () => {
|
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
for (const line of rendered.getText().split("\n")) {
|
|
144
|
-
expect(visibleWidth(line)).toBeLessThanOrEqual(
|
|
144
|
+
expect(visibleWidth(line)).toBeLessThanOrEqual(84);
|
|
145
145
|
}
|
|
146
146
|
});
|
|
147
147
|
});
|
|
@@ -160,7 +160,7 @@ describe("bash renderCall expansion", () => {
|
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
for (const line of rendered.getText().split("\n")) {
|
|
163
|
-
expect(visibleWidth(line)).toBeLessThanOrEqual(
|
|
163
|
+
expect(visibleWidth(line)).toBeLessThanOrEqual(24);
|
|
164
164
|
}
|
|
165
165
|
});
|
|
166
166
|
});
|
|
@@ -373,6 +373,44 @@ describe("piPrettyExtension integration", () => {
|
|
|
373
373
|
expect(events.has("session_start")).toBe(true);
|
|
374
374
|
expect(events.has("session_shutdown")).toBe(true);
|
|
375
375
|
});
|
|
376
|
+
|
|
377
|
+
it("skips tools listed in PRETTY_DISABLE_TOOLS", () => {
|
|
378
|
+
process.env.PRETTY_DISABLE_TOOLS = "read,find";
|
|
379
|
+
load();
|
|
380
|
+
expect(tools.has("read"), "read should be disabled").toBe(false);
|
|
381
|
+
expect(tools.has("find"), "find should be disabled").toBe(false);
|
|
382
|
+
expect(tools.has("bash"), "bash should be enabled").toBe(true);
|
|
383
|
+
expect(tools.has("grep"), "grep should be enabled").toBe(true);
|
|
384
|
+
expect(tools.has("ls"), "ls should be enabled").toBe(true);
|
|
385
|
+
delete process.env.PRETTY_DISABLE_TOOLS;
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("skips multi_grep when listed in PRETTY_DISABLE_TOOLS", () => {
|
|
389
|
+
process.env.PRETTY_DISABLE_TOOLS = "multi_grep";
|
|
390
|
+
load(true);
|
|
391
|
+
expect(tools.has("multi_grep"), "multi_grep should be disabled").toBe(false);
|
|
392
|
+
expect(tools.has("read"), "read should still be enabled").toBe(true);
|
|
393
|
+
delete process.env.PRETTY_DISABLE_TOOLS;
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("handles whitespace in PRETTY_DISABLE_TOOLS", () => {
|
|
397
|
+
process.env.PRETTY_DISABLE_TOOLS = " bash , ls ";
|
|
398
|
+
load();
|
|
399
|
+
expect(tools.has("bash"), "bash should be disabled").toBe(false);
|
|
400
|
+
expect(tools.has("ls"), "ls should be disabled").toBe(false);
|
|
401
|
+
expect(tools.has("read"), "read should be enabled").toBe(true);
|
|
402
|
+
expect(tools.has("grep"), "grep should be enabled").toBe(true);
|
|
403
|
+
delete process.env.PRETTY_DISABLE_TOOLS;
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("empty PRETTY_DISABLE_TOOLS registers all tools", () => {
|
|
407
|
+
process.env.PRETTY_DISABLE_TOOLS = "";
|
|
408
|
+
load();
|
|
409
|
+
for (const n of ["find", "grep", "read", "bash", "ls"]) {
|
|
410
|
+
expect(tools.has(n), `missing: ${n}`).toBe(true);
|
|
411
|
+
}
|
|
412
|
+
delete process.env.PRETTY_DISABLE_TOOLS;
|
|
413
|
+
});
|
|
376
414
|
});
|
|
377
415
|
|
|
378
416
|
// ---- find: SDK fallback (no FFF) -----------------------------------
|