@heyhuynhgiabuu/pi-pretty 0.1.8 → 0.3.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 +4 -4
- package/package.json +7 -4
- package/src/index.ts +467 -4
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pi-pretty
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@heyhuynhgiabuu/pi-pretty)
|
|
4
|
-
[](https://github.com/buddingnewinsights/pi-pretty/releases/latest)
|
|
5
5
|
|
|
6
6
|
A [pi](https://pi.dev) extension that upgrades built-in tool output in the terminal without changing tool behavior.
|
|
7
7
|
|
|
@@ -11,7 +11,7 @@ It currently enhances:
|
|
|
11
11
|
- **`bash`**: colored exit summary (`exit 0`/`exit 1`) with a preview body of command output
|
|
12
12
|
- **`ls` / `find` / `grep`**: Nerd Font file icons with tree/grouped layouts and clearer match rendering
|
|
13
13
|
|
|
14
|
-
> Companion to [@heyhuynhgiabuu/pi-diff](https://github.com/
|
|
14
|
+
> Companion to [@heyhuynhgiabuu/pi-diff](https://github.com/buddingnewinsights/pi-diff) for `write`/`edit` diff rendering.
|
|
15
15
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
@@ -19,7 +19,7 @@ It currently enhances:
|
|
|
19
19
|
pi install npm:@heyhuynhgiabuu/pi-pretty
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Latest release: https://github.com/
|
|
22
|
+
Latest release: https://github.com/buddingnewinsights/pi-pretty/releases/latest
|
|
23
23
|
|
|
24
24
|
Or load locally:
|
|
25
25
|
|
|
@@ -64,4 +64,4 @@ npm test
|
|
|
64
64
|
|
|
65
65
|
## License
|
|
66
66
|
|
|
67
|
-
MIT — [huynhgiabuu](https://github.com/
|
|
67
|
+
MIT — [huynhgiabuu](https://github.com/buddingnewinsights)
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyhuynhgiabuu/pi-pretty",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Pretty terminal output for pi — syntax-highlighted file reads, colored bash output, tree-view directory listings, and more.",
|
|
5
5
|
"author": "huynhgiabuu",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/
|
|
9
|
+
"url": "git+https://github.com/buddingnewinsights/pi-pretty.git"
|
|
10
10
|
},
|
|
11
|
-
"homepage": "https://github.com/
|
|
11
|
+
"homepage": "https://github.com/buddingnewinsights/pi-pretty#readme",
|
|
12
12
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/
|
|
13
|
+
"url": "https://github.com/buddingnewinsights/pi-pretty/issues"
|
|
14
14
|
},
|
|
15
15
|
"keywords": [
|
|
16
16
|
"pi-package",
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@shikijs/cli": "^4.0.2"
|
|
26
26
|
},
|
|
27
|
+
"optionalDependencies": {
|
|
28
|
+
"@ff-labs/fff-node": "0.5.2"
|
|
29
|
+
},
|
|
27
30
|
"peerDependencies": {
|
|
28
31
|
"@mariozechner/pi-coding-agent": "*",
|
|
29
32
|
"@mariozechner/pi-tui": "*"
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* pi-pretty — Pretty terminal output for pi built-in tools.
|
|
3
3
|
*
|
|
4
4
|
* @module pi-pretty
|
|
5
|
-
* @see https://github.com/
|
|
5
|
+
* @see https://github.com/buddingnewinsights/pi-pretty
|
|
6
6
|
*
|
|
7
7
|
* Enhances:
|
|
8
8
|
* • read — syntax-highlighted file content with line numbers
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
* • Large-file fallback (skip highlighting, still show line numbers)
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { existsSync, statSync } from "node:fs";
|
|
27
|
-
import { basename, dirname, extname, relative } from "node:path";
|
|
26
|
+
import { existsSync, mkdirSync, statSync } from "node:fs";
|
|
27
|
+
import { basename, dirname, extname, join, relative } from "node:path";
|
|
28
28
|
|
|
29
29
|
import { codeToANSI } from "@shikijs/cli";
|
|
30
30
|
import type { BundledLanguage, BundledTheme } from "shiki";
|
|
@@ -669,6 +669,103 @@ async function renderGrepResults(text: string, pattern: string): Promise<string>
|
|
|
669
669
|
return out.join("\n");
|
|
670
670
|
}
|
|
671
671
|
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
673
|
+
// FFF integration (optional) — Fast File Finder with frecency & SIMD search
|
|
674
|
+
//
|
|
675
|
+
// If @ff-labs/fff-node is installed, find/grep use FFF for speed + frecency.
|
|
676
|
+
// If not, falls back to wrapping SDK tools (current behavior).
|
|
677
|
+
// ---------------------------------------------------------------------------
|
|
678
|
+
|
|
679
|
+
class CursorStore {
|
|
680
|
+
private cursors = new Map<string, any>();
|
|
681
|
+
private counter = 0;
|
|
682
|
+
|
|
683
|
+
store(cursor: any): string {
|
|
684
|
+
const id = `fff_c${++this.counter}`;
|
|
685
|
+
this.cursors.set(id, cursor);
|
|
686
|
+
if (this.cursors.size > 200) {
|
|
687
|
+
const first = this.cursors.keys().next().value;
|
|
688
|
+
if (first) this.cursors.delete(first);
|
|
689
|
+
}
|
|
690
|
+
return id;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
get(id: string): any | undefined {
|
|
694
|
+
return this.cursors.get(id);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const _cursorStore = new CursorStore();
|
|
699
|
+
let _fffModule: any = null;
|
|
700
|
+
let _fffFinder: any = null;
|
|
701
|
+
let _fffPartialIndex = false;
|
|
702
|
+
let _fffDbDir: string | null = null;
|
|
703
|
+
const FFF_SCAN_TIMEOUT = 15_000;
|
|
704
|
+
|
|
705
|
+
async function fffEnsureFinder(cwd: string): Promise<any> {
|
|
706
|
+
if (_fffFinder && !_fffFinder.isDestroyed) return _fffFinder;
|
|
707
|
+
if (!_fffModule || !_fffDbDir) return null;
|
|
708
|
+
|
|
709
|
+
const result = _fffModule.FileFinder.create({
|
|
710
|
+
basePath: cwd,
|
|
711
|
+
frecencyDbPath: join(_fffDbDir, "frecency.mdb"),
|
|
712
|
+
historyDbPath: join(_fffDbDir, "history.mdb"),
|
|
713
|
+
aiMode: true,
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
if (!result.ok) throw new Error(`FFF init failed: ${result.error}`);
|
|
717
|
+
|
|
718
|
+
_fffFinder = result.value;
|
|
719
|
+
const scan = await _fffFinder.waitForScan(FFF_SCAN_TIMEOUT);
|
|
720
|
+
_fffPartialIndex = scan.ok && !scan.value;
|
|
721
|
+
|
|
722
|
+
return _fffFinder;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function fffDestroy(): void {
|
|
726
|
+
if (_fffFinder && !_fffFinder.isDestroyed) {
|
|
727
|
+
_fffFinder.destroy();
|
|
728
|
+
_fffFinder = null;
|
|
729
|
+
}
|
|
730
|
+
_fffPartialIndex = false;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Convert FFF GrepResult items to ripgrep-style "file:line:content" text.
|
|
735
|
+
* This ensures pi-pretty's renderGrepResults works unchanged.
|
|
736
|
+
*/
|
|
737
|
+
function fffFormatGrepText(items: any[], limit: number): string {
|
|
738
|
+
const capped = items.slice(0, limit);
|
|
739
|
+
if (!capped.length) return "No matches found";
|
|
740
|
+
|
|
741
|
+
const lines: string[] = [];
|
|
742
|
+
let currentFile = "";
|
|
743
|
+
|
|
744
|
+
for (const match of capped) {
|
|
745
|
+
if (match.relativePath !== currentFile) {
|
|
746
|
+
if (currentFile) lines.push("");
|
|
747
|
+
currentFile = match.relativePath;
|
|
748
|
+
}
|
|
749
|
+
if (match.contextBefore?.length) {
|
|
750
|
+
const startLine = match.lineNumber - match.contextBefore.length;
|
|
751
|
+
for (let i = 0; i < match.contextBefore.length; i++) {
|
|
752
|
+
lines.push(`${match.relativePath}-${startLine + i}-${match.contextBefore[i]}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const content =
|
|
756
|
+
match.lineContent.length > 500 ? `${match.lineContent.slice(0, 500)}...` : match.lineContent;
|
|
757
|
+
lines.push(`${match.relativePath}:${match.lineNumber}:${content}`);
|
|
758
|
+
if (match.contextAfter?.length) {
|
|
759
|
+
const startLine = match.lineNumber + 1;
|
|
760
|
+
for (let i = 0; i < match.contextAfter.length; i++) {
|
|
761
|
+
lines.push(`${match.relativePath}-${startLine + i}-${match.contextAfter[i]}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return lines.join("\n");
|
|
767
|
+
}
|
|
768
|
+
|
|
672
769
|
// ---------------------------------------------------------------------------
|
|
673
770
|
// Extension entry point
|
|
674
771
|
// ---------------------------------------------------------------------------
|
|
@@ -681,8 +778,10 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
681
778
|
let createGrepTool: any;
|
|
682
779
|
let TextComponent: any;
|
|
683
780
|
|
|
781
|
+
let sdk: any;
|
|
782
|
+
|
|
684
783
|
try {
|
|
685
|
-
|
|
784
|
+
sdk = require("@mariozechner/pi-coding-agent");
|
|
686
785
|
createReadTool = sdk.createReadToolDefinition ?? sdk.createReadTool;
|
|
687
786
|
createBashTool = sdk.createBashToolDefinition ?? sdk.createBashTool;
|
|
688
787
|
createLsTool = sdk.createLsToolDefinition ?? sdk.createLsTool;
|
|
@@ -698,6 +797,58 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
698
797
|
const home = process.env.HOME ?? "";
|
|
699
798
|
const sp = (p: string) => shortPath(cwd, home, p);
|
|
700
799
|
|
|
800
|
+
// ===================================================================
|
|
801
|
+
// FFF initialization (optional — graceful fallback to SDK)
|
|
802
|
+
// ===================================================================
|
|
803
|
+
|
|
804
|
+
const getAgentDir = (sdk as any).getAgentDir;
|
|
805
|
+
try {
|
|
806
|
+
_fffModule = require("@ff-labs/fff-node");
|
|
807
|
+
if (getAgentDir) {
|
|
808
|
+
_fffDbDir = join(getAgentDir(), "fff");
|
|
809
|
+
try {
|
|
810
|
+
mkdirSync(_fffDbDir, { recursive: true });
|
|
811
|
+
} catch {}
|
|
812
|
+
}
|
|
813
|
+
} catch {
|
|
814
|
+
/* FFF not installed — SDK tools will be used */
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
pi.on("session_start", async (_event: any, ctx: any) => {
|
|
818
|
+
// Try dynamic import if sync require failed (ESM-only package)
|
|
819
|
+
if (!_fffModule) {
|
|
820
|
+
try {
|
|
821
|
+
// @ts-ignore — optional dependency, may not be installed
|
|
822
|
+
_fffModule = await import("@ff-labs/fff-node");
|
|
823
|
+
} catch {}
|
|
824
|
+
}
|
|
825
|
+
if (!_fffModule) return;
|
|
826
|
+
|
|
827
|
+
if (!_fffDbDir) {
|
|
828
|
+
const agentDir = getAgentDir?.() ?? join(home, ".pi/agent");
|
|
829
|
+
_fffDbDir = join(agentDir, "fff");
|
|
830
|
+
try {
|
|
831
|
+
mkdirSync(_fffDbDir, { recursive: true });
|
|
832
|
+
} catch {}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
try {
|
|
836
|
+
await fffEnsureFinder(ctx.cwd);
|
|
837
|
+
if (_fffPartialIndex) {
|
|
838
|
+
ctx.ui?.notify?.("FFF: scan timed out — using partial index. Run /fff-rescan when ready.", "warning");
|
|
839
|
+
} else {
|
|
840
|
+
ctx.ui?.setStatus?.("fff", "FFF indexed");
|
|
841
|
+
setTimeout(() => ctx.ui?.setStatus?.("fff", undefined), 3000);
|
|
842
|
+
}
|
|
843
|
+
} catch (e: any) {
|
|
844
|
+
ctx.ui?.notify?.(`FFF init failed: ${e.message}`, "error");
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
pi.on("session_shutdown", async () => {
|
|
849
|
+
fffDestroy();
|
|
850
|
+
});
|
|
851
|
+
|
|
701
852
|
// ===================================================================
|
|
702
853
|
// read — syntax-highlighted file content
|
|
703
854
|
// ===================================================================
|
|
@@ -1013,6 +1164,43 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
1013
1164
|
name: "find",
|
|
1014
1165
|
|
|
1015
1166
|
async execute(tid: string, params: any, sig: any, upd: any, ctx: any) {
|
|
1167
|
+
// Try FFF first (frecency-ranked, SIMD-accelerated)
|
|
1168
|
+
if (_fffFinder && !_fffFinder.isDestroyed) {
|
|
1169
|
+
try {
|
|
1170
|
+
const effectiveLimit = Math.max(1, params.limit ?? 200);
|
|
1171
|
+
let query = params.pattern ?? "";
|
|
1172
|
+
if (params.path) query = `${params.path} ${query}`;
|
|
1173
|
+
|
|
1174
|
+
const searchResult = _fffFinder.fileSearch(query, { pageSize: effectiveLimit });
|
|
1175
|
+
if (searchResult.ok) {
|
|
1176
|
+
const items = searchResult.value.items.slice(0, effectiveLimit);
|
|
1177
|
+
let textContent = items.map((i: any) => i.relativePath).join("\n");
|
|
1178
|
+
const matchCount = items.length;
|
|
1179
|
+
|
|
1180
|
+
const notices: string[] = [];
|
|
1181
|
+
if (_fffPartialIndex) notices.push("Warning: partial file index");
|
|
1182
|
+
if (items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
|
|
1183
|
+
if (searchResult.value.totalMatched > items.length) {
|
|
1184
|
+
notices.push(`${searchResult.value.totalMatched} total matches`);
|
|
1185
|
+
}
|
|
1186
|
+
if (notices.length) textContent += `\n\n[${notices.join(". ")}]`;
|
|
1187
|
+
|
|
1188
|
+
return {
|
|
1189
|
+
content: [{ type: "text", text: textContent }],
|
|
1190
|
+
details: {
|
|
1191
|
+
_type: "findResult",
|
|
1192
|
+
text: textContent,
|
|
1193
|
+
pattern: params.pattern ?? "",
|
|
1194
|
+
matchCount,
|
|
1195
|
+
},
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
} catch {
|
|
1199
|
+
/* fall through to SDK */
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// SDK fallback
|
|
1016
1204
|
const result = await origFind.execute(tid, params, sig, upd, ctx);
|
|
1017
1205
|
|
|
1018
1206
|
const textContent = result.content
|
|
@@ -1082,6 +1270,58 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
1082
1270
|
name: "grep",
|
|
1083
1271
|
|
|
1084
1272
|
async execute(tid: string, params: any, sig: any, upd: any, ctx: any) {
|
|
1273
|
+
// Try FFF first (SIMD-accelerated, frecency-ranked)
|
|
1274
|
+
if (_fffFinder && !_fffFinder.isDestroyed) {
|
|
1275
|
+
try {
|
|
1276
|
+
const effectiveLimit = Math.max(1, params.limit ?? 100);
|
|
1277
|
+
let query = params.pattern ?? "";
|
|
1278
|
+
if (params.glob) query = `${params.glob} ${query}`;
|
|
1279
|
+
else if (params.path) query = `${params.path} ${query}`;
|
|
1280
|
+
|
|
1281
|
+
const mode = params.literal ? "plain" : "regex";
|
|
1282
|
+
|
|
1283
|
+
const grepResult = _fffFinder.grep(query, {
|
|
1284
|
+
mode,
|
|
1285
|
+
smartCase: !params.ignoreCase,
|
|
1286
|
+
maxMatchesPerFile: Math.min(effectiveLimit, 50),
|
|
1287
|
+
cursor: null,
|
|
1288
|
+
beforeContext: params.context ?? 0,
|
|
1289
|
+
afterContext: params.context ?? 0,
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
if (grepResult.ok) {
|
|
1293
|
+
const result = grepResult.value;
|
|
1294
|
+
let textContent = fffFormatGrepText(result.items, effectiveLimit);
|
|
1295
|
+
const matchCount = Math.min(result.items.length, effectiveLimit);
|
|
1296
|
+
|
|
1297
|
+
const notices: string[] = [];
|
|
1298
|
+
if (_fffPartialIndex) notices.push("Warning: partial file index");
|
|
1299
|
+
if (result.items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
|
|
1300
|
+
if ((result as any).regexFallbackError) {
|
|
1301
|
+
notices.push(`Regex failed: ${(result as any).regexFallbackError}, used literal match`);
|
|
1302
|
+
}
|
|
1303
|
+
if (result.nextCursor) {
|
|
1304
|
+
const cursorId = _cursorStore.store(result.nextCursor);
|
|
1305
|
+
notices.push(`More results available. Use cursor="${cursorId}" to continue`);
|
|
1306
|
+
}
|
|
1307
|
+
if (notices.length) textContent += `\n\n[${notices.join(". ")}]`;
|
|
1308
|
+
|
|
1309
|
+
return {
|
|
1310
|
+
content: [{ type: "text", text: textContent }],
|
|
1311
|
+
details: {
|
|
1312
|
+
_type: "grepResult",
|
|
1313
|
+
text: textContent,
|
|
1314
|
+
pattern: params.pattern ?? "",
|
|
1315
|
+
matchCount,
|
|
1316
|
+
},
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
} catch {
|
|
1320
|
+
/* fall through to SDK */
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// SDK fallback
|
|
1085
1325
|
const result = await origGrep.execute(tid, params, sig, upd, ctx);
|
|
1086
1326
|
|
|
1087
1327
|
const textContent = result.content
|
|
@@ -1156,4 +1396,227 @@ export default function piPrettyExtension(pi: any): void {
|
|
|
1156
1396
|
},
|
|
1157
1397
|
});
|
|
1158
1398
|
}
|
|
1399
|
+
|
|
1400
|
+
// ===================================================================
|
|
1401
|
+
// multi_grep — FFF-only OR-logic multi-pattern search
|
|
1402
|
+
// ===================================================================
|
|
1403
|
+
|
|
1404
|
+
if (_fffModule) {
|
|
1405
|
+
pi.registerTool({
|
|
1406
|
+
name: "multi_grep",
|
|
1407
|
+
label: "multi_grep (fff)",
|
|
1408
|
+
description: [
|
|
1409
|
+
"Search file contents for lines matching ANY of multiple patterns (OR logic).",
|
|
1410
|
+
"Uses SIMD-accelerated Aho-Corasick multi-pattern matching. Faster than regex alternation.",
|
|
1411
|
+
"Patterns are literal text — never escape special characters.",
|
|
1412
|
+
"Use the constraints parameter for file filtering ('*.rs', 'src/', '!test/').",
|
|
1413
|
+
].join(" "),
|
|
1414
|
+
promptSnippet:
|
|
1415
|
+
"Multi-pattern OR search across file contents (FFF: SIMD-accelerated, frecency-ranked)",
|
|
1416
|
+
promptGuidelines: [
|
|
1417
|
+
"Use multi_grep when you need to find multiple identifiers at once (OR logic).",
|
|
1418
|
+
"Include all naming conventions: snake_case, PascalCase, camelCase variants.",
|
|
1419
|
+
"Patterns are literal text. Never escape special characters.",
|
|
1420
|
+
"Use the constraints parameter for file type/path filtering, not inside patterns.",
|
|
1421
|
+
],
|
|
1422
|
+
|
|
1423
|
+
parameters: {
|
|
1424
|
+
type: "object",
|
|
1425
|
+
properties: {
|
|
1426
|
+
patterns: {
|
|
1427
|
+
type: "array",
|
|
1428
|
+
items: { type: "string" },
|
|
1429
|
+
description: "Patterns to search for (OR logic — matches lines containing ANY pattern).",
|
|
1430
|
+
},
|
|
1431
|
+
constraints: {
|
|
1432
|
+
type: "string",
|
|
1433
|
+
description: "File constraints, e.g. '*.{ts,tsx} !test/' to filter files.",
|
|
1434
|
+
},
|
|
1435
|
+
context: {
|
|
1436
|
+
type: "number",
|
|
1437
|
+
description: "Number of context lines before and after each match (default: 0)",
|
|
1438
|
+
},
|
|
1439
|
+
limit: {
|
|
1440
|
+
type: "number",
|
|
1441
|
+
description: "Maximum number of matches to return (default: 100)",
|
|
1442
|
+
},
|
|
1443
|
+
},
|
|
1444
|
+
required: ["patterns"],
|
|
1445
|
+
},
|
|
1446
|
+
|
|
1447
|
+
async execute(_tid: string, params: any, sig: any, _upd: any, _ctx: any) {
|
|
1448
|
+
if (sig?.aborted) return { content: [{ type: "text", text: "Aborted" }], details: {} };
|
|
1449
|
+
|
|
1450
|
+
if (!params.patterns || params.patterns.length === 0) {
|
|
1451
|
+
return {
|
|
1452
|
+
content: [{ type: "text", text: "Error: patterns array must have at least 1 element" }],
|
|
1453
|
+
details: { error: "empty patterns" },
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
if (!_fffFinder || _fffFinder.isDestroyed) {
|
|
1458
|
+
return {
|
|
1459
|
+
content: [{ type: "text", text: "FFF not initialized. Wait for session start or run /fff-rescan." }],
|
|
1460
|
+
details: {},
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
try {
|
|
1465
|
+
const effectiveLimit = Math.max(1, params.limit ?? 100);
|
|
1466
|
+
|
|
1467
|
+
const grepResult = _fffFinder.multiGrep({
|
|
1468
|
+
patterns: params.patterns,
|
|
1469
|
+
constraints: params.constraints,
|
|
1470
|
+
maxMatchesPerFile: Math.min(effectiveLimit, 50),
|
|
1471
|
+
smartCase: true,
|
|
1472
|
+
cursor: null,
|
|
1473
|
+
beforeContext: params.context ?? 0,
|
|
1474
|
+
afterContext: params.context ?? 0,
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
if (!grepResult.ok) {
|
|
1478
|
+
return {
|
|
1479
|
+
content: [{ type: "text", text: `multi_grep error: ${grepResult.error}` }],
|
|
1480
|
+
details: { error: grepResult.error },
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
const result = grepResult.value;
|
|
1485
|
+
let textContent = fffFormatGrepText(result.items, effectiveLimit);
|
|
1486
|
+
const matchCount = Math.min(result.items.length, effectiveLimit);
|
|
1487
|
+
|
|
1488
|
+
const notices: string[] = [];
|
|
1489
|
+
if (_fffPartialIndex) notices.push("Warning: partial file index");
|
|
1490
|
+
if (result.items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
|
|
1491
|
+
if (result.nextCursor) {
|
|
1492
|
+
const cursorId = _cursorStore.store(result.nextCursor);
|
|
1493
|
+
notices.push(`More results: cursor="${cursorId}"`);
|
|
1494
|
+
}
|
|
1495
|
+
if (notices.length) textContent += `\n\n[${notices.join(". ")}]`;
|
|
1496
|
+
|
|
1497
|
+
return {
|
|
1498
|
+
content: [{ type: "text", text: textContent }],
|
|
1499
|
+
details: {
|
|
1500
|
+
_type: "grepResult",
|
|
1501
|
+
text: textContent,
|
|
1502
|
+
pattern: params.patterns.join(" | "),
|
|
1503
|
+
matchCount,
|
|
1504
|
+
},
|
|
1505
|
+
};
|
|
1506
|
+
} catch (e: any) {
|
|
1507
|
+
return {
|
|
1508
|
+
content: [{ type: "text", text: `multi_grep error: ${e.message}` }],
|
|
1509
|
+
details: { error: e.message },
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
},
|
|
1513
|
+
|
|
1514
|
+
renderCall(args: any, theme: any, ctx: any) {
|
|
1515
|
+
resolveBaseBackground(theme);
|
|
1516
|
+
const patterns = args?.patterns ?? [];
|
|
1517
|
+
const constraints = args?.constraints;
|
|
1518
|
+
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1519
|
+
let content =
|
|
1520
|
+
theme.fg("toolTitle", theme.bold("multi_grep")) +
|
|
1521
|
+
" " +
|
|
1522
|
+
theme.fg("accent", patterns.map((p: string) => `"${p}"`).join(", "));
|
|
1523
|
+
if (constraints) content += theme.fg("muted", ` (${constraints})`);
|
|
1524
|
+
text.setText(content);
|
|
1525
|
+
return text;
|
|
1526
|
+
},
|
|
1527
|
+
|
|
1528
|
+
renderResult(result: any, _opt: any, theme: any, ctx: any) {
|
|
1529
|
+
resolveBaseBackground(theme);
|
|
1530
|
+
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1531
|
+
|
|
1532
|
+
if (ctx.isError) {
|
|
1533
|
+
const e = result.content?.[0]?.text ?? "Error";
|
|
1534
|
+
text.setText(`\n${theme.fg("error", e)}`);
|
|
1535
|
+
return text;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
const d = result.details;
|
|
1539
|
+
if (d?._type === "grepResult" && d.text) {
|
|
1540
|
+
const key = `mgrep:${d.pattern}:${d.matchCount}:${termW()}`;
|
|
1541
|
+
if (ctx.state._mgk !== key) {
|
|
1542
|
+
ctx.state._mgk = key;
|
|
1543
|
+
const info = `${FG_DIM}${d.matchCount} matches${RST}`;
|
|
1544
|
+
ctx.state._mgt = ` ${info}`;
|
|
1545
|
+
|
|
1546
|
+
renderGrepResults(d.text, d.pattern)
|
|
1547
|
+
.then((rendered: string) => {
|
|
1548
|
+
if (ctx.state._mgk !== key) return;
|
|
1549
|
+
ctx.state._mgt = ` ${info}\n${rendered}`;
|
|
1550
|
+
ctx.invalidate();
|
|
1551
|
+
})
|
|
1552
|
+
.catch(() => {});
|
|
1553
|
+
}
|
|
1554
|
+
text.setText(ctx.state._mgt ?? ` ${FG_DIM}${d.matchCount} matches${RST}`);
|
|
1555
|
+
return text;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
const fallback = result.content?.[0]?.text ?? "searched";
|
|
1559
|
+
text.setText(` ${theme.fg("dim", String(fallback).slice(0, 120))}`);
|
|
1560
|
+
return text;
|
|
1561
|
+
},
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// ===================================================================
|
|
1566
|
+
// FFF commands
|
|
1567
|
+
// ===================================================================
|
|
1568
|
+
|
|
1569
|
+
if (_fffModule) {
|
|
1570
|
+
pi.registerCommand("fff-health", {
|
|
1571
|
+
description: "Show FFF file finder health and indexer status",
|
|
1572
|
+
handler: async (_args: any, ctx: any) => {
|
|
1573
|
+
if (!_fffFinder || _fffFinder.isDestroyed) {
|
|
1574
|
+
ctx.ui?.notify?.("FFF not initialized", "warning");
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
const health = _fffFinder.healthCheck();
|
|
1579
|
+
if (!health.ok) {
|
|
1580
|
+
ctx.ui?.notify?.(`Health check failed: ${health.error}`, "error");
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
const h = health.value;
|
|
1585
|
+
const lines = [
|
|
1586
|
+
`FFF v${h.version}`,
|
|
1587
|
+
`Git: ${h.git.repositoryFound ? `yes (${h.git.workdir ?? "unknown"})` : "no"}`,
|
|
1588
|
+
`Picker: ${h.filePicker.initialized ? `${h.filePicker.indexedFiles ?? 0} files` : "not initialized"}`,
|
|
1589
|
+
`Frecency: ${h.frecency.initialized ? "active" : "disabled"}`,
|
|
1590
|
+
`Query tracker: ${h.queryTracker.initialized ? "active" : "disabled"}`,
|
|
1591
|
+
`Partial index: ${_fffPartialIndex ? "yes (scan timed out)" : "no"}`,
|
|
1592
|
+
];
|
|
1593
|
+
|
|
1594
|
+
const progress = _fffFinder.getScanProgress();
|
|
1595
|
+
if (progress.ok) {
|
|
1596
|
+
lines.push(`Scanning: ${progress.value.isScanning ? "yes" : "no"} (${progress.value.scannedFilesCount} files)`);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
ctx.ui?.notify?.(lines.join("\n"), "info");
|
|
1600
|
+
},
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
pi.registerCommand("fff-rescan", {
|
|
1604
|
+
description: "Trigger FFF to rescan files",
|
|
1605
|
+
handler: async (_args: any, ctx: any) => {
|
|
1606
|
+
if (!_fffFinder || _fffFinder.isDestroyed) {
|
|
1607
|
+
ctx.ui?.notify?.("FFF not initialized", "warning");
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const result = _fffFinder.scanFiles();
|
|
1612
|
+
if (!result.ok) {
|
|
1613
|
+
ctx.ui?.notify?.(`Rescan failed: ${result.error}`, "error");
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
_fffPartialIndex = false;
|
|
1618
|
+
ctx.ui?.notify?.("FFF rescan triggered", "info");
|
|
1619
|
+
},
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1159
1622
|
}
|