@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.
Files changed (3) hide show
  1. package/README.md +4 -4
  2. package/package.json +7 -4
  3. package/src/index.ts +467 -4
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # pi-pretty
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@heyhuynhgiabuu/pi-pretty)](https://www.npmjs.com/package/@heyhuynhgiabuu/pi-pretty)
4
- [![GitHub release](https://img.shields.io/github/v/release/heyhuynhgiabuu/pi-pretty)](https://github.com/heyhuynhgiabuu/pi-pretty/releases/latest)
4
+ [![GitHub release](https://img.shields.io/github/v/release/buddingnewinsights/pi-pretty)](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/heyhuynhgiabuu/pi-diff) for `write`/`edit` diff rendering.
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/heyhuynhgiabuu/pi-pretty/releases/latest
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/heyhuynhgiabuu)
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.1.8",
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/heyhuynhgiabuu/pi-pretty.git"
9
+ "url": "git+https://github.com/buddingnewinsights/pi-pretty.git"
10
10
  },
11
- "homepage": "https://github.com/heyhuynhgiabuu/pi-pretty#readme",
11
+ "homepage": "https://github.com/buddingnewinsights/pi-pretty#readme",
12
12
  "bugs": {
13
- "url": "https://github.com/heyhuynhgiabuu/pi-pretty/issues"
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/heyhuynhgiabuu/pi-pretty
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
- const sdk = require("@mariozechner/pi-coding-agent");
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
  }