@heyhuynhgiabuu/pi-pretty 0.3.3 → 0.4.1

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/src/index.ts CHANGED
@@ -24,9 +24,23 @@
24
24
  */
25
25
 
26
26
  import * as childProcess from "node:child_process";
27
- import { existsSync, mkdirSync, statSync } from "node:fs";
27
+ import { mkdirSync } from "node:fs";
28
28
  import { basename, dirname, extname, join, relative } from "node:path";
29
29
 
30
+ import type { FileFinder, FileItem, GrepResult, SearchResult } from "@ff-labs/fff-node";
31
+ import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
32
+ import type {
33
+ AgentToolResult,
34
+ AgentToolUpdateCallback,
35
+ BashToolInput,
36
+ ExtensionCommandContext,
37
+ ExtensionContext,
38
+ FindToolInput,
39
+ GrepToolInput,
40
+ LsToolInput,
41
+ ReadToolInput,
42
+ ToolRenderResultOptions,
43
+ } from "@mariozechner/pi-coding-agent";
30
44
  import { codeToANSI } from "@shikijs/cli";
31
45
  import type { BundledLanguage, BundledTheme } from "shiki";
32
46
 
@@ -53,8 +67,6 @@ const CACHE_LIMIT = envInt("PRETTY_CACHE_LIMIT", 128);
53
67
 
54
68
  let RST = "\x1b[0m";
55
69
  const BOLD = "\x1b[1m";
56
- const DIM = "\x1b[2m";
57
- const ITALIC = "\x1b[3m";
58
70
 
59
71
  const FG_LNUM = "\x1b[38;2;100;100;100m";
60
72
  const FG_DIM = "\x1b[38;2;80;80;80m";
@@ -63,12 +75,7 @@ const FG_GREEN = "\x1b[38;2;100;180;120m";
63
75
  const FG_RED = "\x1b[38;2;200;100;100m";
64
76
  const FG_YELLOW = "\x1b[38;2;220;180;80m";
65
77
  const FG_BLUE = "\x1b[38;2;100;140;220m";
66
- const FG_CYAN = "\x1b[38;2;80;190;190m";
67
78
  const FG_MUTED = "\x1b[38;2;139;148;158m";
68
- const FG_ORANGE = "\x1b[38;2;220;140;60m";
69
- const FG_PURPLE = "\x1b[38;2;170;120;200m";
70
-
71
- const BG_STDERR = "\x1b[48;2;40;25;25m";
72
79
 
73
80
  const BG_DEFAULT = "\x1b[49m";
74
81
  let BG_BASE = BG_DEFAULT; // tool box success/base bg — updated from theme's toolSuccessBg
@@ -159,8 +166,9 @@ function fillToolBackground(text: string, bg = BG_BASE): string {
159
166
  }
160
167
 
161
168
  function termW(): number {
169
+ const stderrWithColumns = process.stderr as NodeJS.WriteStream & { columns?: number };
162
170
  const raw =
163
- process.stdout.columns || (process.stderr as any).columns || Number.parseInt(process.env.COLUMNS ?? "", 10) || 200;
171
+ process.stdout.columns || stderrWithColumns.columns || Number.parseInt(process.env.COLUMNS ?? "", 10) || 200;
164
172
  return Math.max(80, Math.min(raw - 4, 210));
165
173
  }
166
174
 
@@ -456,7 +464,6 @@ const USE_ICONS = ICONS_MODE !== "none" && ICONS_MODE !== "off";
456
464
 
457
465
  // Nerd Font codepoints + ANSI color per file type
458
466
  const NF_DIR = `${FG_BLUE}\ue5ff${RST}`; // folder
459
- const NF_DIR_OPEN = `${FG_BLUE}\ue5fe${RST}`; // folder open
460
467
  const NF_DEFAULT = `${FG_DIM}\uf15b${RST}`; // generic file
461
468
 
