@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 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.0",
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
- const width = termW();
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
- const stderrWithColumns = process.stderr as NodeJS.WriteStream & { columns?: number };
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.stdout.columns || stderrWithColumns.columns || Number.parseInt(process.env.COLUMNS ?? "", 10) || 200;
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
- pi.registerTool({
1251
+ if (isToolEnabled("read")) {
1252
+ pi.registerTool({
1115
1253
  ...origRead,
1116
1254
  name: "read",
1117
1255
 
1118
- async execute(
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 key = `read:${d.filePath}:${d.offset}:${d.lineCount}:${termW()}`;
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 info = `${FG_DIM}${d.lineCount} lines${RST}`;
1199
- ctx.state._rt = fillToolBackground(` ${info}`);
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 execute(
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 execute(
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 execute(
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 execute(
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 key = `grep:${d.pattern}:${d.matchCount}:${termW()}`;
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 info = `${FG_DIM}${d.matchCount} matches${RST}`;
1602
- ctx.state._gt = fillToolBackground(` ${info}`);
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 execute(
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 info = `${FG_DIM}${d.matchCount} matches${RST}`;
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(80);
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(20);
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) -----------------------------------