@ff-labs/pi-fff 0.11.0 → 0.11.1-nightly.12142b0
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 -0
- package/package.json +3 -3
- package/src/index.ts +135 -26
package/README.md
CHANGED
|
@@ -83,8 +83,11 @@ This extension registers FFF-powered tools (`fffind`, `ffgrep`, `fff-multi-grep`
|
|
|
83
83
|
|
|
84
84
|
Search file contents. Smart case, plain text by default, regex optional.
|
|
85
85
|
|
|
86
|
+
Set `definitions_only: true` to search for matching declarations. If the completed search finds none, the tool reports that and returns ordinary matches. Cursors remember the search or fallback state. The optional `fff-multi-grep` tool accepts the same flag. Omitting the flag keeps ordinary search behavior.
|
|
87
|
+
|
|
86
88
|
Parameters:
|
|
87
89
|
- `pattern` — search text or regex
|
|
90
|
+
- `definitions_only` — prefer declarations; fall back to ordinary matches with a notice when none exist
|
|
88
91
|
- `path` — directory/file constraint (e.g. `src/`, `*.ts`)
|
|
89
92
|
- `ignoreCase` — force case-insensitive
|
|
90
93
|
- `literal` — treat as literal string (default: true)
|
|
@@ -107,6 +110,7 @@ OR-logic multi-pattern content search. SIMD-accelerated Aho-Corasick.
|
|
|
107
110
|
|
|
108
111
|
Parameters:
|
|
109
112
|
- `patterns` — array of literal patterns (OR logic)
|
|
113
|
+
- `definitions_only` — prefer declarations with the same fallback behavior
|
|
110
114
|
- `constraints` — file constraints (e.g. `*.{ts,tsx} !test/`)
|
|
111
115
|
- `context` — context lines
|
|
112
116
|
- `limit` — max matches (default: 100)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ff-labs/pi-fff",
|
|
3
3
|
"public": true,
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.1-nightly.12142b0",
|
|
5
5
|
"description": "pi extension: FFF-powered fuzzy file and content search",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"typecheck": "tsc --noEmit"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@ff-labs/fff-bun": "0.11.
|
|
45
|
-
"@ff-labs/fff-node": "0.11.
|
|
44
|
+
"@ff-labs/fff-bun": "0.11.1-nightly.12142b0",
|
|
45
|
+
"@ff-labs/fff-node": "0.11.1-nightly.12142b0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@earendil-works/pi-coding-agent": "*",
|
package/src/index.ts
CHANGED
|
@@ -89,10 +89,19 @@ function toolNameList(names: ToolNames): string[] {
|
|
|
89
89
|
// Cursor store — simple bounded Map for pagination cursors
|
|
90
90
|
// ---------------------------------------------------------------------------
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
interface ToolGrepCursor {
|
|
93
|
+
cursor: GrepCursor;
|
|
94
|
+
finder: FileFinderApi;
|
|
95
|
+
definitionsOnly: boolean;
|
|
96
|
+
foundDefinitions: boolean;
|
|
97
|
+
mode?: GrepMode;
|
|
98
|
+
query?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const cursorCache = new Map<string, ToolGrepCursor>();
|
|
93
102
|
let cursorCounter = 0;
|
|
94
103
|
|
|
95
|
-
function storeCursor(cursor:
|
|
104
|
+
function storeCursor(cursor: ToolGrepCursor): string {
|
|
96
105
|
const id = `fff_c${++cursorCounter}`;
|
|
97
106
|
cursorCache.set(id, cursor);
|
|
98
107
|
if (cursorCache.size > 200) {
|
|
@@ -102,7 +111,7 @@ function storeCursor(cursor: GrepCursor): string {
|
|
|
102
111
|
return id;
|
|
103
112
|
}
|
|
104
113
|
|
|
105
|
-
function getCursor(id: string):
|
|
114
|
+
function getCursor(id: string): ToolGrepCursor | undefined {
|
|
106
115
|
return cursorCache.get(id);
|
|
107
116
|
}
|
|
108
117
|
|
|
@@ -546,7 +555,6 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
546
555
|
}
|
|
547
556
|
|
|
548
557
|
function destroyFinder() {
|
|
549
|
-
stopHomeScanStatus();
|
|
550
558
|
if (mainFinder && !mainFinder.isDestroyed) {
|
|
551
559
|
mainFinder.destroy();
|
|
552
560
|
mainFinder = null;
|
|
@@ -556,6 +564,8 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
556
564
|
auxPool?.destroy();
|
|
557
565
|
auxPool = null;
|
|
558
566
|
pickers = null;
|
|
567
|
+
// UI teardown last: a throwing ctx must not leak the native finders (#948).
|
|
568
|
+
stopHomeScanStatus();
|
|
559
569
|
}
|
|
560
570
|
|
|
561
571
|
async function resolveFinderForPath(
|
|
@@ -854,8 +864,11 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
854
864
|
}
|
|
855
865
|
});
|
|
856
866
|
|
|
857
|
-
|
|
867
|
+
// A ctx captured from a replaced session is stale and throws on access (#948).
|
|
868
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
869
|
+
uiCtx = ctx;
|
|
858
870
|
destroyFinder();
|
|
871
|
+
uiCtx = null;
|
|
859
872
|
});
|
|
860
873
|
|
|
861
874
|
// --- Shared render helpers ---
|
|
@@ -954,7 +967,15 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
954
967
|
|
|
955
968
|
// --- grep tool ---
|
|
956
969
|
|
|
970
|
+
const definitionsOnlySchema = Type.Optional(
|
|
971
|
+
Type.Boolean({
|
|
972
|
+
description:
|
|
973
|
+
"Return matching declarations. If none are found, report that and return other matches.",
|
|
974
|
+
}),
|
|
975
|
+
);
|
|
976
|
+
|
|
957
977
|
const grepSchema = Type.Object({
|
|
978
|
+
definitions_only: definitionsOnlySchema,
|
|
958
979
|
pattern: Type.String({
|
|
959
980
|
description: "Search pattern (literal text or regex)",
|
|
960
981
|
}),
|
|
@@ -996,6 +1017,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
996
1017
|
promptSnippet: "Grep contents",
|
|
997
1018
|
promptGuidelines: (names) => [
|
|
998
1019
|
`${names.grep}: prefer bare identifiers as patterns. Literal queries are most efficient.`,
|
|
1020
|
+
`${names.grep}: use definitions_only: true to find declarations; if none are found, the tool returns other matches with a notice.`,
|
|
999
1021
|
`${names.grep}: use path for include ('src/', '*.ts') and exclude for noise ('test/,*.min.js').`,
|
|
1000
1022
|
`${names.grep}: caseSensitive: true when you need exact case (smart-case otherwise).`,
|
|
1001
1023
|
`${names.grep}: after 1-2 greps, read the top match instead of more greps.`,
|
|
@@ -1006,25 +1028,31 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1006
1028
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
1007
1029
|
|
|
1008
1030
|
const pattern = params.pattern;
|
|
1009
|
-
const
|
|
1031
|
+
const resumed = params.cursor ? getCursor(params.cursor) : undefined;
|
|
1032
|
+
const aux = resumed
|
|
1033
|
+
? null
|
|
1034
|
+
: await resolveFinderForPath(params.path, pattern, params.exclude);
|
|
1010
1035
|
|
|
1011
|
-
const picker =
|
|
1036
|
+
const picker =
|
|
1037
|
+
resumed?.finder ?? (aux ? aux.finder : await ensureFinder(activeCwd));
|
|
1038
|
+
if (picker.isDestroyed)
|
|
1039
|
+
throw new Error("Search cursor expired. Start a new search without the cursor.");
|
|
1012
1040
|
const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT);
|
|
1013
1041
|
// pageSize caps TOTAL matches across all files (soft cap: the current file
|
|
1014
1042
|
// is always finished first). maxMatchesPerFile stays decoupled at the engine
|
|
1015
1043
|
// default so same-file overflow remains reachable via cursor (#825).
|
|
1016
1044
|
const pageSize = Math.min(effectiveLimit, GREP_PAGE_SIZE_MAX);
|
|
1017
1045
|
const context = clampContext(params.context);
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
: buildQuery(params.path, pattern, params.exclude, activeCwd);
|
|
1046
|
+
let query =
|
|
1047
|
+
resumed?.query ??
|
|
1048
|
+
(aux ? aux.query : buildQuery(params.path, pattern, params.exclude, activeCwd));
|
|
1021
1049
|
|
|
1022
1050
|
// Auto-detect: regex if the pattern has regex metacharacters AND parses
|
|
1023
1051
|
// as a valid regex, otherwise plain literal. The fuzzy fallback below
|
|
1024
1052
|
// only kicks in for plain mode — regex queries are intentional.
|
|
1025
1053
|
const hasRegexSyntax = pattern !== pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1026
1054
|
|
|
1027
|
-
let mode: GrepMode = hasRegexSyntax ? "regex" : "plain";
|
|
1055
|
+
let mode: GrepMode = resumed?.mode ?? (hasRegexSyntax ? "regex" : "plain");
|
|
1028
1056
|
if (mode === "regex") {
|
|
1029
1057
|
try {
|
|
1030
1058
|
new RegExp(pattern);
|
|
@@ -1059,21 +1087,42 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1059
1087
|
// (case-insensitive when pattern is all lowercase).
|
|
1060
1088
|
const smartCase = params.caseSensitive !== true;
|
|
1061
1089
|
|
|
1062
|
-
|
|
1090
|
+
let definitionsOnly = resumed?.definitionsOnly ?? params.definitions_only ?? false;
|
|
1091
|
+
const preferDefinitions = definitionsOnly;
|
|
1092
|
+
const foundDefinitions = resumed?.foundDefinitions ?? false;
|
|
1093
|
+
const options = {
|
|
1063
1094
|
mode,
|
|
1064
1095
|
smartCase,
|
|
1065
1096
|
maxMatchesPerFile: GREP_MAX_MATCHES_PER_FILE,
|
|
1066
1097
|
pageSize,
|
|
1067
|
-
cursor:
|
|
1098
|
+
cursor: resumed?.cursor ?? null,
|
|
1068
1099
|
beforeContext: context,
|
|
1069
1100
|
afterContext: context,
|
|
1070
|
-
classifyDefinitions: true,
|
|
1071
1101
|
timeBudgetMs: GREP_TIME_BUDGET_MS,
|
|
1102
|
+
};
|
|
1103
|
+
const grepResult = picker.grep(query, {
|
|
1104
|
+
...options,
|
|
1105
|
+
classifyDefinitions: definitionsOnly ? "definition_only" : "classify",
|
|
1072
1106
|
});
|
|
1073
1107
|
|
|
1074
1108
|
if (!grepResult.ok) throw new Error(grepResult.error);
|
|
1075
1109
|
|
|
1076
1110
|
let result = grepResult.value;
|
|
1111
|
+
const definitionsFallback =
|
|
1112
|
+
definitionsOnly &&
|
|
1113
|
+
!foundDefinitions &&
|
|
1114
|
+
result.items.length === 0 &&
|
|
1115
|
+
!result.nextCursor;
|
|
1116
|
+
if (definitionsFallback) {
|
|
1117
|
+
const fallback = picker.grep(query, {
|
|
1118
|
+
...options,
|
|
1119
|
+
cursor: null,
|
|
1120
|
+
classifyDefinitions: "classify",
|
|
1121
|
+
});
|
|
1122
|
+
if (!fallback.ok) throw new Error(fallback.error);
|
|
1123
|
+
result = fallback.value;
|
|
1124
|
+
definitionsOnly = false;
|
|
1125
|
+
}
|
|
1077
1126
|
let fuzzyNotice: string | null = null;
|
|
1078
1127
|
|
|
1079
1128
|
// if we hit the timeout do not run the fuzzy fallback
|
|
@@ -1081,7 +1130,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1081
1130
|
if (
|
|
1082
1131
|
result.items.length === 0 &&
|
|
1083
1132
|
!result.nextCursor &&
|
|
1084
|
-
!params.cursor &&
|
|
1133
|
+
(!params.cursor || definitionsFallback) &&
|
|
1085
1134
|
mode !== "regex"
|
|
1086
1135
|
) {
|
|
1087
1136
|
// When the caller pinned a specific file (path has an extension), the
|
|
@@ -1091,7 +1140,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1091
1140
|
// excluded / out-of-scope directories.
|
|
1092
1141
|
const lastSeg = params.path?.split(/[\\/]/).pop() ?? "";
|
|
1093
1142
|
const pathTargetsFile = /\.[a-zA-Z][a-zA-Z0-9]{0,9}$/.test(lastSeg);
|
|
1094
|
-
const fuzzyQuery = pathTargetsFile ? pattern : query;
|
|
1143
|
+
const fuzzyQuery = pathTargetsFile && !preferDefinitions ? pattern : query;
|
|
1095
1144
|
const fuzzy = picker.grep(fuzzyQuery, {
|
|
1096
1145
|
mode: "fuzzy",
|
|
1097
1146
|
smartCase,
|
|
@@ -1100,13 +1149,15 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1100
1149
|
cursor: null,
|
|
1101
1150
|
beforeContext: 0,
|
|
1102
1151
|
afterContext: 0,
|
|
1103
|
-
classifyDefinitions:
|
|
1152
|
+
classifyDefinitions: definitionsOnly ? "definition_only" : "classify",
|
|
1104
1153
|
timeBudgetMs: GREP_TIME_BUDGET_MS,
|
|
1105
1154
|
});
|
|
1106
1155
|
|
|
1107
1156
|
if (fuzzy.ok && fuzzy.value.items.length > 0) {
|
|
1108
1157
|
fuzzyNotice = `0 exact matches. Maybe you meant this?`;
|
|
1109
1158
|
result = fuzzy.value;
|
|
1159
|
+
mode = "fuzzy";
|
|
1160
|
+
query = fuzzyQuery;
|
|
1110
1161
|
}
|
|
1111
1162
|
}
|
|
1112
1163
|
|
|
@@ -1116,11 +1167,27 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1116
1167
|
notices.push(`Invalid regex: ${result.regexFallbackError}, used literal match`);
|
|
1117
1168
|
}
|
|
1118
1169
|
if (result.nextCursor) {
|
|
1119
|
-
|
|
1170
|
+
const cursor = storeCursor({
|
|
1171
|
+
cursor: result.nextCursor,
|
|
1172
|
+
finder: picker,
|
|
1173
|
+
definitionsOnly,
|
|
1174
|
+
foundDefinitions:
|
|
1175
|
+
foundDefinitions || (definitionsOnly && result.items.length > 0),
|
|
1176
|
+
mode,
|
|
1177
|
+
query,
|
|
1178
|
+
});
|
|
1179
|
+
notices.push(`Continue with cursor="${cursor}"`);
|
|
1120
1180
|
}
|
|
1121
1181
|
|
|
1122
1182
|
if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`;
|
|
1123
1183
|
if (fuzzyNotice) output = `[${fuzzyNotice}]\n${output}`;
|
|
1184
|
+
if (definitionsFallback) {
|
|
1185
|
+
const notice =
|
|
1186
|
+
result.items.length > 0 || result.nextCursor
|
|
1187
|
+
? "No matching definitions found. Showing other matches:"
|
|
1188
|
+
: "No matching definitions or other matches found.";
|
|
1189
|
+
output = `[${notice}]\n${output}`;
|
|
1190
|
+
}
|
|
1124
1191
|
|
|
1125
1192
|
return {
|
|
1126
1193
|
content: [{ type: "text", text: output }],
|
|
@@ -1300,6 +1367,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1300
1367
|
|
|
1301
1368
|
if (enableMultiGrep) {
|
|
1302
1369
|
const multiGrepSchema = Type.Object({
|
|
1370
|
+
definitions_only: definitionsOnlySchema,
|
|
1303
1371
|
patterns: Type.Array(Type.String(), {
|
|
1304
1372
|
description:
|
|
1305
1373
|
"Literal patterns (OR). Include snake_case/camelCase/PascalCase variants.",
|
|
@@ -1328,6 +1396,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1328
1396
|
`${names.multiGrep}: use when searching for several identifiers at once.`,
|
|
1329
1397
|
`${names.multiGrep}: include all naming-convention variants (snake/camel/Pascal).`,
|
|
1330
1398
|
`${names.multiGrep}: patterns are literal. Use constraints for file filters.`,
|
|
1399
|
+
`${names.multiGrep}: use definitions_only: true to find declarations; if none are found, the tool returns other matches with a notice.`,
|
|
1331
1400
|
],
|
|
1332
1401
|
parameters: multiGrepSchema,
|
|
1333
1402
|
|
|
@@ -1336,36 +1405,76 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1336
1405
|
if (!params.patterns?.length)
|
|
1337
1406
|
throw new Error("patterns array must have at least 1 element");
|
|
1338
1407
|
|
|
1339
|
-
const
|
|
1408
|
+
const resumed = params.cursor ? getCursor(params.cursor) : undefined;
|
|
1409
|
+
const f = resumed?.finder ?? (await ensureFinder(activeCwd));
|
|
1410
|
+
if (f.isDestroyed)
|
|
1411
|
+
throw new Error(
|
|
1412
|
+
"Search cursor expired. Start a new search without the cursor.",
|
|
1413
|
+
);
|
|
1340
1414
|
const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT);
|
|
1341
1415
|
const pageSize = Math.min(effectiveLimit, GREP_PAGE_SIZE_MAX);
|
|
1342
1416
|
const context = clampContext(params.context);
|
|
1343
1417
|
|
|
1344
|
-
|
|
1418
|
+
let definitionsOnly =
|
|
1419
|
+
resumed?.definitionsOnly ?? params.definitions_only ?? false;
|
|
1420
|
+
const foundDefinitions = resumed?.foundDefinitions ?? false;
|
|
1421
|
+
const options = {
|
|
1345
1422
|
patterns: params.patterns,
|
|
1346
1423
|
constraints: params.constraints,
|
|
1347
1424
|
maxMatchesPerFile: GREP_MAX_MATCHES_PER_FILE,
|
|
1348
1425
|
pageSize,
|
|
1349
1426
|
smartCase: true,
|
|
1350
|
-
cursor:
|
|
1427
|
+
cursor: resumed?.cursor ?? null,
|
|
1351
1428
|
beforeContext: context,
|
|
1352
1429
|
afterContext: context,
|
|
1430
|
+
};
|
|
1431
|
+
const grepResult = f.multiGrep({
|
|
1432
|
+
...options,
|
|
1433
|
+
classifyDefinitions: definitionsOnly ? "definition_only" : "off",
|
|
1353
1434
|
});
|
|
1354
1435
|
|
|
1355
1436
|
if (!grepResult.ok) throw new Error(grepResult.error);
|
|
1356
1437
|
|
|
1357
|
-
|
|
1438
|
+
let result = grepResult.value;
|
|
1439
|
+
const definitionsFallback =
|
|
1440
|
+
definitionsOnly &&
|
|
1441
|
+
!foundDefinitions &&
|
|
1442
|
+
result.items.length === 0 &&
|
|
1443
|
+
!result.nextCursor;
|
|
1444
|
+
if (definitionsFallback) {
|
|
1445
|
+
const fallback = f.multiGrep({
|
|
1446
|
+
...options,
|
|
1447
|
+
cursor: null,
|
|
1448
|
+
classifyDefinitions: "off",
|
|
1449
|
+
});
|
|
1450
|
+
if (!fallback.ok) throw new Error(fallback.error);
|
|
1451
|
+
result = fallback.value;
|
|
1452
|
+
definitionsOnly = false;
|
|
1453
|
+
}
|
|
1358
1454
|
let output = formatGrepOutput(result);
|
|
1359
1455
|
|
|
1360
1456
|
const notices: string[] = [];
|
|
1361
1457
|
if (result.items.length >= effectiveLimit)
|
|
1362
1458
|
notices.push(`${effectiveLimit}+ matches (refine patterns)`);
|
|
1363
|
-
if (result.nextCursor)
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1459
|
+
if (result.nextCursor) {
|
|
1460
|
+
const cursor = storeCursor({
|
|
1461
|
+
cursor: result.nextCursor,
|
|
1462
|
+
finder: f,
|
|
1463
|
+
definitionsOnly,
|
|
1464
|
+
foundDefinitions:
|
|
1465
|
+
foundDefinitions || (definitionsOnly && result.items.length > 0),
|
|
1466
|
+
});
|
|
1467
|
+
notices.push(`More available. cursor="${cursor}" to continue`);
|
|
1468
|
+
}
|
|
1367
1469
|
|
|
1368
1470
|
if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`;
|
|
1471
|
+
if (definitionsFallback) {
|
|
1472
|
+
const notice =
|
|
1473
|
+
result.items.length > 0 || result.nextCursor
|
|
1474
|
+
? "No matching definitions found. Showing other matches:"
|
|
1475
|
+
: "No matching definitions or other matches found.";
|
|
1476
|
+
output = `[${notice}]\n${output}`;
|
|
1477
|
+
}
|
|
1369
1478
|
|
|
1370
1479
|
return {
|
|
1371
1480
|
content: [{ type: "text", text: output }],
|