462
469
  const EXT_ICON: Record<string, string> = {
@@ -676,7 +683,7 @@ function renderBashOutput(text: string, exitCode: number | null): { summary: str
676
683
  }
677
684
 
678
685
  /** Render ls output as a tree view with icons. */
679
- function renderTree(text: string, basePath: string): string {
686
+ function renderTree(text: string, _basePath: string): string {
680
687
  const lines = text.trim().split("\n").filter(Boolean);
681
688
  if (!lines.length) return `${FG_DIM}(empty directory)${RST}`;
682
689
 
@@ -719,7 +726,8 @@ function renderFindResults(text: string): string {
719
726
  const dir = dirname(trimmed) || ".";
720
727
  const file = basename(trimmed);
721
728
  if (!groups.has(dir)) groups.set(dir, []);
722
- groups.get(dir)!.push(file);
729
+ const bucket = groups.get(dir);
730
+ if (bucket) bucket.push(file);
723
731
  }
724
732
 
725
733
  const out: string[] = [];
@@ -749,7 +757,6 @@ async function renderGrepResults(text: string, pattern: string): Promise<string>
749
757
  const lines = text.split("\n");
750
758
  if (!lines.length || (lines.length === 1 && !lines[0].trim())) return `${FG_DIM}(no matches)${RST}`;
751
759
 
752
- const tw = termW();
753
760
  const out: string[] = [];
754
761
  let currentFile = "";
755
762
  let count = 0;
@@ -805,9 +812,131 @@ async function renderGrepResults(text: string, pattern: string): Promise<string>
805
812
  // If not, falls back to wrapping SDK tools (current behavior).
806
813
  // ---------------------------------------------------------------------------
807
814
 
815
+ type ToolTextContent = TextContent;
816
+ type ToolImageContent = ImageContent;
817
+ type ToolContent = TextContent | ImageContent;
818
+ type ToolResultLike<TDetails = unknown> = AgentToolResult<TDetails | undefined>;
819
+ type TextComponentLike = { setText(value: string): void; getText?: () => string };
820
+ type TextComponentCtor = new (text?: string, x?: number, y?: number) => TextComponentLike;
821
+ type ThemeLike = BgTheme & FgTheme & { bold: (text: string) => string };
822
+ type RenderContextLike<TState extends Record<string, string | undefined> = Record<string, string | undefined>> = {
823
+ lastComponent?: TextComponentLike;
824
+ state: TState;
825
+ expanded: boolean;
826
+ isError: boolean;
827
+ invalidate: () => void;
828
+ };
829
+ type SessionContextLike = ExtensionContext;
830
+ type CommandContextLike = ExtensionCommandContext;
831
+ type ToolExecutor<TParams, TDetails = unknown> = (
832
+ toolCallId: string,
833
+ params: TParams,
834
+ signal?: AbortSignal,
835
+ onUpdate?: AgentToolUpdateCallback<TDetails | undefined>,
836
+ ctx?: ExtensionContext,
837
+ ) => Promise<ToolResultLike<TDetails>>;
838
+ type ToolFactory<TParams, TDetails = unknown> = (cwd: string) => {
839
+ name?: string;
840
+ description?: string;
841
+ label?: string;
842
+ parameters?: unknown;
843
+ execute: ToolExecutor<TParams, TDetails>;
844
+ };
845
+ type PiPrettySdk = {
846
+ createReadToolDefinition?: ToolFactory<ReadToolInput>;
847
+ createReadTool?: ToolFactory<ReadToolInput>;
848
+ createBashToolDefinition?: ToolFactory<BashToolInput>;
849
+ createBashTool?: ToolFactory<BashToolInput>;
850
+ createLsToolDefinition?: ToolFactory<LsToolInput>;
851
+ createLsTool?: ToolFactory<LsToolInput>;
852
+ createFindToolDefinition?: ToolFactory<FindToolInput>;
853
+ createFindTool?: ToolFactory<FindToolInput>;
854
+ createGrepToolDefinition?: ToolFactory<GrepToolInput>;
855
+ createGrepTool?: ToolFactory<GrepToolInput>;
856
+ getAgentDir?: () => string;
857
+ };
858
+ type PiPrettyApi = {
859
+ registerTool: (tool: unknown) => void;
860
+ registerCommand: (
861
+ name: string,
862
+ command: {
863
+ description?: string;
864
+ handler: (args: string, ctx: CommandContextLike) => Promise<void> | void;
865
+ },
866
+ ) => void;
867
+ on: (event: string, handler: (event: unknown, ctx: SessionContextLike) => Promise<void> | void) => void;
868
+ };
869
+ type OptionalFffModule = { FileFinder: typeof FileFinder };
870
+ type FffBackedFinder = FileFinder;
871
+ type ReadParams = ReadToolInput;
872
+ type BashParams = BashToolInput;
873
+ type LsParams = LsToolInput;
874
+ type FindParams = FindToolInput;
875
+ type GrepParams = GrepToolInput;
876
+ type MultiGrepParams = {
877
+ patterns: string[];
878
+ constraints?: string;
879
+ context?: number;
880
+ limit?: number;
881
+ };
882
+ type GrepRenderState = { _gk?: string; _gt?: string };
883
+ type MultiGrepRenderState = { _mgk?: string; _mgt?: string };
884
+ type FindResultDetails = { _type: "findResult"; text: string; pattern: string; matchCount: number };
885
+ type GrepResultDetails = { _type: "grepResult"; text: string; pattern: string; matchCount: number };
886
+ type RenderDetails =
887
+ | { _type: "readImage"; filePath: string; data: string; mimeType: string }
888
+ | { _type: "readFile"; filePath: string; content: string; offset: number; lineCount: number }
889
+ | { _type: "bashResult"; text: string; exitCode: number | null; command: string }
890
+ | { _type: "lsResult"; text: string; path: string; entryCount: number }
891
+ | FindResultDetails
892
+ | GrepResultDetails;
893
+
894
+ function isTextContent(content: ToolContent): content is ToolTextContent {
895
+ return content.type === "text";
896
+ }
897
+
898
+ function isImageContent(content: ToolContent): content is ToolImageContent {
899
+ return content.type === "image";
900
+ }
901
+
902
+ function getTextContent(result: ToolResultLike): string {
903
+ return (
904
+ result.content
905
+ ?.filter(isTextContent)
906
+ .map((content) => content.text || "")
907
+ .join("\n") ?? ""
908
+ );
909
+ }
910
+
911
+ function setResultDetails<T>(result: ToolResultLike, details: T): void {
912
+ result.details = details;
913
+ }
914
+
915
+ function makeTextResult<TDetails>(text: string, details: TDetails): ToolResultLike<TDetails> {
916
+ return {
917
+ content: [{ type: "text", text }],
918
+ details,
919
+ };
920
+ }
921
+
922
+ function appendNotices(text: string, notices: string[]): string {
923
+ return notices.length ? `${text}\n\n[${notices.join(". ")}]` : text;
924
+ }
925
+
926
+ function countRipgrepMatches(text: string): number {
927
+ return text
928
+ .trim()
929
+ .split("\n")
930
+ .filter((line) => /^.+?[:-]\d+[:-]/.test(line)).length;
931
+ }
932
+
933
+ function getErrorMessage(error: unknown): string {
934
+ return error instanceof Error ? error.message : String(error);
935
+ }
936
+
808
937
  const _cursorStore = new CursorStore();
809
- let _fffModule: any = null;
810
- let _fffFinder: any = null;
938
+ let _fffModule: OptionalFffModule | null = null;
939
+ let _fffFinder: FffBackedFinder | null = null;
811
940
  let _fffPartialIndex = false;
812
941
  let _fffDbDir: string | null = null;
813
942
  const FFF_SCAN_TIMEOUT = 15_000;
@@ -816,7 +945,7 @@ function getPiPrettyFffDir(agentDir: string): string {
816
945
  return join(agentDir, "pi-pretty", "fff");
817
946
  }
818
947
 
819
- async function fffEnsureFinder(cwd: string): Promise<any> {
948
+ async function fffEnsureFinder(cwd: string): Promise<FffBackedFinder | null> {
820
949
  if (_fffFinder && !_fffFinder.isDestroyed) return _fffFinder;
821
950
  if (!_fffModule || !_fffDbDir) return null;
822
951
 
@@ -853,20 +982,20 @@ function fffDestroy(): void {
853
982
  * In production, omit `deps` — the extension uses require() to load them.
854
983
  */
855
984
  export interface PiPrettyDeps {
856
- sdk: any;
857
- TextComponent: any;
858
- fffModule?: any;
985
+ sdk: PiPrettySdk;
986
+ TextComponent: TextComponentCtor;
987
+ fffModule?: OptionalFffModule;
859
988
  }
860
989
 
861
- export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
862
- let createReadTool: any;
863
- let createBashTool: any;
864
- let createLsTool: any;
865
- let createFindTool: any;
866
- let createGrepTool: any;
867
- let TextComponent: any;
990
+ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps): void {
991
+ let createReadTool: ToolFactory<ReadToolInput> | undefined;
992
+ let createBashTool: ToolFactory<BashToolInput> | undefined;
993
+ let createLsTool: ToolFactory<LsToolInput> | undefined;
994
+ let createFindTool: ToolFactory<FindToolInput> | undefined;
995
+ let createGrepTool: ToolFactory<GrepToolInput> | undefined;
996
+ let TextComponent: TextComponentCtor;
868
997
 
869
- let sdk: any;
998
+ let sdk: PiPrettySdk;
870
999
 
871
1000
  if (deps) {
872
1001
  // Test path: use injected dependencies, reset module state
@@ -904,7 +1033,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
904
1033
  // FFF initialization (optional — graceful fallback to SDK)
905
1034
  // ===================================================================
906
1035
 
907
- const getAgentDir = (sdk as any).getAgentDir;
1036
+ const getAgentDir = sdk.getAgentDir;
908
1037
  if (!deps) {
909
1038
  // Only try require() in production — tests inject fffModule via deps
910
1039
  try {
@@ -925,12 +1054,12 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
925
1054
  } catch {}
926
1055
  }
927
1056
 
928
- pi.on("session_start", async (_event: any, ctx: any) => {
1057
+ pi.on("session_start", async (_event, ctx) => {
929
1058
  // Try dynamic import if sync require failed (ESM-only package)
930
1059
  if (!_fffModule) {
931
1060
  try {
932
- // @ts-ignore optional dependency, may not be installed
933
- _fffModule = await import("@ff-labs/fff-node");
1061
+ const imported = await import("@ff-labs/fff-node");
1062
+ _fffModule = { FileFinder: imported.FileFinder };
934
1063
  } catch {}
935
1064
  }
936
1065
  if (!_fffModule) return;
@@ -951,8 +1080,8 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
951
1080
  ctx.ui?.setStatus?.("fff", "FFF indexed");
952
1081
  setTimeout(() => ctx.ui?.setStatus?.("fff", undefined), 3000);
953
1082
  }
954
- } catch (e: any) {
955
- ctx.ui?.notify?.(`FFF init failed: ${e.message}`, "error");
1083
+ } catch (error: unknown) {
1084
+ ctx.ui?.notify?.(`FFF init failed: ${getErrorMessage(error)}`, "error");
956
1085
  }
957
1086
  });
958
1087
 
@@ -970,69 +1099,68 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
970
1099
  ...origRead,
971
1100
  name: "read",
972
1101
 
973
- async execute(tid: string, params: any, sig: any, upd: any, ctx: any) {
974
- const result = await origRead.execute(tid, params, sig, upd, ctx);
1102
+ async execute(
1103
+ tid: string,
1104
+ params: ReadParams,
1105
+ sig: AbortSignal | undefined,
1106
+ upd: AgentToolUpdateCallback<unknown> | undefined,
1107
+ ctx: ExtensionContext,
1108
+ ) {
1109
+ const result = (await origRead.execute(tid, params, sig, upd, ctx)) as ToolResultLike;
975
1110
 
976
1111
  const fp = params.path ?? "";
977
1112
  const offset = params.offset ?? 1;
978
1113
 
979
- // Check for image content
980
- const imageBlock = result.content?.find((c: any) => c.type === "image");
1114
+ const imageBlock = result.content?.find(isImageContent);
981
1115
  if (imageBlock) {
982
- (result as any).details = {
1116
+ setResultDetails(result, {
983
1117
  _type: "readImage",
984
1118
  filePath: fp,
985
1119
  data: imageBlock.data,
986
1120
  mimeType: imageBlock.mimeType ?? "image/png",
987
- };
1121
+ });
988
1122
  return result;
989
1123
  }
990
1124
 
991
- // Extract text content for rendering
992
- const textContent = result.content
993
- ?.filter((c: any) => c.type === "text")
994
- .map((c: any) => c.text || "")
995
- .join("\n");
996
-
1125
+ const textContent = getTextContent(result);
997
1126
  if (textContent && fp) {
998
1127
  const lineCount = textContent.split("\n").length;
999
- (result as any).details = {
1128
+ setResultDetails(result, {
1000
1129
  _type: "readFile",
1001
1130
  filePath: fp,
1002
1131
  content: textContent,
1003
1132
  offset,
1004
1133
  lineCount,
1005
- };
1134
+ });
1006
1135
  }
1007
1136
 
1008
1137
  return result;
1009
1138
  },
1010
1139
 
1011
- renderCall(args: any, theme: any, ctx: any) {
1140
+ renderCall(args: ReadParams, theme: ThemeLike, ctx: RenderContextLike) {
1012
1141
  resolveBaseBackground(theme);
1013
- const fp = args?.path ?? "";
1142
+ const fp = args.path ?? "";
1014
1143
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1015
- const offset = args?.offset ? ` ${theme.fg("muted", `from line ${args.offset}`)}` : "";
1016
- const limit = args?.limit ? ` ${theme.fg("muted", `(${args.limit} lines)`)}` : "";
1017
- text.setText(fillToolBackground(`${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", sp(fp))}${offset}${limit}`));
1144
+ const offset = args.offset ? ` ${theme.fg("muted", `from line ${args.offset}`)}` : "";
1145
+ const limit = args.limit ? ` ${theme.fg("muted", `(${args.limit} lines)`)}` : "";
1146
+ text.setText(
1147
+ fillToolBackground(
1148
+ `${theme.fg("toolTitle", theme.bold("read"))} ${theme.fg("accent", sp(fp))}${offset}${limit}`,
1149
+ ),
1150
+ );
1018
1151
  return text;
1019
1152
  },
1020
1153
 
1021
- renderResult(result: any, _opt: any, theme: any, ctx: any) {
1154
+ renderResult(result: ToolResultLike, _opt: unknown, theme: ThemeLike, ctx: RenderContextLike) {
1022
1155
  resolveBaseBackground(theme);
1023
1156
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1024
1157
 
1025
1158
  if (ctx.isError) {
1026
- const e =
1027
- result.content
1028
- ?.filter((c: any) => c.type === "text")
1029
- .map((c: any) => c.text || "")
1030
- .join("\n") ?? "Error";
1031
- text.setText(renderToolError(e, theme));
1159
+ text.setText(renderToolError(getTextContent(result) || "Error", theme));
1032
1160
  return text;
1033
1161
  }
1034
1162
 
1035
- const d = result.details;
1163
+ const d = result.details as RenderDetails | undefined;
1036
1164
 
1037
1165
  // Image rendering
1038
1166
  if (d?._type === "readImage") {
@@ -1052,7 +1180,9 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1052
1180
  out.push(` ${FG_YELLOW}${passthroughWarning}${RST}`);
1053
1181
  } else if (protocol === "kitty") {
1054
1182
  if (d.mimeType && d.mimeType !== "image/png") {
1055
- out.push(` ${FG_YELLOW}Kitty/Ghostty inline preview currently supports PNG payloads (got ${d.mimeType})${RST}`);
1183
+ out.push(
1184
+ ` ${FG_YELLOW}Kitty/Ghostty inline preview currently supports PNG payloads (got ${d.mimeType})${RST}`,
1185
+ );
1056
1186
  } else {
1057
1187
  const imgCols = Math.min(tw - 4, 80);
1058
1188
  out.push(renderKittyImage(d.data, { cols: imgCols }));
@@ -1095,8 +1225,9 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1095
1225
  }
1096
1226
 
1097
1227
  // Fallback
1098
- const fallback = result.content?.[0]?.text ?? "read";
1099
- text.setText(fillToolBackground(` ${theme.fg("dim", String(fallback).slice(0, 120))}`));
1228
+ const fallback = result.content?.[0];
1229
+ const fallbackText = fallback && isTextContent(fallback) ? fallback.text : "read";
1230
+ text.setText(fillToolBackground(` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`));
1100
1231
  return text;
1101
1232
  },
1102
1233
  });
@@ -1112,69 +1243,65 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1112
1243
  ...origBash,
1113
1244
  name: "bash",
1114
1245
 
1115
- async execute(tid: string, params: any, sig: any, upd: any, ctx: any) {
1116
- const result = await origBash.execute(tid, params, sig, upd, ctx);
1246
+ async execute(
1247
+ tid: string,
1248
+ params: BashParams,
1249
+ sig: AbortSignal | undefined,
1250
+ upd: AgentToolUpdateCallback<unknown> | undefined,
1251
+ ctx: ExtensionContext,
1252
+ ) {
1253
+ const result = (await origBash.execute(tid, params, sig, upd, ctx)) as ToolResultLike;
1254
+ const textContent = getTextContent(result);
1117
1255
 
1118
- const textContent = result.content
1119
- ?.filter((c: any) => c.type === "text")
1120
- .map((c: any) => c.text || "")
1121
- .join("\n");
1122
-
1123
- // Try to extract exit code from the output
1124
1256
  let exitCode: number | null = 0;
1125
1257
  if (textContent) {
1126
1258
  const exitMatch = textContent.match(/(?:exit code|exited with|exit status)[:\s]*(\d+)/i);
1127
1259
  if (exitMatch) exitCode = Number(exitMatch[1]);
1128
- // Check for common error indicators
1129
1260
  if (textContent.includes("command not found") || textContent.includes("No such file")) {
1130
1261
  exitCode = 1;
1131
1262
  }
1132
1263
  }
1133
1264
 
1134
- (result as any).details = {
1265
+ setResultDetails(result, {
1135
1266
  _type: "bashResult",
1136
1267
  text: textContent ?? "",
1137
1268
  exitCode,
1138
1269
  command: params.command ?? "",
1139
- };
1270
+ });
1140
1271
 
1141
1272
  return result;
1142
1273
  },
1143
1274
 
1144
- renderCall(args: any, theme: any, ctx: any) {
1145
- resolveBaseBackground(theme);
1146
- const cmd = args?.command ?? "";
1275
+ renderCall(args: BashParams, theme: ThemeLike, ctx: RenderContextLike) {
1276
+ resolveBaseBackground(theme);
1277
+ const cmd = args.command ?? "";
1147
1278
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1148
- const timeout = args?.timeout ? ` ${theme.fg("muted", `(${args.timeout}s timeout)`)}` : "";
1279
+ const timeout = args.timeout ? ` ${theme.fg("muted", `(${args.timeout}s timeout)`)}` : "";
1149
1280
  text.setText(
1150
- fillToolBackground(`${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg("accent", cmd.length > 80 ? cmd.slice(0, 77) + "…" : cmd)}${timeout}`),
1281
+ fillToolBackground(
1282
+ `${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg("accent", cmd.length > 80 ? `${cmd.slice(0, 77)}…` : cmd)}${timeout}`,
1283
+ ),
1151
1284
  );
1152
1285
  return text;
1153
1286
  },
1154
1287
 
1155
- renderResult(result: any, _opt: any, theme: any, ctx: any) {
1156
- resolveBaseBackground(theme);
1288
+ renderResult(result: ToolResultLike, _opt: unknown, theme: ThemeLike, ctx: RenderContextLike) {
1289
+ resolveBaseBackground(theme);
1157
1290
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1158
1291
 
1159
1292
  if (ctx.isError) {
1160
- const e =
1161
- result.content
1162
- ?.filter((c: any) => c.type === "text")
1163
- .map((c: any) => c.text || "")
1164
- .join("\n") ?? "Error";
1165
- text.setText(renderToolError(e, theme));
1293
+ text.setText(renderToolError(getTextContent(result) || "Error", theme));
1166
1294
  return text;
1167
1295
  }
1168
1296
 
1169
- const d = result.details;
1297
+ const d = result.details as RenderDetails | undefined;
1170
1298
  if (d?._type === "bashResult") {
1171
- const { summary, body } = renderBashOutput(d.text, d.exitCode);
1299
+ const { summary } = renderBashOutput(d.text, d.exitCode);
1172
1300
  const lines = d.text.split("\n");
1173
1301
  const lineCount = lines.length;
1174
1302
  const lineInfo = lineCount > 1 ? ` ${FG_DIM}(${lineCount} lines)${RST}` : "";
1175
1303
  const header = ` ${summary}${lineInfo}`;
1176
1304
 
1177
- // Show output content
1178
1305
  if (d.text.trim()) {
1179
1306
  const maxShow = ctx.expanded ? lineCount : MAX_PREVIEW_LINES;
1180
1307
  const show = lines.slice(0, maxShow);
@@ -1194,8 +1321,9 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1194
1321
  return text;
1195
1322
  }
1196
1323
 
1197
- const fallback = result.content?.[0]?.text ?? "done";
1198
- text.setText(fillToolBackground(` ${theme.fg("dim", String(fallback).slice(0, 120))}`));
1324
+ const fallback = result.content?.[0];
1325
+ const fallbackText = fallback && isTextContent(fallback) ? fallback.text : "done";
1326
+ text.setText(fillToolBackground(` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`));
1199
1327
  return text;
1200
1328
  },
1201
1329
  });
@@ -1212,50 +1340,46 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1212
1340
  ...origLs,
1213
1341
  name: "ls",
1214
1342
 
1215
- async execute(tid: string, params: any, sig: any, upd: any, ctx: any) {
1216
- const result = await origLs.execute(tid, params, sig, upd, ctx);
1217
-
1218
- const textContent = result.content
1219
- ?.filter((c: any) => c.type === "text")
1220
- .map((c: any) => c.text || "")
1221
- .join("\n");
1222
-
1343
+ async execute(
1344
+ tid: string,
1345
+ params: LsParams,
1346
+ sig: AbortSignal | undefined,
1347
+ upd: AgentToolUpdateCallback<unknown> | undefined,
1348
+ ctx: ExtensionContext,
1349
+ ) {
1350
+ const result = (await origLs.execute(tid, params, sig, upd, ctx)) as ToolResultLike;
1351
+ const textContent = getTextContent(result);
1223
1352
  const fp = params.path ?? cwd;
1224
1353
  const entryCount = textContent ? textContent.trim().split("\n").filter(Boolean).length : 0;
1225
1354
 
1226
- (result as any).details = {
1355
+ setResultDetails(result, {
1227
1356
  _type: "lsResult",
1228
1357
  text: textContent ?? "",
1229
1358
  path: fp,
1230
1359
  entryCount,
1231
- };
1360
+ });
1232
1361
 
1233
1362
  return result;
1234
1363
  },
1235
1364
 
1236
- renderCall(args: any, theme: any, ctx: any) {
1237
- resolveBaseBackground(theme);
1238
- const fp = args?.path ?? ".";
1365
+ renderCall(args: LsParams, theme: ThemeLike, ctx: RenderContextLike) {
1366
+ resolveBaseBackground(theme);
1367
+ const fp = args.path ?? ".";
1239
1368
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1240
1369
  text.setText(fillToolBackground(`${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", sp(fp))}`));
1241
1370
  return text;
1242
1371
  },
1243
1372
 
1244
- renderResult(result: any, _opt: any, theme: any, ctx: any) {
1245
- resolveBaseBackground(theme);
1373
+ renderResult(result: ToolResultLike, _opt: unknown, theme: ThemeLike, ctx: RenderContextLike) {
1374
+ resolveBaseBackground(theme);
1246
1375
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1247
1376
 
1248
1377
  if (ctx.isError) {
1249
- const e =
1250
- result.content
1251
- ?.filter((c: any) => c.type === "text")
1252
- .map((c: any) => c.text || "")
1253
- .join("\n") ?? "Error";
1254
- text.setText(renderToolError(e, theme));
1378
+ text.setText(renderToolError(getTextContent(result) || "Error", theme));
1255
1379
  return text;
1256
1380
  }
1257
1381
 
1258
- const d = result.details;
1382
+ const d = result.details as RenderDetails | undefined;
1259
1383
  if (d?._type === "lsResult" && d.text) {
1260
1384
  const tree = renderTree(d.text, d.path);
1261
1385
  const info = `${FG_DIM}${d.entryCount} entries${RST}`;
@@ -1263,8 +1387,9 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1263
1387
  return text;
1264
1388
  }
1265
1389
 
1266
- const fallback = result.content?.[0]?.text ?? "listed";
1267
- text.setText(fillToolBackground(` ${theme.fg("dim", String(fallback).slice(0, 120))}`));
1390
+ const fallback = result.content?.[0];
1391
+ const fallbackText = fallback && isTextContent(fallback) ? fallback.text : "listed";
1392
+ text.setText(fillToolBackground(` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`));
1268
1393
  return text;
1269
1394
  },
1270
1395
  });
@@ -1281,37 +1406,36 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1281
1406
  ...origFind,
1282
1407
  name: "find",
1283
1408
 
1284
- async execute(tid: string, params: any, sig: any, upd: any, ctx: any) {
1409
+ async execute(
1410
+ tid: string,
1411
+ params: FindParams,
1412
+ sig: AbortSignal | undefined,
1413
+ upd: unknown,
1414
+ ctx: ExtensionContext,
1415
+ ) {
1285
1416
  // Try FFF first (frecency-ranked, SIMD-accelerated)
1286
1417
  if (_fffFinder && !_fffFinder.isDestroyed) {
1287
1418
  try {
1288
1419
  const effectiveLimit = Math.max(1, params.limit ?? 200);
1289
- let query = params.pattern ?? "";
1420
+ let query = params.pattern;
1290
1421
  if (params.path) query = `${params.path} ${query}`;
1291
1422
 
1292
1423
  const searchResult = _fffFinder.fileSearch(query, { pageSize: effectiveLimit });
1293
1424
  if (searchResult.ok) {
1294
- const items = searchResult.value.items.slice(0, effectiveLimit);
1295
- let textContent = items.map((i: any) => i.relativePath).join("\n");
1296
- const matchCount = items.length;
1297
-
1425
+ const search: SearchResult = searchResult.value;
1426
+ const items: FileItem[] = search.items.slice(0, effectiveLimit);
1298
1427
  const notices: string[] = [];
1299
1428
  if (_fffPartialIndex) notices.push("Warning: partial file index");
1300
1429
  if (items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
1301
- if (searchResult.value.totalMatched > items.length) {
1302
- notices.push(`${searchResult.value.totalMatched} total matches`);
1303
- }
1304
- if (notices.length) textContent += `\n\n[${notices.join(". ")}]`;
1305
-
1306
- return {
1307
- content: [{ type: "text", text: textContent }],
1308
- details: {
1309
- _type: "findResult",
1310
- text: textContent,
1311
- pattern: params.pattern ?? "",
1312
- matchCount,
1313
- },
1314
- };
1430
+ if (search.totalMatched > items.length) notices.push(`${search.totalMatched} total matches`);
1431
+
1432
+ const textContent = appendNotices(items.map((item) => item.relativePath).join("\n"), notices);
1433
+ return makeTextResult<FindResultDetails>(textContent, {
1434
+ _type: "findResult",
1435
+ text: textContent,
1436
+ pattern: params.pattern,
1437
+ matchCount: items.length,
1438
+ });
1315
1439
  }
1316
1440
  } catch {
1317
1441
  /* fall through to SDK */
@@ -1319,45 +1443,42 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1319
1443
  }
1320
1444
 
1321
1445
  // SDK fallback
1322
- const result = await origFind.execute(tid, params, sig, upd, ctx);
1323
-
1324
- const textContent = result.content
1325
- ?.filter((c: any) => c.type === "text")
1326
- .map((c: any) => c.text || "")
1327
- .join("\n");
1328
-
1446
+ const result = await origFind.execute(tid, params, sig, upd as never, ctx);
1447
+ const textContent = getTextContent(result);
1329
1448
  const matchCount = textContent ? textContent.trim().split("\n").filter(Boolean).length : 0;
1330
1449
 
1331
- (result as any).details = {
1450
+ setResultDetails<FindResultDetails>(result, {
1332
1451
  _type: "findResult",
1333
- text: textContent ?? "",
1334
- pattern: params.pattern ?? "",
1452
+ text: textContent,
1453
+ pattern: params.pattern,
1335
1454
  matchCount,
1336
- };
1455
+ });
1337
1456
 
1338
1457
  return result;
1339
1458
  },
1340
1459
 
1341
- renderCall(args: any, theme: any, ctx: any) {
1342
- resolveBaseBackground(theme);
1343
- const pattern = args?.pattern ?? "";
1344
- const path = args?.path ? ` ${theme.fg("muted", `in ${sp(args.path)}`)}` : "";
1460
+ renderCall(args: FindParams, theme: ThemeLike, ctx: RenderContextLike) {
1461
+ resolveBaseBackground(theme);
1462
+ const pattern = args.pattern ?? "";
1463
+ const path = args.path ? ` ${theme.fg("muted", `in ${sp(args.path)}`)}` : "";
1345
1464
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1346
- text.setText(fillToolBackground(`${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}${path}`));
1465
+ text.setText(
1466
+ fillToolBackground(`${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}${path}`),
1467
+ );
1347
1468
  return text;
1348
1469
  },
1349
1470
 
1350
- renderResult(result: any, _opt: any, theme: any, ctx: any) {
1351
- resolveBaseBackground(theme);
1471
+ renderResult(
1472
+ result: ToolResultLike<FindResultDetails>,
1473
+ _opt: ToolRenderResultOptions,
1474
+ theme: ThemeLike,
1475
+ ctx: RenderContextLike,
1476
+ ) {
1477
+ resolveBaseBackground(theme);
1352
1478
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1353
1479
 
1354
1480
  if (ctx.isError) {
1355
- const e =
1356
- result.content
1357
- ?.filter((c: any) => c.type === "text")
1358
- .map((c: any) => c.text || "")
1359
- .join("\n") ?? "Error";
1360
- text.setText(renderToolError(e, theme));
1481
+ text.setText(renderToolError(getTextContent(result) || "Error", theme));
1361
1482
  return text;
1362
1483
  }
1363
1484
 
@@ -1369,8 +1490,9 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1369
1490
  return text;
1370
1491
  }
1371
1492
 
1372
- const fallback = result.content?.[0]?.text ?? "found";
1373
- text.setText(fillToolBackground(` ${theme.fg("dim", String(fallback).slice(0, 120))}`));
1493
+ const fallback = result.content?.[0];
1494
+ const fallbackText = fallback && isTextContent(fallback) ? fallback.text : "found";
1495
+ text.setText(fillToolBackground(` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`));
1374
1496
  return text;
1375
1497
  },
1376
1498
  });
@@ -1387,19 +1509,23 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1387
1509
  ...origGrep,
1388
1510
  name: "grep",
1389
1511
 
1390
- async execute(tid: string, params: any, sig: any, upd: any, ctx: any) {
1391
- // Try FFF first (SIMD-accelerated, frecency-ranked)
1392
- if (_fffFinder && !_fffFinder.isDestroyed) {
1512
+ async execute(
1513
+ tid: string,
1514
+ params: GrepParams,
1515
+ sig: AbortSignal | undefined,
1516
+ upd: unknown,
1517
+ ctx: ExtensionContext,
1518
+ ) {
1519
+ // Try FFF first (SIMD-accelerated, frecency-ranked).
1520
+ // FFF 0.5.2 can abort the process when path/glob constraints meet
1521
+ // Unicode filenames, so constrained searches use the SDK fallback.
1522
+ if (_fffFinder && !_fffFinder.isDestroyed && !params.path && !params.glob) {
1393
1523
  try {
1394
1524
  const effectiveLimit = Math.max(1, params.limit ?? 100);
1395
- let query = params.pattern ?? "";
1396
- if (params.glob) query = `${params.glob} ${query}`;
1397
- else if (params.path) query = `${params.path} ${query}`;
1398
-
1399
- const mode = params.literal ? "plain" : "regex";
1525
+ const query = params.pattern;
1400
1526
 
1401
1527
  const grepResult = _fffFinder.grep(query, {
1402
- mode,
1528
+ mode: params.literal ? "plain" : "regex",
1403
1529
  smartCase: !params.ignoreCase,
1404
1530
  maxMatchesPerFile: Math.min(effectiveLimit, 50),
1405
1531
  cursor: null,
@@ -1408,31 +1534,23 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1408
1534
  });
1409
1535
 
1410
1536
  if (grepResult.ok) {
1411
- const result = grepResult.value;
1412
- let textContent = fffFormatGrepText(result.items, effectiveLimit);
1413
- const matchCount = Math.min(result.items.length, effectiveLimit);
1414
-
1537
+ const grep: GrepResult = grepResult.value;
1415
1538
  const notices: string[] = [];
1416
1539
  if (_fffPartialIndex) notices.push("Warning: partial file index");
1417
- if (result.items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
1418
- if ((result as any).regexFallbackError) {
1419
- notices.push(`Regex failed: ${(result as any).regexFallbackError}, used literal match`);
1420
- }
1421
- if (result.nextCursor) {
1422
- const cursorId = _cursorStore.store(result.nextCursor);
1540
+ if (grep.items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
1541
+ if (grep.regexFallbackError) notices.push(`Regex failed: ${grep.regexFallbackError}, used literal match`);
1542
+ if (grep.nextCursor) {
1543
+ const cursorId = _cursorStore.store(grep.nextCursor);
1423
1544
  notices.push(`More results available. Use cursor="${cursorId}" to continue`);
1424
1545
  }
1425
- if (notices.length) textContent += `\n\n[${notices.join(". ")}]`;
1426
-
1427
- return {
1428
- content: [{ type: "text", text: textContent }],
1429
- details: {
1430
- _type: "grepResult",
1431
- text: textContent,
1432
- pattern: params.pattern ?? "",
1433
- matchCount,
1434
- },
1435
- };
1546
+
1547
+ const textContent = appendNotices(fffFormatGrepText(grep.items, effectiveLimit), notices);
1548
+ return makeTextResult<GrepResultDetails>(textContent, {
1549
+ _type: "grepResult",
1550
+ text: textContent,
1551
+ pattern: params.pattern,
1552
+ matchCount: Math.min(grep.items.length, effectiveLimit),
1553
+ });
1436
1554
  }
1437
1555
  } catch {
1438
1556
  /* fall through to SDK */
@@ -1440,51 +1558,45 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1440
1558
  }
1441
1559
 
1442
1560
  // SDK fallback
1443
- const result = await origGrep.execute(tid, params, sig, upd, ctx);
1444
-
1445
- const textContent = result.content
1446
- ?.filter((c: any) => c.type === "text")
1447
- .map((c: any) => c.text || "")
1448
- .join("\n");
1449
-
1450
- const matchCount = textContent
1451
- ? textContent
1452
- .trim()
1453
- .split("\n")
1454
- .filter((l: string) => l.match(/^.+?[:\-]\d+[:\-]/)).length
1455
- : 0;
1561
+ const result = await origGrep.execute(tid, params, sig, upd as never, ctx);
1562
+ const textContent = getTextContent(result);
1563
+ const matchCount = textContent ? countRipgrepMatches(textContent) : 0;
1456
1564
 
1457
- (result as any).details = {
1565
+ setResultDetails<GrepResultDetails>(result, {
1458
1566
  _type: "grepResult",
1459
- text: textContent ?? "",
1460
- pattern: params.pattern ?? "",
1567
+ text: textContent,
1568
+ pattern: params.pattern,
1461
1569
  matchCount,
1462
- };
1570
+ });
1463
1571
 
1464
1572
  return result;
1465
1573
  },
1466
1574
 
1467
- renderCall(args: any, theme: any, ctx: any) {
1468
- resolveBaseBackground(theme);
1469
- const pattern = args?.pattern ?? "";
1470
- const path = args?.path ? ` ${theme.fg("muted", `in ${sp(args.path)}`)}` : "";
1471
- const glob = args?.glob ? ` ${theme.fg("muted", `(${args.glob})`)}` : "";
1575
+ renderCall(args: GrepParams, theme: ThemeLike, ctx: RenderContextLike) {
1576
+ resolveBaseBackground(theme);
1577
+ const pattern = args.pattern ?? "";
1578
+ const path = args.path ? ` ${theme.fg("muted", `in ${sp(args.path)}`)}` : "";
1579
+ const glob = args.glob ? ` ${theme.fg("muted", `(${args.glob})`)}` : "";
1472
1580
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1473
- text.setText(fillToolBackground(`${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", pattern)}${path}${glob}`));
1581
+ text.setText(
1582
+ fillToolBackground(
1583
+ `${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", pattern)}${path}${glob}`,
1584
+ ),
1585
+ );
1474
1586
  return text;
1475
1587
  },
1476
1588
 
1477
- renderResult(result: any, _opt: any, theme: any, ctx: any) {
1478
- resolveBaseBackground(theme);
1589
+ renderResult(
1590
+ result: ToolResultLike<GrepResultDetails>,
1591
+ _opt: ToolRenderResultOptions,
1592
+ theme: ThemeLike,
1593
+ ctx: RenderContextLike<GrepRenderState>,
1594
+ ) {
1595
+ resolveBaseBackground(theme);
1479
1596
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1480
1597
 
1481
1598
  if (ctx.isError) {
1482
- const e =
1483
- result.content
1484
- ?.filter((c: any) => c.type === "text")
1485
- .map((c: any) => c.text || "")
1486
- .join("\n") ?? "Error";
1487
- text.setText(renderToolError(e, theme));
1599
+ text.setText(renderToolError(getTextContent(result) || "Error", theme));
1488
1600
  return text;
1489
1601
  }
1490
1602
 
@@ -1508,8 +1620,9 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1508
1620
  return text;
1509
1621
  }
1510
1622
 
1511
- const fallback = result.content?.[0]?.text ?? "searched";
1512
- text.setText(fillToolBackground(` ${theme.fg("dim", String(fallback).slice(0, 120))}`));
1623
+ const fallback = result.content?.[0];
1624
+ const fallbackText = fallback && isTextContent(fallback) ? fallback.text : "searched";
1625
+ text.setText(fillToolBackground(` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`));
1513
1626
  return text;
1514
1627
  },
1515
1628
  });
@@ -1529,8 +1642,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1529
1642
  "Patterns are literal text — never escape special characters.",
1530
1643
  "Use the constraints parameter for file filtering ('*.rs', 'src/', '!test/').",
1531
1644
  ].join(" "),
1532
- promptSnippet:
1533
- "Multi-pattern OR search across file contents (FFF: SIMD-accelerated, frecency-ranked)",
1645
+ promptSnippet: "Multi-pattern OR search across file contents (FFF: SIMD-accelerated, frecency-ranked)",
1534
1646
  promptGuidelines: [
1535
1647
  "Use multi_grep when you need to find multiple identifiers at once (OR logic).",
1536
1648
  "Include all naming conventions: snake_case, PascalCase, camelCase variants.",
@@ -1562,21 +1674,21 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1562
1674
  required: ["patterns"],
1563
1675
  },
1564
1676
 
1565
- async execute(_tid: string, params: any, sig: any, _upd: any, _ctx: any) {
1566
- if (sig?.aborted) return { content: [{ type: "text", text: "Aborted" }], details: {} };
1677
+ async execute(
1678
+ _tid: string,
1679
+ params: MultiGrepParams,
1680
+ sig: AbortSignal | undefined,
1681
+ _upd: unknown,
1682
+ _ctx: ExtensionContext,
1683
+ ) {
1684
+ if (sig?.aborted) return makeTextResult("Aborted", {});
1567
1685
 
1568
1686
  if (!params.patterns || params.patterns.length === 0) {
1569
- return {
1570
- content: [{ type: "text", text: "Error: patterns array must have at least 1 element" }],
1571
- details: { error: "empty patterns" },
1572
- };
1687
+ return makeTextResult("Error: patterns array must have at least 1 element", { error: "empty patterns" });
1573
1688
  }
1574
1689
 
1575
1690
  if (!_fffFinder || _fffFinder.isDestroyed) {
1576
- return {
1577
- content: [{ type: "text", text: "FFF not initialized. Wait for session start or run /fff-rescan." }],
1578
- details: {},
1579
- };
1691
+ return makeTextResult("FFF not initialized. Wait for session start or run /fff-rescan.", {});
1580
1692
  }
1581
1693
 
1582
1694
  try {
@@ -1593,68 +1705,61 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1593
1705
  });
1594
1706
 
1595
1707
  if (!grepResult.ok) {
1596
- return {
1597
- content: [{ type: "text", text: `multi_grep error: ${grepResult.error}` }],
1598
- details: { error: grepResult.error },
1599
- };
1708
+ return makeTextResult(`multi_grep error: ${grepResult.error}`, { error: grepResult.error });
1600
1709
  }
1601
1710
 
1602
- const result = grepResult.value;
1603
- let textContent = fffFormatGrepText(result.items, effectiveLimit);
1604
- const matchCount = Math.min(result.items.length, effectiveLimit);
1605
-
1711
+ const grep: GrepResult = grepResult.value;
1606
1712
  const notices: string[] = [];
1607
1713
  if (_fffPartialIndex) notices.push("Warning: partial file index");
1608
- if (result.items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
1609
- if (result.nextCursor) {
1610
- const cursorId = _cursorStore.store(result.nextCursor);
1714
+ if (grep.items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
1715
+ if (grep.nextCursor) {
1716
+ const cursorId = _cursorStore.store(grep.nextCursor);
1611
1717
  notices.push(`More results: cursor="${cursorId}"`);
1612
1718
  }
1613
- if (notices.length) textContent += `\n\n[${notices.join(". ")}]`;
1614
-
1615
- return {
1616
- content: [{ type: "text", text: textContent }],
1617
- details: {
1618
- _type: "grepResult",
1619
- text: textContent,
1620
- pattern: params.patterns.join(" | "),
1621
- matchCount,
1622
- },
1623
- };
1624
- } catch (e: any) {
1625
- return {
1626
- content: [{ type: "text", text: `multi_grep error: ${e.message}` }],
1627
- details: { error: e.message },
1628
- };
1719
+
1720
+ const textContent = appendNotices(fffFormatGrepText(grep.items, effectiveLimit), notices);
1721
+ return makeTextResult<GrepResultDetails>(textContent, {
1722
+ _type: "grepResult",
1723
+ text: textContent,
1724
+ pattern: params.patterns.join(" | "),
1725
+ matchCount: Math.min(grep.items.length, effectiveLimit),
1726
+ });
1727
+ } catch (error: unknown) {
1728
+ const message = getErrorMessage(error);
1729
+ return makeTextResult(`multi_grep error: ${message}`, { error: message });
1629
1730
  }
1630
1731
  },
1631
1732
 
1632
- renderCall(args: any, theme: any, ctx: any) {
1733
+ renderCall(args: MultiGrepParams, theme: ThemeLike, ctx: RenderContextLike) {
1633
1734
  resolveBaseBackground(theme);
1634
- const patterns = args?.patterns ?? [];
1635
- const constraints = args?.constraints;
1735
+ const patterns = args.patterns ?? [];
1736
+ const constraints = args.constraints;
1636
1737
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1637
1738
  let content =
1638
1739
  theme.fg("toolTitle", theme.bold("multi_grep")) +
1639
1740
  " " +
1640
- theme.fg("accent", patterns.map((p: string) => `"${p}"`).join(", "));
1741
+ theme.fg("accent", patterns.map((p) => `"${p}"`).join(", "));
1641
1742
  if (constraints) content += theme.fg("muted", ` (${constraints})`);
1642
1743
  text.setText(content);
1643
1744
  return text;
1644
1745
  },
1645
1746
 
1646
- renderResult(result: any, _opt: any, theme: any, ctx: any) {
1747
+ renderResult(
1748
+ result: ToolResultLike<GrepResultDetails | { error?: string }>,
1749
+ _opt: ToolRenderResultOptions,
1750
+ theme: ThemeLike,
1751
+ ctx: RenderContextLike<MultiGrepRenderState>,
1752
+ ) {
1647
1753
  resolveBaseBackground(theme);
1648
1754
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1649
1755
 
1650
1756
  if (ctx.isError) {
1651
- const e = result.content?.[0]?.text ?? "Error";
1652
- text.setText(`\n${theme.fg("error", e)}`);
1757
+ text.setText(`\n${theme.fg("error", getTextContent(result) || "Error")}`);
1653
1758
  return text;
1654
1759
  }
1655
1760
 
1656
1761
  const d = result.details;
1657
- if (d?._type === "grepResult" && d.text) {
1762
+ if (d && "_type" in d && d._type === "grepResult" && d.text) {
1658
1763
  const key = `mgrep:${d.pattern}:${d.matchCount}:${termW()}`;
1659
1764
  if (ctx.state._mgk !== key) {
1660
1765
  ctx.state._mgk = key;
@@ -1673,8 +1778,9 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1673
1778
  return text;
1674
1779
  }
1675
1780
 
1676
- const fallback = result.content?.[0]?.text ?? "searched";
1677
- text.setText(` ${theme.fg("dim", String(fallback).slice(0, 120))}`);
1781
+ const fallback = result.content?.[0];
1782
+ const fallbackText = fallback && isTextContent(fallback) ? fallback.text : "searched";
1783
+ text.setText(` ${theme.fg("dim", String(fallbackText).slice(0, 120))}`);
1678
1784
  return text;
1679
1785
  },
1680
1786
  });
@@ -1687,7 +1793,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1687
1793
  if (_fffModule) {
1688
1794
  pi.registerCommand("fff-health", {
1689
1795
  description: "Show FFF file finder health and indexer status",
1690
- handler: async (_args: any, ctx: any) => {
1796
+ handler: async (_args: string, ctx: CommandContextLike) => {
1691
1797
  if (!_fffFinder || _fffFinder.isDestroyed) {
1692
1798
  ctx.ui?.notify?.("FFF not initialized", "warning");
1693
1799
  return;
@@ -1711,7 +1817,9 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1711
1817
 
1712
1818
  const progress = _fffFinder.getScanProgress();
1713
1819
  if (progress.ok) {
1714
- lines.push(`Scanning: ${progress.value.isScanning ? "yes" : "no"} (${progress.value.scannedFilesCount} files)`);
1820
+ lines.push(
1821
+ `Scanning: ${progress.value.isScanning ? "yes" : "no"} (${progress.value.scannedFilesCount} files)`,
1822
+ );
1715
1823
  }
1716
1824
 
1717
1825
  ctx.ui?.notify?.(lines.join("\n"), "info");
@@ -1720,7 +1828,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
1720
1828
 
1721
1829
  pi.registerCommand("fff-rescan", {
1722
1830
  description: "Trigger FFF to rescan files",
1723
- handler: async (_args: any, ctx: any) => {
1831
+ handler: async (_args: string, ctx: CommandContextLike) => {
1724
1832
  if (!_fffFinder || _fffFinder.isDestroyed) {
1725
1833
  ctx.ui?.notify?.("FFF not initialized", "warning");
1726
1834
  return;