@heyhuynhgiabuu/pi-pretty 0.3.2 → 0.4.0
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 +82 -2
- package/package.json +2 -4
- package/release-notes/v0.3.3.md +48 -0
- package/release-notes/v0.4.0.md +58 -0
- package/src/fff-helpers.ts +7 -6
- package/src/index.ts +444 -300
- package/test/fff-integration.test.ts +16 -3
- 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,36 +75,44 @@ 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
|
-
let BG_BASE = BG_DEFAULT; // tool box base bg — updated from theme's toolSuccessBg
|
|
81
|
+
let BG_BASE = BG_DEFAULT; // tool box success/base bg — updated from theme's toolSuccessBg
|
|
82
|
+
let BG_ERROR = BG_DEFAULT; // tool box error bg — updated from theme's toolErrorBg
|
|
83
|
+
|
|
84
|
+
type BgTheme = { getBgAnsi?: (key: string) => string };
|
|
85
|
+
type FgTheme = { fg: (key: string, text: string) => string };
|
|
75
86
|
|
|
76
87
|
/** Parse an ANSI 24-bit color escape into { r, g, b }. Handles both fg (38;2) and bg (48;2). */
|
|
77
88
|
function parseAnsiRgb(ansi: string): { r: number; g: number; b: number } | null {
|
|
78
|
-
const m = ansi.match(
|
|
89
|
+
const m = ansi.match(new RegExp(`${ESC_RE}\\[(?:38|48);2;(\\d+);(\\d+);(\\d+)m`));
|
|
79
90
|
return m ? { r: +m[1], g: +m[2], b: +m[3] } : null;
|
|
80
91
|
}
|
|
81
92
|
|
|
82
|
-
|
|
93
|
+
function getThemeBgAnsi(theme: BgTheme, key: string): string | null {
|
|
94
|
+
try {
|
|
95
|
+
const bgAnsi = theme.getBgAnsi?.(key);
|
|
96
|
+
return bgAnsi && parseAnsiRgb(bgAnsi) ? bgAnsi : null;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Read themed tool backgrounds and update BG_BASE / BG_ERROR + RST.
|
|
83
103
|
* Call once when theme is first available. Idempotent. */
|
|
84
104
|
let _bgBaseResolved = false;
|
|
85
|
-
function resolveBaseBackground(theme:
|
|
105
|
+
function resolveBaseBackground(theme: BgTheme | null | undefined): void {
|
|
86
106
|
if (_bgBaseResolved || !theme?.getBgAnsi) return;
|
|
87
107
|
_bgBaseResolved = true;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
|
|
109
|
+
BG_BASE = getThemeBgAnsi(theme, "toolSuccessBg") ?? BG_DEFAULT;
|
|
110
|
+
BG_ERROR = getThemeBgAnsi(theme, "toolErrorBg") ?? BG_BASE;
|
|
111
|
+
RST = `\x1b[0m${BG_BASE}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function renderToolError(error: string, theme: FgTheme): string {
|
|
115
|
+
return fillToolBackground(`\n${theme.fg("error", error)}`, BG_ERROR);
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
const ESC_RE = "\u001b";
|
|
@@ -126,9 +146,29 @@ function strip(s: string): string {
|
|
|
126
146
|
return s.replace(ANSI_RE, "");
|
|
127
147
|
}
|
|
128
148
|
|
|
149
|
+
function preserveToolBackground(ansi: string, bg: string): string {
|
|
150
|
+
return ansi.replace(ANSI_CAPTURE_RE, (seq, params: string) => {
|
|
151
|
+
const codes = params.split(";");
|
|
152
|
+
return params === "0" || codes.includes("49") ? `${seq}${bg}` : seq;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function fillToolBackground(text: string, bg = BG_BASE): string {
|
|
157
|
+
const width = termW();
|
|
158
|
+
return text
|
|
159
|
+
.split("\n")
|
|
160
|
+
.map((line) => {
|
|
161
|
+
const normalized = preserveToolBackground(line, bg);
|
|
162
|
+
const padding = Math.max(0, width - strip(normalized).length);
|
|
163
|
+
return `${bg}${normalized}${" ".repeat(padding)}${RST}`;
|
|
164
|
+
})
|
|
165
|
+
.join("\n");
|
|
166
|
+
}
|
|
167
|
+
|
|
129
168
|
function termW(): number {
|
|
169
|
+
const stderrWithColumns = process.stderr as NodeJS.WriteStream & { columns?: number };
|
|
130
170
|
const raw =
|
|
131
|
-
process.stdout.columns ||
|
|
171
|
+
process.stdout.columns || stderrWithColumns.columns || Number.parseInt(process.env.COLUMNS ?? "", 10) || 200;
|
|
132
172
|
return Math.max(80, Math.min(raw - 4, 210));
|
|
133
173
|
}
|
|
134
174
|
|
|
@@ -424,7 +464,6 @@ const USE_ICONS = ICONS_MODE !== "none" && ICONS_MODE !== "off";
|
|
|
424
464
|
|
|
425
465
|
// Nerd Font codepoints + ANSI color per file type
|
|
426
466
|
const NF_DIR = `${FG_BLUE}\ue5ff${RST}`; // folder
|
|
427
|
-
const NF_DIR_OPEN = `${FG_BLUE}\ue5fe${RST}`; // folder open
|
|
428
467
|
const NF_DEFAULT = `${FG_DIM}\uf15b${RST}`; // generic file
|
|
429
468
|
|
|
430
469
|
const EXT_ICON: Record<string, string> = {
|
|
@@ -644,7 +683,7 @@ function renderBashOutput(text: string, exitCode: number | null): { summary: str
|
|
|
644
683
|
}
|
|
645
684
|
|
|
646
685
|
/** Render ls output as a tree view with icons. */
|
|
647
|
-
function renderTree(text: string,
|
|
686
|
+
function renderTree(text: string, _basePath: string): string {
|
|
648
687
|
const lines = text.trim().split("\n").filter(Boolean);
|
|
649
688
|
if (!lines.length) return `${FG_DIM}(empty directory)${RST}`;
|
|
650
689
|
|
|
@@ -687,7 +726,8 @@ function renderFindResults(text: string): string {
|
|
|
687
726
|
const dir = dirname(trimmed) || ".";
|
|
688
727
|
const file = basename(trimmed);
|
|
689
728
|
if (!groups.has(dir)) groups.set(dir, []);
|
|
690
|
-
groups.get(dir)
|
|
729
|
+
const bucket = groups.get(dir);
|
|
730
|
+
if (bucket) bucket.push(file);
|
|
691
731
|
}
|
|
692
732
|
|
|
693
733
|
const out: string[] = [];
|
|
@@ -717,7 +757,6 @@ async function renderGrepResults(text: string, pattern: string): Promise<string>
|
|
|
717
757
|
const lines = text.split("\n");
|
|
718
758
|
if (!lines.length || (lines.length === 1 && !lines[0].trim())) return `${FG_DIM}(no matches)${RST}`;
|
|
719
759
|
|
|
720
|
-
const tw = termW();
|
|
721
760
|
const out: string[] = [];
|
|
722
761
|
let currentFile = "";
|
|
723
762
|
let count = 0;
|
|
@@ -773,14 +812,140 @@ async function renderGrepResults(text: string, pattern: string): Promise<string>
|
|
|
773
812
|
// If not, falls back to wrapping SDK tools (current behavior).
|
|
774
813
|
// ---------------------------------------------------------------------------
|
|
775
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
|
+
|
|
776
937
|
const _cursorStore = new CursorStore();
|
|
777
|
-
let _fffModule:
|
|
778
|
-
let _fffFinder:
|
|
938
|
+
let _fffModule: OptionalFffModule | null = null;
|
|
939
|
+
let _fffFinder: FffBackedFinder | null = null;
|
|
779
940
|
let _fffPartialIndex = false;
|
|
780
941
|
let _fffDbDir: string | null = null;
|
|
781
942
|
const FFF_SCAN_TIMEOUT = 15_000;
|
|
782
943
|
|
|
783
|
-
|
|
944
|
+
function getPiPrettyFffDir(agentDir: string): string {
|
|
945
|
+
return join(agentDir, "pi-pretty", "fff");
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
async function fffEnsureFinder(cwd: string): Promise<FffBackedFinder | null> {
|
|
784
949
|
if (_fffFinder && !_fffFinder.isDestroyed) return _fffFinder;
|
|
785
950
|
if (!_fffModule || !_fffDbDir) return null;
|
|
786
951
|
|
|
@@ -817,20 +982,20 @@ function fffDestroy(): void {
|
|
|
817
982
|
* In production, omit `deps` — the extension uses require() to load them.
|
|
818
983
|
*/
|
|
819
984
|
export interface PiPrettyDeps {
|
|
820
|
-
sdk:
|
|
821
|
-
TextComponent:
|
|
822
|
-
fffModule?:
|
|
985
|
+
sdk: PiPrettySdk;
|
|
986
|
+
TextComponent: TextComponentCtor;
|
|
987
|
+
fffModule?: OptionalFffModule;
|
|
823
988
|
}
|
|
824
989
|
|
|
825
|
-
export default function piPrettyExtension(pi:
|
|
826
|
-
let createReadTool:
|
|
827
|
-
let createBashTool:
|
|
828
|
-
let createLsTool:
|
|
829
|
-
let createFindTool:
|
|
830
|
-
let createGrepTool:
|
|
831
|
-
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;
|
|
832
997
|
|
|
833
|
-
let sdk:
|
|
998
|
+
let sdk: PiPrettySdk;
|
|
834
999
|
|
|
835
1000
|
if (deps) {
|
|
836
1001
|
// Test path: use injected dependencies, reset module state
|
|
@@ -868,13 +1033,13 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
868
1033
|
// FFF initialization (optional — graceful fallback to SDK)
|
|
869
1034
|
// ===================================================================
|
|
870
1035
|
|
|
871
|
-
const getAgentDir =
|
|
1036
|
+
const getAgentDir = sdk.getAgentDir;
|
|
872
1037
|
if (!deps) {
|
|
873
1038
|
// Only try require() in production — tests inject fffModule via deps
|
|
874
1039
|
try {
|
|
875
1040
|
_fffModule = require("@ff-labs/fff-node");
|
|
876
1041
|
if (getAgentDir) {
|
|
877
|
-
_fffDbDir =
|
|
1042
|
+
_fffDbDir = getPiPrettyFffDir(getAgentDir());
|
|
878
1043
|
try {
|
|
879
1044
|
mkdirSync(_fffDbDir, { recursive: true });
|
|
880
1045
|
} catch {}
|
|
@@ -883,25 +1048,25 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
883
1048
|
/* FFF not installed — SDK tools will be used */
|
|
884
1049
|
}
|
|
885
1050
|
} else if (_fffModule && getAgentDir) {
|
|
886
|
-
_fffDbDir =
|
|
1051
|
+
_fffDbDir = getPiPrettyFffDir(getAgentDir());
|
|
887
1052
|
try {
|
|
888
1053
|
mkdirSync(_fffDbDir, { recursive: true });
|
|
889
1054
|
} catch {}
|
|
890
1055
|
}
|
|
891
1056
|
|
|
892
|
-
pi.on("session_start", async (_event
|
|
1057
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
893
1058
|
// Try dynamic import if sync require failed (ESM-only package)
|
|
894
1059
|
if (!_fffModule) {
|
|
895
1060
|
try {
|
|
896
|
-
|
|
897
|
-
_fffModule =
|
|
1061
|
+
const imported = await import("@ff-labs/fff-node");
|
|
1062
|
+
_fffModule = { FileFinder: imported.FileFinder };
|
|
898
1063
|
} catch {}
|
|
899
1064
|
}
|
|
900
1065
|
if (!_fffModule) return;
|
|
901
1066
|
|
|
902
1067
|
if (!_fffDbDir) {
|
|
903
1068
|
const agentDir = getAgentDir?.() ?? join(home, ".pi/agent");
|
|
904
|
-
_fffDbDir =
|
|
1069
|
+
_fffDbDir = getPiPrettyFffDir(agentDir);
|
|
905
1070
|
try {
|
|
906
1071
|
mkdirSync(_fffDbDir, { recursive: true });
|
|
907
1072
|
} catch {}
|
|
@@ -915,8 +1080,8 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
915
1080
|
ctx.ui?.setStatus?.("fff", "FFF indexed");
|
|
916
1081
|
setTimeout(() => ctx.ui?.setStatus?.("fff", undefined), 3000);
|
|
917
1082
|
}
|
|
918
|
-
} catch (
|
|
919
|
-
ctx.ui?.notify?.(`FFF init failed: ${
|
|
1083
|
+
} catch (error: unknown) {
|
|
1084
|
+
ctx.ui?.notify?.(`FFF init failed: ${getErrorMessage(error)}`, "error");
|
|
920
1085
|
}
|
|
921
1086
|
});
|
|
922
1087
|
|
|
@@ -934,69 +1099,68 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
934
1099
|
...origRead,
|
|
935
1100
|
name: "read",
|
|
936
1101
|
|
|
937
|
-
async execute(
|
|
938
|
-
|
|
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;
|
|
939
1110
|
|
|
940
1111
|
const fp = params.path ?? "";
|
|
941
1112
|
const offset = params.offset ?? 1;
|
|
942
1113
|
|
|
943
|
-
|
|
944
|
-
const imageBlock = result.content?.find((c: any) => c.type === "image");
|
|
1114
|
+
const imageBlock = result.content?.find(isImageContent);
|
|
945
1115
|
if (imageBlock) {
|
|
946
|
-
(result
|
|
1116
|
+
setResultDetails(result, {
|
|
947
1117
|
_type: "readImage",
|
|
948
1118
|
filePath: fp,
|
|
949
1119
|
data: imageBlock.data,
|
|
950
1120
|
mimeType: imageBlock.mimeType ?? "image/png",
|
|
951
|
-
};
|
|
1121
|
+
});
|
|
952
1122
|
return result;
|
|
953
1123
|
}
|
|
954
1124
|
|
|
955
|
-
|
|
956
|
-
const textContent = result.content
|
|
957
|
-
?.filter((c: any) => c.type === "text")
|
|
958
|
-
.map((c: any) => c.text || "")
|
|
959
|
-
.join("\n");
|
|
960
|
-
|
|
1125
|
+
const textContent = getTextContent(result);
|
|
961
1126
|
if (textContent && fp) {
|
|
962
1127
|
const lineCount = textContent.split("\n").length;
|
|
963
|
-
(result
|
|
1128
|
+
setResultDetails(result, {
|
|
964
1129
|
_type: "readFile",
|
|
965
1130
|
filePath: fp,
|
|
966
1131
|
content: textContent,
|
|
967
1132
|
offset,
|
|
968
1133
|
lineCount,
|
|
969
|
-
};
|
|
1134
|
+
});
|
|
970
1135
|
}
|
|
971
1136
|
|
|
972
1137
|
return result;
|
|
973
1138
|
},
|
|
974
1139
|
|
|
975
|
-
renderCall(args:
|
|
1140
|
+
renderCall(args: ReadParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
976
1141
|
resolveBaseBackground(theme);
|
|
977
|
-
const fp = args
|
|
1142
|
+
const fp = args.path ?? "";
|
|
978
1143
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
979
|
-
const offset = args
|
|
980
|
-
const limit = args
|
|
981
|
-
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
|
+
);
|
|
982
1151
|
return text;
|
|
983
1152
|
},
|
|
984
1153
|
|
|
985
|
-
renderResult(result:
|
|
1154
|
+
renderResult(result: ToolResultLike, _opt: unknown, theme: ThemeLike, ctx: RenderContextLike) {
|
|
986
1155
|
resolveBaseBackground(theme);
|
|
987
1156
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
988
1157
|
|
|
989
1158
|
if (ctx.isError) {
|
|
990
|
-
|
|
991
|
-
result.content
|
|
992
|
-
?.filter((c: any) => c.type === "text")
|
|
993
|
-
.map((c: any) => c.text || "")
|
|
994
|
-
.join("\n") ?? "Error";
|
|
995
|
-
text.setText(`\n${theme.fg("error", e)}`);
|
|
1159
|
+
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
996
1160
|
return text;
|
|
997
1161
|
}
|
|
998
1162
|
|
|
999
|
-
const d = result.details;
|
|
1163
|
+
const d = result.details as RenderDetails | undefined;
|
|
1000
1164
|
|
|
1001
1165
|
// Image rendering
|
|
1002
1166
|
if (d?._type === "readImage") {
|
|
@@ -1016,7 +1180,9 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1016
1180
|
out.push(` ${FG_YELLOW}${passthroughWarning}${RST}`);
|
|
1017
1181
|
} else if (protocol === "kitty") {
|
|
1018
1182
|
if (d.mimeType && d.mimeType !== "image/png") {
|
|
1019
|
-
out.push(
|
|
1183
|
+
out.push(
|
|
1184
|
+
` ${FG_YELLOW}Kitty/Ghostty inline preview currently supports PNG payloads (got ${d.mimeType})${RST}`,
|
|
1185
|
+
);
|
|
1020
1186
|
} else {
|
|
1021
1187
|
const imgCols = Math.min(tw - 4, 80);
|
|
1022
1188
|
out.push(renderKittyImage(d.data, { cols: imgCols }));
|
|
@@ -1034,7 +1200,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1034
1200
|
}
|
|
1035
1201
|
|
|
1036
1202
|
out.push(rule(tw));
|
|
1037
|
-
text.setText(out.join("\n"));
|
|
1203
|
+
text.setText(fillToolBackground(out.join("\n")));
|
|
1038
1204
|
return text;
|
|
1039
1205
|
}
|
|
1040
1206
|
|
|
@@ -1043,24 +1209,25 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1043
1209
|
if (ctx.state._rk !== key) {
|
|
1044
1210
|
ctx.state._rk = key;
|
|
1045
1211
|
const info = `${FG_DIM}${d.lineCount} lines${RST}`;
|
|
1046
|
-
ctx.state._rt = ` ${info}
|
|
1212
|
+
ctx.state._rt = fillToolBackground(` ${info}`);
|
|
1047
1213
|
|
|
1048
1214
|
const maxShow = ctx.expanded ? d.lineCount : MAX_PREVIEW_LINES;
|
|
1049
1215
|
renderFileContent(d.content, d.filePath, d.offset, maxShow)
|
|
1050
1216
|
.then((rendered: string) => {
|
|
1051
1217
|
if (ctx.state._rk !== key) return;
|
|
1052
|
-
ctx.state._rt = ` ${info}\n${rendered}
|
|
1218
|
+
ctx.state._rt = fillToolBackground(` ${info}\n${rendered}`);
|
|
1053
1219
|
ctx.invalidate();
|
|
1054
1220
|
})
|
|
1055
1221
|
.catch(() => {});
|
|
1056
1222
|
}
|
|
1057
|
-
text.setText(ctx.state._rt ?? ` ${FG_DIM}${d.lineCount} lines${RST}`);
|
|
1223
|
+
text.setText(ctx.state._rt ?? fillToolBackground(` ${FG_DIM}${d.lineCount} lines${RST}`));
|
|
1058
1224
|
return text;
|
|
1059
1225
|
}
|
|
1060
1226
|
|
|
1061
1227
|
// Fallback
|
|
1062
|
-
const fallback = result.content?.[0]
|
|
1063
|
-
|
|
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))}`));
|
|
1064
1231
|
return text;
|
|
1065
1232
|
},
|
|
1066
1233
|
});
|
|
@@ -1076,69 +1243,65 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1076
1243
|
...origBash,
|
|
1077
1244
|
name: "bash",
|
|
1078
1245
|
|
|
1079
|
-
async execute(
|
|
1080
|
-
|
|
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);
|
|
1081
1255
|
|
|
1082
|
-
const textContent = result.content
|
|
1083
|
-
?.filter((c: any) => c.type === "text")
|
|
1084
|
-
.map((c: any) => c.text || "")
|
|
1085
|
-
.join("\n");
|
|
1086
|
-
|
|
1087
|
-
// Try to extract exit code from the output
|
|
1088
1256
|
let exitCode: number | null = 0;
|
|
1089
1257
|
if (textContent) {
|
|
1090
1258
|
const exitMatch = textContent.match(/(?:exit code|exited with|exit status)[:\s]*(\d+)/i);
|
|
1091
1259
|
if (exitMatch) exitCode = Number(exitMatch[1]);
|
|
1092
|
-
// Check for common error indicators
|
|
1093
1260
|
if (textContent.includes("command not found") || textContent.includes("No such file")) {
|
|
1094
1261
|
exitCode = 1;
|
|
1095
1262
|
}
|
|
1096
1263
|
}
|
|
1097
1264
|
|
|
1098
|
-
(result
|
|
1265
|
+
setResultDetails(result, {
|
|
1099
1266
|
_type: "bashResult",
|
|
1100
1267
|
text: textContent ?? "",
|
|
1101
1268
|
exitCode,
|
|
1102
1269
|
command: params.command ?? "",
|
|
1103
|
-
};
|
|
1270
|
+
});
|
|
1104
1271
|
|
|
1105
1272
|
return result;
|
|
1106
1273
|
},
|
|
1107
1274
|
|
|
1108
|
-
renderCall(args:
|
|
1109
|
-
|
|
1110
|
-
const cmd = args
|
|
1275
|
+
renderCall(args: BashParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
1276
|
+
resolveBaseBackground(theme);
|
|
1277
|
+
const cmd = args.command ?? "";
|
|
1111
1278
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1112
|
-
const timeout = args
|
|
1279
|
+
const timeout = args.timeout ? ` ${theme.fg("muted", `(${args.timeout}s timeout)`)}` : "";
|
|
1113
1280
|
text.setText(
|
|
1114
|
-
|
|
1281
|
+
fillToolBackground(
|
|
1282
|
+
`${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg("accent", cmd.length > 80 ? `${cmd.slice(0, 77)}…` : cmd)}${timeout}`,
|
|
1283
|
+
),
|
|
1115
1284
|
);
|
|
1116
1285
|
return text;
|
|
1117
1286
|
},
|
|
1118
1287
|
|
|
1119
|
-
renderResult(result:
|
|
1120
|
-
|
|
1288
|
+
renderResult(result: ToolResultLike, _opt: unknown, theme: ThemeLike, ctx: RenderContextLike) {
|
|
1289
|
+
resolveBaseBackground(theme);
|
|
1121
1290
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1122
1291
|
|
|
1123
1292
|
if (ctx.isError) {
|
|
1124
|
-
|
|
1125
|
-
result.content
|
|
1126
|
-
?.filter((c: any) => c.type === "text")
|
|
1127
|
-
.map((c: any) => c.text || "")
|
|
1128
|
-
.join("\n") ?? "Error";
|
|
1129
|
-
text.setText(`\n${theme.fg("error", e)}`);
|
|
1293
|
+
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
1130
1294
|
return text;
|
|
1131
1295
|
}
|
|
1132
1296
|
|
|
1133
|
-
const d = result.details;
|
|
1297
|
+
const d = result.details as RenderDetails | undefined;
|
|
1134
1298
|
if (d?._type === "bashResult") {
|
|
1135
|
-
const { summary
|
|
1299
|
+
const { summary } = renderBashOutput(d.text, d.exitCode);
|
|
1136
1300
|
const lines = d.text.split("\n");
|
|
1137
1301
|
const lineCount = lines.length;
|
|
1138
1302
|
const lineInfo = lineCount > 1 ? ` ${FG_DIM}(${lineCount} lines)${RST}` : "";
|
|
1139
1303
|
const header = ` ${summary}${lineInfo}`;
|
|
1140
1304
|
|
|
1141
|
-
// Show output content
|
|
1142
1305
|
if (d.text.trim()) {
|
|
1143
1306
|
const maxShow = ctx.expanded ? lineCount : MAX_PREVIEW_LINES;
|
|
1144
1307
|
const show = lines.slice(0, maxShow);
|
|
@@ -1151,15 +1314,16 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1151
1314
|
if (lineCount > maxShow) {
|
|
1152
1315
|
out.push(`${FG_DIM} … ${lineCount - maxShow} more lines${RST}`);
|
|
1153
1316
|
}
|
|
1154
|
-
text.setText(out.join("\n"));
|
|
1317
|
+
text.setText(fillToolBackground(out.join("\n")));
|
|
1155
1318
|
} else {
|
|
1156
|
-
text.setText(header);
|
|
1319
|
+
text.setText(fillToolBackground(header));
|
|
1157
1320
|
}
|
|
1158
1321
|
return text;
|
|
1159
1322
|
}
|
|
1160
1323
|
|
|
1161
|
-
const fallback = result.content?.[0]
|
|
1162
|
-
|
|
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))}`));
|
|
1163
1327
|
return text;
|
|
1164
1328
|
},
|
|
1165
1329
|
});
|
|
@@ -1176,59 +1340,56 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1176
1340
|
...origLs,
|
|
1177
1341
|
name: "ls",
|
|
1178
1342
|
|
|
1179
|
-
async execute(
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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);
|
|
1187
1352
|
const fp = params.path ?? cwd;
|
|
1188
1353
|
const entryCount = textContent ? textContent.trim().split("\n").filter(Boolean).length : 0;
|
|
1189
1354
|
|
|
1190
|
-
(result
|
|
1355
|
+
setResultDetails(result, {
|
|
1191
1356
|
_type: "lsResult",
|
|
1192
1357
|
text: textContent ?? "",
|
|
1193
1358
|
path: fp,
|
|
1194
1359
|
entryCount,
|
|
1195
|
-
};
|
|
1360
|
+
});
|
|
1196
1361
|
|
|
1197
1362
|
return result;
|
|
1198
1363
|
},
|
|
1199
1364
|
|
|
1200
|
-
renderCall(args:
|
|
1201
|
-
|
|
1202
|
-
const fp = args
|
|
1365
|
+
renderCall(args: LsParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
1366
|
+
resolveBaseBackground(theme);
|
|
1367
|
+
const fp = args.path ?? ".";
|
|
1203
1368
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1204
|
-
text.setText(`${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", sp(fp))}`);
|
|
1369
|
+
text.setText(fillToolBackground(`${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", sp(fp))}`));
|
|
1205
1370
|
return text;
|
|
1206
1371
|
},
|
|
1207
1372
|
|
|
1208
|
-
renderResult(result:
|
|
1209
|
-
|
|
1373
|
+
renderResult(result: ToolResultLike, _opt: unknown, theme: ThemeLike, ctx: RenderContextLike) {
|
|
1374
|
+
resolveBaseBackground(theme);
|
|
1210
1375
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1211
1376
|
|
|
1212
1377
|
if (ctx.isError) {
|
|
1213
|
-
|
|
1214
|
-
result.content
|
|
1215
|
-
?.filter((c: any) => c.type === "text")
|
|
1216
|
-
.map((c: any) => c.text || "")
|
|
1217
|
-
.join("\n") ?? "Error";
|
|
1218
|
-
text.setText(`\n${theme.fg("error", e)}`);
|
|
1378
|
+
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
1219
1379
|
return text;
|
|
1220
1380
|
}
|
|
1221
1381
|
|
|
1222
|
-
const d = result.details;
|
|
1382
|
+
const d = result.details as RenderDetails | undefined;
|
|
1223
1383
|
if (d?._type === "lsResult" && d.text) {
|
|
1224
1384
|
const tree = renderTree(d.text, d.path);
|
|
1225
1385
|
const info = `${FG_DIM}${d.entryCount} entries${RST}`;
|
|
1226
|
-
text.setText(` ${info}\n${tree}`);
|
|
1386
|
+
text.setText(fillToolBackground(` ${info}\n${tree}`));
|
|
1227
1387
|
return text;
|
|
1228
1388
|
}
|
|
1229
1389
|
|
|
1230
|
-
const fallback = result.content?.[0]
|
|
1231
|
-
|
|
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))}`));
|
|
1232
1393
|
return text;
|
|
1233
1394
|
},
|
|
1234
1395
|
});
|
|
@@ -1245,37 +1406,36 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1245
1406
|
...origFind,
|
|
1246
1407
|
name: "find",
|
|
1247
1408
|
|
|
1248
|
-
async execute(
|
|
1409
|
+
async execute(
|
|
1410
|
+
tid: string,
|
|
1411
|
+
params: FindParams,
|
|
1412
|
+
sig: AbortSignal | undefined,
|
|
1413
|
+
upd: unknown,
|
|
1414
|
+
ctx: ExtensionContext,
|
|
1415
|
+
) {
|
|
1249
1416
|
// Try FFF first (frecency-ranked, SIMD-accelerated)
|
|
1250
1417
|
if (_fffFinder && !_fffFinder.isDestroyed) {
|
|
1251
1418
|
try {
|
|
1252
1419
|
const effectiveLimit = Math.max(1, params.limit ?? 200);
|
|
1253
|
-
let query = params.pattern
|
|
1420
|
+
let query = params.pattern;
|
|
1254
1421
|
if (params.path) query = `${params.path} ${query}`;
|
|
1255
1422
|
|
|
1256
1423
|
const searchResult = _fffFinder.fileSearch(query, { pageSize: effectiveLimit });
|
|
1257
1424
|
if (searchResult.ok) {
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
const matchCount = items.length;
|
|
1261
|
-
|
|
1425
|
+
const search: SearchResult = searchResult.value;
|
|
1426
|
+
const items: FileItem[] = search.items.slice(0, effectiveLimit);
|
|
1262
1427
|
const notices: string[] = [];
|
|
1263
1428
|
if (_fffPartialIndex) notices.push("Warning: partial file index");
|
|
1264
1429
|
if (items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
|
|
1265
|
-
if (
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
text: textContent,
|
|
1275
|
-
pattern: params.pattern ?? "",
|
|
1276
|
-
matchCount,
|
|
1277
|
-
},
|
|
1278
|
-
};
|
|
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
|
+
});
|
|
1279
1439
|
}
|
|
1280
1440
|
} catch {
|
|
1281
1441
|
/* fall through to SDK */
|
|
@@ -1283,45 +1443,42 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1283
1443
|
}
|
|
1284
1444
|
|
|
1285
1445
|
// SDK fallback
|
|
1286
|
-
const result = await origFind.execute(tid, params, sig, upd, ctx);
|
|
1287
|
-
|
|
1288
|
-
const textContent = result.content
|
|
1289
|
-
?.filter((c: any) => c.type === "text")
|
|
1290
|
-
.map((c: any) => c.text || "")
|
|
1291
|
-
.join("\n");
|
|
1292
|
-
|
|
1446
|
+
const result = await origFind.execute(tid, params, sig, upd as never, ctx);
|
|
1447
|
+
const textContent = getTextContent(result);
|
|
1293
1448
|
const matchCount = textContent ? textContent.trim().split("\n").filter(Boolean).length : 0;
|
|
1294
1449
|
|
|
1295
|
-
(result
|
|
1450
|
+
setResultDetails<FindResultDetails>(result, {
|
|
1296
1451
|
_type: "findResult",
|
|
1297
|
-
text: textContent
|
|
1298
|
-
pattern: params.pattern
|
|
1452
|
+
text: textContent,
|
|
1453
|
+
pattern: params.pattern,
|
|
1299
1454
|
matchCount,
|
|
1300
|
-
};
|
|
1455
|
+
});
|
|
1301
1456
|
|
|
1302
1457
|
return result;
|
|
1303
1458
|
},
|
|
1304
1459
|
|
|
1305
|
-
renderCall(args:
|
|
1306
|
-
|
|
1307
|
-
const pattern = args
|
|
1308
|
-
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)}`)}` : "";
|
|
1309
1464
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1310
|
-
text.setText(
|
|
1465
|
+
text.setText(
|
|
1466
|
+
fillToolBackground(`${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}${path}`),
|
|
1467
|
+
);
|
|
1311
1468
|
return text;
|
|
1312
1469
|
},
|
|
1313
1470
|
|
|
1314
|
-
renderResult(
|
|
1315
|
-
|
|
1471
|
+
renderResult(
|
|
1472
|
+
result: ToolResultLike<FindResultDetails>,
|
|
1473
|
+
_opt: ToolRenderResultOptions,
|
|
1474
|
+
theme: ThemeLike,
|
|
1475
|
+
ctx: RenderContextLike,
|
|
1476
|
+
) {
|
|
1477
|
+
resolveBaseBackground(theme);
|
|
1316
1478
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1317
1479
|
|
|
1318
1480
|
if (ctx.isError) {
|
|
1319
|
-
|
|
1320
|
-
result.content
|
|
1321
|
-
?.filter((c: any) => c.type === "text")
|
|
1322
|
-
.map((c: any) => c.text || "")
|
|
1323
|
-
.join("\n") ?? "Error";
|
|
1324
|
-
text.setText(`\n${theme.fg("error", e)}`);
|
|
1481
|
+
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
1325
1482
|
return text;
|
|
1326
1483
|
}
|
|
1327
1484
|
|
|
@@ -1329,12 +1486,13 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1329
1486
|
if (d?._type === "findResult" && d.text) {
|
|
1330
1487
|
const rendered = renderFindResults(d.text);
|
|
1331
1488
|
const info = `${FG_DIM}${d.matchCount} files${RST}`;
|
|
1332
|
-
text.setText(` ${info}\n${rendered}`);
|
|
1489
|
+
text.setText(fillToolBackground(` ${info}\n${rendered}`));
|
|
1333
1490
|
return text;
|
|
1334
1491
|
}
|
|
1335
1492
|
|
|
1336
|
-
const fallback = result.content?.[0]
|
|
1337
|
-
|
|
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))}`));
|
|
1338
1496
|
return text;
|
|
1339
1497
|
},
|
|
1340
1498
|
});
|
|
@@ -1351,19 +1509,23 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1351
1509
|
...origGrep,
|
|
1352
1510
|
name: "grep",
|
|
1353
1511
|
|
|
1354
|
-
async execute(
|
|
1512
|
+
async execute(
|
|
1513
|
+
tid: string,
|
|
1514
|
+
params: GrepParams,
|
|
1515
|
+
sig: AbortSignal | undefined,
|
|
1516
|
+
upd: unknown,
|
|
1517
|
+
ctx: ExtensionContext,
|
|
1518
|
+
) {
|
|
1355
1519
|
// Try FFF first (SIMD-accelerated, frecency-ranked)
|
|
1356
1520
|
if (_fffFinder && !_fffFinder.isDestroyed) {
|
|
1357
1521
|
try {
|
|
1358
1522
|
const effectiveLimit = Math.max(1, params.limit ?? 100);
|
|
1359
|
-
let query = params.pattern
|
|
1523
|
+
let query = params.pattern;
|
|
1360
1524
|
if (params.glob) query = `${params.glob} ${query}`;
|
|
1361
1525
|
else if (params.path) query = `${params.path} ${query}`;
|
|
1362
1526
|
|
|
1363
|
-
const mode = params.literal ? "plain" : "regex";
|
|
1364
|
-
|
|
1365
1527
|
const grepResult = _fffFinder.grep(query, {
|
|
1366
|
-
mode,
|
|
1528
|
+
mode: params.literal ? "plain" : "regex",
|
|
1367
1529
|
smartCase: !params.ignoreCase,
|
|
1368
1530
|
maxMatchesPerFile: Math.min(effectiveLimit, 50),
|
|
1369
1531
|
cursor: null,
|
|
@@ -1372,31 +1534,23 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1372
1534
|
});
|
|
1373
1535
|
|
|
1374
1536
|
if (grepResult.ok) {
|
|
1375
|
-
const
|
|
1376
|
-
let textContent = fffFormatGrepText(result.items, effectiveLimit);
|
|
1377
|
-
const matchCount = Math.min(result.items.length, effectiveLimit);
|
|
1378
|
-
|
|
1537
|
+
const grep: GrepResult = grepResult.value;
|
|
1379
1538
|
const notices: string[] = [];
|
|
1380
1539
|
if (_fffPartialIndex) notices.push("Warning: partial file index");
|
|
1381
|
-
if (
|
|
1382
|
-
if ((
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
if (result.nextCursor) {
|
|
1386
|
-
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);
|
|
1387
1544
|
notices.push(`More results available. Use cursor="${cursorId}" to continue`);
|
|
1388
1545
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
return {
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
matchCount,
|
|
1398
|
-
},
|
|
1399
|
-
};
|
|
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
|
+
});
|
|
1400
1554
|
}
|
|
1401
1555
|
} catch {
|
|
1402
1556
|
/* fall through to SDK */
|
|
@@ -1404,51 +1558,45 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1404
1558
|
}
|
|
1405
1559
|
|
|
1406
1560
|
// SDK fallback
|
|
1407
|
-
const result = await origGrep.execute(tid, params, sig, upd, ctx);
|
|
1408
|
-
|
|
1409
|
-
const
|
|
1410
|
-
?.filter((c: any) => c.type === "text")
|
|
1411
|
-
.map((c: any) => c.text || "")
|
|
1412
|
-
.join("\n");
|
|
1413
|
-
|
|
1414
|
-
const matchCount = textContent
|
|
1415
|
-
? textContent
|
|
1416
|
-
.trim()
|
|
1417
|
-
.split("\n")
|
|
1418
|
-
.filter((l: string) => l.match(/^.+?[:\-]\d+[:\-]/)).length
|
|
1419
|
-
: 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;
|
|
1420
1564
|
|
|
1421
|
-
(result
|
|
1565
|
+
setResultDetails<GrepResultDetails>(result, {
|
|
1422
1566
|
_type: "grepResult",
|
|
1423
|
-
text: textContent
|
|
1424
|
-
pattern: params.pattern
|
|
1567
|
+
text: textContent,
|
|
1568
|
+
pattern: params.pattern,
|
|
1425
1569
|
matchCount,
|
|
1426
|
-
};
|
|
1570
|
+
});
|
|
1427
1571
|
|
|
1428
1572
|
return result;
|
|
1429
1573
|
},
|
|
1430
1574
|
|
|
1431
|
-
renderCall(args:
|
|
1432
|
-
|
|
1433
|
-
const pattern = args
|
|
1434
|
-
const path = args
|
|
1435
|
-
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})`)}` : "";
|
|
1436
1580
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1437
|
-
text.setText(
|
|
1581
|
+
text.setText(
|
|
1582
|
+
fillToolBackground(
|
|
1583
|
+
`${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", pattern)}${path}${glob}`,
|
|
1584
|
+
),
|
|
1585
|
+
);
|
|
1438
1586
|
return text;
|
|
1439
1587
|
},
|
|
1440
1588
|
|
|
1441
|
-
renderResult(
|
|
1442
|
-
|
|
1589
|
+
renderResult(
|
|
1590
|
+
result: ToolResultLike<GrepResultDetails>,
|
|
1591
|
+
_opt: ToolRenderResultOptions,
|
|
1592
|
+
theme: ThemeLike,
|
|
1593
|
+
ctx: RenderContextLike<GrepRenderState>,
|
|
1594
|
+
) {
|
|
1595
|
+
resolveBaseBackground(theme);
|
|
1443
1596
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1444
1597
|
|
|
1445
1598
|
if (ctx.isError) {
|
|
1446
|
-
|
|
1447
|
-
result.content
|
|
1448
|
-
?.filter((c: any) => c.type === "text")
|
|
1449
|
-
.map((c: any) => c.text || "")
|
|
1450
|
-
.join("\n") ?? "Error";
|
|
1451
|
-
text.setText(`\n${theme.fg("error", e)}`);
|
|
1599
|
+
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
1452
1600
|
return text;
|
|
1453
1601
|
}
|
|
1454
1602
|
|
|
@@ -1458,22 +1606,23 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1458
1606
|
if (ctx.state._gk !== key) {
|
|
1459
1607
|
ctx.state._gk = key;
|
|
1460
1608
|
const info = `${FG_DIM}${d.matchCount} matches${RST}`;
|
|
1461
|
-
ctx.state._gt = ` ${info}
|
|
1609
|
+
ctx.state._gt = fillToolBackground(` ${info}`);
|
|
1462
1610
|
|
|
1463
1611
|
renderGrepResults(d.text, d.pattern)
|
|
1464
1612
|
.then((rendered: string) => {
|
|
1465
1613
|
if (ctx.state._gk !== key) return;
|
|
1466
|
-
ctx.state._gt = ` ${info}\n${rendered}
|
|
1614
|
+
ctx.state._gt = fillToolBackground(` ${info}\n${rendered}`);
|
|
1467
1615
|
ctx.invalidate();
|
|
1468
1616
|
})
|
|
1469
1617
|
.catch(() => {});
|
|
1470
1618
|
}
|
|
1471
|
-
text.setText(ctx.state._gt ?? ` ${FG_DIM}${d.matchCount} matches${RST}`);
|
|
1619
|
+
text.setText(ctx.state._gt ?? fillToolBackground(` ${FG_DIM}${d.matchCount} matches${RST}`));
|
|
1472
1620
|
return text;
|
|
1473
1621
|
}
|
|
1474
1622
|
|
|
1475
|
-
const fallback = result.content?.[0]
|
|
1476
|
-
|
|
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))}`));
|
|
1477
1626
|
return text;
|
|
1478
1627
|
},
|
|
1479
1628
|
});
|
|
@@ -1493,8 +1642,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1493
1642
|
"Patterns are literal text — never escape special characters.",
|
|
1494
1643
|
"Use the constraints parameter for file filtering ('*.rs', 'src/', '!test/').",
|
|
1495
1644
|
].join(" "),
|
|
1496
|
-
promptSnippet:
|
|
1497
|
-
"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)",
|
|
1498
1646
|
promptGuidelines: [
|
|
1499
1647
|
"Use multi_grep when you need to find multiple identifiers at once (OR logic).",
|
|
1500
1648
|
"Include all naming conventions: snake_case, PascalCase, camelCase variants.",
|
|
@@ -1526,21 +1674,21 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1526
1674
|
required: ["patterns"],
|
|
1527
1675
|
},
|
|
1528
1676
|
|
|
1529
|
-
async execute(
|
|
1530
|
-
|
|
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", {});
|
|
1531
1685
|
|
|
1532
1686
|
if (!params.patterns || params.patterns.length === 0) {
|
|
1533
|
-
return {
|
|
1534
|
-
content: [{ type: "text", text: "Error: patterns array must have at least 1 element" }],
|
|
1535
|
-
details: { error: "empty patterns" },
|
|
1536
|
-
};
|
|
1687
|
+
return makeTextResult("Error: patterns array must have at least 1 element", { error: "empty patterns" });
|
|
1537
1688
|
}
|
|
1538
1689
|
|
|
1539
1690
|
if (!_fffFinder || _fffFinder.isDestroyed) {
|
|
1540
|
-
return {
|
|
1541
|
-
content: [{ type: "text", text: "FFF not initialized. Wait for session start or run /fff-rescan." }],
|
|
1542
|
-
details: {},
|
|
1543
|
-
};
|
|
1691
|
+
return makeTextResult("FFF not initialized. Wait for session start or run /fff-rescan.", {});
|
|
1544
1692
|
}
|
|
1545
1693
|
|
|
1546
1694
|
try {
|
|
@@ -1557,68 +1705,61 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1557
1705
|
});
|
|
1558
1706
|
|
|
1559
1707
|
if (!grepResult.ok) {
|
|
1560
|
-
return {
|
|
1561
|
-
content: [{ type: "text", text: `multi_grep error: ${grepResult.error}` }],
|
|
1562
|
-
details: { error: grepResult.error },
|
|
1563
|
-
};
|
|
1708
|
+
return makeTextResult(`multi_grep error: ${grepResult.error}`, { error: grepResult.error });
|
|
1564
1709
|
}
|
|
1565
1710
|
|
|
1566
|
-
const
|
|
1567
|
-
let textContent = fffFormatGrepText(result.items, effectiveLimit);
|
|
1568
|
-
const matchCount = Math.min(result.items.length, effectiveLimit);
|
|
1569
|
-
|
|
1711
|
+
const grep: GrepResult = grepResult.value;
|
|
1570
1712
|
const notices: string[] = [];
|
|
1571
1713
|
if (_fffPartialIndex) notices.push("Warning: partial file index");
|
|
1572
|
-
if (
|
|
1573
|
-
if (
|
|
1574
|
-
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);
|
|
1575
1717
|
notices.push(`More results: cursor="${cursorId}"`);
|
|
1576
1718
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
return {
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
};
|
|
1588
|
-
} catch (e: any) {
|
|
1589
|
-
return {
|
|
1590
|
-
content: [{ type: "text", text: `multi_grep error: ${e.message}` }],
|
|
1591
|
-
details: { error: e.message },
|
|
1592
|
-
};
|
|
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 });
|
|
1593
1730
|
}
|
|
1594
1731
|
},
|
|
1595
1732
|
|
|
1596
|
-
renderCall(args:
|
|
1733
|
+
renderCall(args: MultiGrepParams, theme: ThemeLike, ctx: RenderContextLike) {
|
|
1597
1734
|
resolveBaseBackground(theme);
|
|
1598
|
-
const patterns = args
|
|
1599
|
-
const constraints = args
|
|
1735
|
+
const patterns = args.patterns ?? [];
|
|
1736
|
+
const constraints = args.constraints;
|
|
1600
1737
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1601
1738
|
let content =
|
|
1602
1739
|
theme.fg("toolTitle", theme.bold("multi_grep")) +
|
|
1603
1740
|
" " +
|
|
1604
|
-
theme.fg("accent", patterns.map((p
|
|
1741
|
+
theme.fg("accent", patterns.map((p) => `"${p}"`).join(", "));
|
|
1605
1742
|
if (constraints) content += theme.fg("muted", ` (${constraints})`);
|
|
1606
1743
|
text.setText(content);
|
|
1607
1744
|
return text;
|
|
1608
1745
|
},
|
|
1609
1746
|
|
|
1610
|
-
renderResult(
|
|
1747
|
+
renderResult(
|
|
1748
|
+
result: ToolResultLike<GrepResultDetails | { error?: string }>,
|
|
1749
|
+
_opt: ToolRenderResultOptions,
|
|
1750
|
+
theme: ThemeLike,
|
|
1751
|
+
ctx: RenderContextLike<MultiGrepRenderState>,
|
|
1752
|
+
) {
|
|
1611
1753
|
resolveBaseBackground(theme);
|
|
1612
1754
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1613
1755
|
|
|
1614
1756
|
if (ctx.isError) {
|
|
1615
|
-
|
|
1616
|
-
text.setText(`\n${theme.fg("error", e)}`);
|
|
1757
|
+
text.setText(`\n${theme.fg("error", getTextContent(result) || "Error")}`);
|
|
1617
1758
|
return text;
|
|
1618
1759
|
}
|
|
1619
1760
|
|
|
1620
1761
|
const d = result.details;
|
|
1621
|
-
if (d
|
|
1762
|
+
if (d && "_type" in d && d._type === "grepResult" && d.text) {
|
|
1622
1763
|
const key = `mgrep:${d.pattern}:${d.matchCount}:${termW()}`;
|
|
1623
1764
|
if (ctx.state._mgk !== key) {
|
|
1624
1765
|
ctx.state._mgk = key;
|
|
@@ -1637,8 +1778,9 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1637
1778
|
return text;
|
|
1638
1779
|
}
|
|
1639
1780
|
|
|
1640
|
-
const fallback = result.content?.[0]
|
|
1641
|
-
|
|
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))}`);
|
|
1642
1784
|
return text;
|
|
1643
1785
|
},
|
|
1644
1786
|
});
|
|
@@ -1651,7 +1793,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1651
1793
|
if (_fffModule) {
|
|
1652
1794
|
pi.registerCommand("fff-health", {
|
|
1653
1795
|
description: "Show FFF file finder health and indexer status",
|
|
1654
|
-
handler: async (_args:
|
|
1796
|
+
handler: async (_args: string, ctx: CommandContextLike) => {
|
|
1655
1797
|
if (!_fffFinder || _fffFinder.isDestroyed) {
|
|
1656
1798
|
ctx.ui?.notify?.("FFF not initialized", "warning");
|
|
1657
1799
|
return;
|
|
@@ -1675,7 +1817,9 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1675
1817
|
|
|
1676
1818
|
const progress = _fffFinder.getScanProgress();
|
|
1677
1819
|
if (progress.ok) {
|
|
1678
|
-
lines.push(
|
|
1820
|
+
lines.push(
|
|
1821
|
+
`Scanning: ${progress.value.isScanning ? "yes" : "no"} (${progress.value.scannedFilesCount} files)`,
|
|
1822
|
+
);
|
|
1679
1823
|
}
|
|
1680
1824
|
|
|
1681
1825
|
ctx.ui?.notify?.(lines.join("\n"), "info");
|
|
@@ -1684,7 +1828,7 @@ export default function piPrettyExtension(pi: any, deps?: PiPrettyDeps): void {
|
|
|
1684
1828
|
|
|
1685
1829
|
pi.registerCommand("fff-rescan", {
|
|
1686
1830
|
description: "Trigger FFF to rescan files",
|
|
1687
|
-
handler: async (_args:
|
|
1831
|
+
handler: async (_args: string, ctx: CommandContextLike) => {
|
|
1688
1832
|
if (!_fffFinder || _fffFinder.isDestroyed) {
|
|
1689
1833
|
ctx.ui?.notify?.("FFF not initialized", "warning");
|
|
1690
1834
|
return;
|