@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/README.md +74 -4
- package/package.json +2 -4
- package/release-notes/v0.4.0.md +58 -0
- package/release-notes/v0.4.1.md +28 -0
- package/src/fff-helpers.ts +7 -6
- package/src/index.ts +385 -277
- package/test/fff-integration.test.ts +11 -2
- package/test/image-rendering.test.ts +6 -0
package/src/index.ts
CHANGED
|
@@ -24,9 +24,23 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import * as childProcess from "node:child_process";
|
|
27
|
-
import {
|
|
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 ||
|
|
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,
|
|
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)
|
|
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:
|
|
810
|
-
let _fffFinder:
|
|
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<
|
|
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:
|
|
857
|
-
TextComponent:
|
|
858
|
-
fffModule?:
|
|
985
|
+
sdk: PiPrettySdk;
|
|
986
|
+
TextComponent: TextComponentCtor;
|
|
987
|
+
fffModule?: OptionalFffModule;
|
|
859
988
|
}
|
|
860
989
|
|
|
861
|
-
export default function piPrettyExtension(pi:
|
|
862
|
-
let createReadTool:
|
|
863
|
-
let createBashTool:
|
|
864
|
-
let createLsTool:
|
|
865
|
-
let createFindTool:
|
|
866
|
-
let createGrepTool:
|
|
867
|
-
let TextComponent:
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
-
|
|
933
|
-
_fffModule =
|
|
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 (
|
|
955
|
-
ctx.ui?.notify?.(`FFF init failed: ${
|
|
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(
|
|
974
|
-
|
|
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
|
-
|
|
980
|
-
const imageBlock = result.content?.find((c: any) => c.type === "image");
|
|
1114
|
+
const imageBlock = result.content?.find(isImageContent);
|
|
981
1115
|
if (imageBlock) {
|
|
982
|
-
(result
|
|
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
|
-
|
|
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
|
|
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:
|
|
1140
|
+
renderCall(args: ReadParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
1012
1141
|
resolveBaseBackground(theme);
|
|
1013
|
-
const fp = args
|
|
1142
|
+
const fp = args.path ?? "";
|
|
1014
1143
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1015
|
-
const offset = args
|
|
1016
|
-
const limit = args
|
|
1017
|
-
text.setText(
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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]
|
|
1099
|
-
|
|
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(
|
|
1116
|
-
|
|
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
|
|
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:
|
|
1145
|
-
|
|
1146
|
-
const cmd = args
|
|
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
|
|
1279
|
+
const timeout = args.timeout ? ` ${theme.fg("muted", `(${args.timeout}s timeout)`)}` : "";
|
|
1149
1280
|
text.setText(
|
|
1150
|
-
fillToolBackground(
|
|
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:
|
|
1156
|
-
|
|
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
|
-
|
|
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
|
|
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]
|
|
1198
|
-
|
|
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(
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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
|
|
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:
|
|
1237
|
-
|
|
1238
|
-
const fp = args
|
|
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:
|
|
1245
|
-
|
|
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
|
-
|
|
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]
|
|
1267
|
-
|
|
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(
|
|
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
|
|
1295
|
-
|
|
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 (
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
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
|
|
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:
|
|
1342
|
-
|
|
1343
|
-
const pattern = args
|
|
1344
|
-
const path = args
|
|
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(
|
|
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(
|
|
1351
|
-
|
|
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
|
-
|
|
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]
|
|
1373
|
-
|
|
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(
|
|
1391
|
-
|
|
1392
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
1418
|
-
if ((
|
|
1419
|
-
|
|
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
|
-
|
|
1426
|
-
|
|
1427
|
-
return {
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
|
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
|
|
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:
|
|
1468
|
-
|
|
1469
|
-
const pattern = args
|
|
1470
|
-
const path = args
|
|
1471
|
-
const glob = args
|
|
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(
|
|
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(
|
|
1478
|
-
|
|
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
|
-
|
|
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]
|
|
1512
|
-
|
|
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(
|
|
1566
|
-
|
|
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
|
|
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 (
|
|
1609
|
-
if (
|
|
1610
|
-
const cursorId = _cursorStore.store(
|
|
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
|
-
|
|
1614
|
-
|
|
1615
|
-
return {
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
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:
|
|
1733
|
+
renderCall(args: MultiGrepParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
1633
1734
|
resolveBaseBackground(theme);
|
|
1634
|
-
const patterns = args
|
|
1635
|
-
const constraints = args
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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]
|
|
1677
|
-
|
|
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:
|
|
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(
|
|
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:
|
|
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;
|