@ff-labs/pi-fff 0.10.1 → 0.10.2-nightly.031005e

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ff-labs/pi-fff",
3
3
  "public": true,
4
- "version": "0.10.1",
4
+ "version": "0.10.2-nightly.031005e",
5
5
  "description": "pi extension: FFF-powered fuzzy file and content search",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -40,8 +40,8 @@
40
40
  "typecheck": "tsc --noEmit"
41
41
  },
42
42
  "dependencies": {
43
- "@ff-labs/fff-bun": "*",
44
- "@ff-labs/fff-node": "*"
43
+ "@ff-labs/fff-bun": "0.10.2-nightly.031005e",
44
+ "@ff-labs/fff-node": "0.10.2-nightly.031005e"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@earendil-works/pi-coding-agent": "*",
@@ -14,13 +14,15 @@ interface AuxPicker {
14
14
  }
15
15
 
16
16
  export interface AuxOpts {
17
- frecencyDbPath?: string;
18
- historyDbPath?: string;
19
17
  enableFsRootScanning: boolean;
20
18
  }
21
19
 
22
20
  export class AuxFinderPool {
23
21
  private entries: AuxPicker[] = [];
22
+ // In-flight creations keyed by root. Concurrent acquire() calls for the same
23
+ // (or a covering) root share one finder/scan instead of each starting a full
24
+ // duplicate traversal — issue #746. Mirrors the main finder's finderPromise.
25
+ private pending = new Map<string, Promise<AuxPicker>>();
24
26
  constructor(private opts: AuxOpts) {}
25
27
 
26
28
  destroy(): void {
@@ -29,6 +31,7 @@ export class AuxFinderPool {
29
31
  }
30
32
 
31
33
  this.entries = [];
34
+ this.pending.clear();
32
35
  }
33
36
 
34
37
  private sweepIdle(now = Date.now()): void {
@@ -60,31 +63,53 @@ export class AuxFinderPool {
60
63
  return { finder: covering.finder, root: covering.root };
61
64
  }
62
65
 
66
+ // Coalesce concurrent creations for the same root so we scan once. A slow
67
+ // full-home scan started by one call is awaited by the others instead of
68
+ // each spawning its own traversal (#746).
69
+ const inflight = this.pending.get(maybeRoot);
70
+ if (inflight) {
71
+ const e = await inflight;
72
+ e.lastUsed = Date.now();
73
+ return { finder: e.finder, root: e.root };
74
+ }
75
+
76
+ const creation = this.create(maybeRoot).finally(() => {
77
+ this.pending.delete(maybeRoot);
78
+ });
79
+ this.pending.set(maybeRoot, creation);
80
+ const entry = await creation;
81
+ return { finder: entry.finder, root: entry.root };
82
+ }
83
+
84
+ private async create(root: string): Promise<AuxPicker> {
63
85
  if (this.entries.length >= MAX_AUX) {
64
86
  let oldest = this.entries[0];
65
- for (const e of this.entries)
66
- if (e.lastUsed < oldest.lastUsed) oldest = e;
87
+ for (const e of this.entries) if (e.lastUsed < oldest.lastUsed) oldest = e;
67
88
  if (!oldest.finder.isDestroyed) oldest.finder.destroy();
68
89
  this.entries = this.entries.filter((e) => e !== oldest);
69
90
  }
70
91
 
71
92
  const { FileFinder } = await loadSdk();
93
+ // LMDB env can only be opened once per process; the main finder already
94
+ // owns the frecency/history DBs. Aux finders are transient and run without
95
+ // persistent scoring — see issue #700.
72
96
  const result = FileFinder.create({
73
- basePath: maybeRoot,
74
- frecencyDbPath: this.opts.frecencyDbPath,
75
- historyDbPath: this.opts.historyDbPath,
97
+ basePath: root,
76
98
  aiMode: true,
77
99
  enableHomeDirScanning: true,
78
100
  enableFsRootScanning: this.opts.enableFsRootScanning,
79
101
  });
80
102
  if (!result.ok)
81
- throw new Error(
82
- `Failed to create aux file finder for ${maybeRoot}: ${result.error}`,
83
- );
103
+ throw new Error(`Failed to create aux file finder for ${root}: ${result.error}`);
84
104
 
85
105
  await result.value.waitForScan(SCAN_TIMEOUT_MS);
86
- this.entries.push({ root: maybeRoot, finder: result.value, lastUsed: Date.now() });
87
- return { finder: result.value, root: maybeRoot };
106
+ const entry: AuxPicker = {
107
+ root,
108
+ finder: result.value,
109
+ lastUsed: Date.now(),
110
+ };
111
+ this.entries.push(entry);
112
+ return entry;
88
113
  }
89
114
 
90
115
  size(): number {
@@ -97,9 +122,7 @@ export class AuxFinderPool {
97
122
  // remainder usable as a fuzzy path constraint relative to that root. Glob and
98
123
  // nonexistent segments both go into the suffix: we walk up to the nearest
99
124
  // existing ancestor so partially-wrong paths still resolve to a search root.
100
- export function resolveAuxRoot(
101
- absPath: string,
102
- ): { root: string; suffix: string } | null {
125
+ export function resolveAuxRoot(absPath: string): { root: string; suffix: string } | null {
103
126
  const trimmed = path.normalize(absPath.trim()).replace(/\/+$/, "") || "/";
104
127
  if (!path.isAbsolute(trimmed)) return null;
105
128
  if (trimmed === path.sep) return { root: path.sep, suffix: "" };
@@ -159,7 +182,6 @@ export function routePathConstraint(
159
182
  return resolveAuxRoot(candidate);
160
183
  }
161
184
 
162
-
163
185
  export function rootCovers(root: string, target: string): boolean {
164
186
  if (root === target) return true;
165
187
  const prefix = root.endsWith(path.sep) ? root : root + path.sep;
package/src/index.ts CHANGED
@@ -36,6 +36,9 @@ const DEFAULT_FIND_LIMIT = 30;
36
36
  const GREP_MAX_LINE_LENGTH = 500;
37
37
  const MENTION_MAX_RESULTS = 20;
38
38
 
39
+ // If we exceed 10 seconds for indexed grep - something is definitely off
40
+ const GREP_TIME_BUDGET_MS = 10_000;
41
+
39
42
  type FffMode = "tools-and-ui" | "tools-only" | "override";
40
43
 
41
44
  const VALID_MODES: FffMode[] = ["tools-and-ui", "tools-only", "override"];
@@ -261,9 +264,7 @@ function createFffMentionProvider(
261
264
 
262
265
  const query = prefix.startsWith('@"') ? prefix.slice(2) : prefix.slice(1);
263
266
  const items = await getItems(query, options.signal);
264
- return options.signal.aborted || items.length === 0
265
- ? null
266
- : { items, prefix };
267
+ return options.signal.aborted || items.length === 0 ? null : { items, prefix };
267
268
  },
268
269
  applyCompletion(_lines, cursorLine, cursorCol, item, prefix) {
269
270
  const currentLine = _lines[cursorLine] || "";
@@ -272,11 +273,7 @@ function createFffMentionProvider(
272
273
  const newLine = before + item.value + after;
273
274
  const newCursorCol = cursorCol - prefix.length + item.value.length;
274
275
  return {
275
- lines: [
276
- ..._lines.slice(0, cursorLine),
277
- newLine,
278
- ..._lines.slice(cursorLine + 1),
279
- ],
276
+ lines: [..._lines.slice(0, cursorLine), newLine, ..._lines.slice(cursorLine + 1)],
280
277
  cursorLine,
281
278
  cursorCol: newCursorCol,
282
279
  };
@@ -344,8 +341,6 @@ export default function fffExtension(pi: ExtensionAPI) {
344
341
  }
345
342
 
346
343
  let auxPool = new AuxFinderPool({
347
- frecencyDbPath,
348
- historyDbPath,
349
344
  enableFsRootScanning,
350
345
  });
351
346
 
@@ -409,9 +404,7 @@ export default function fffExtension(pi: ExtensionAPI) {
409
404
  const aux = await auxPool.acquire(route.root);
410
405
  // A broader covering picker may have been reused; rebase the suffix so the
411
406
  // constraint stays relative to the picker's actual root.
412
- const rebase = nodePath
413
- .relative(aux.root, route.root)
414
- .replaceAll(nodePath.sep, "/");
407
+ const rebase = nodePath.relative(aux.root, route.root).replaceAll(nodePath.sep, "/");
415
408
  const suffix = [rebase, route.suffix].filter(Boolean).join("/");
416
409
  const query = buildQuery(suffix || undefined, pattern, exclude, aux.root);
417
410
  return { finder: aux.finder, query, root: aux.root };
@@ -428,22 +421,20 @@ export default function fffExtension(pi: ExtensionAPI) {
428
421
  const result = f.mixedSearch(query, { pageSize: MENTION_MAX_RESULTS });
429
422
  if (!result.ok) return [];
430
423
 
431
- return result.value.items
432
- .slice(0, MENTION_MAX_RESULTS)
433
- .map((mixed: MixedItem) => {
434
- if (mixed.type === "directory") {
435
- return {
436
- value: buildAtCompletionValue(mixed.item.relativePath),
437
- label: mixed.item.dirName,
438
- description: mixed.item.relativePath,
439
- };
440
- }
424
+ return result.value.items.slice(0, MENTION_MAX_RESULTS).map((mixed: MixedItem) => {
425
+ if (mixed.type === "directory") {
441
426
  return {
442
427
  value: buildAtCompletionValue(mixed.item.relativePath),
443
- label: mixed.item.fileName,
428
+ label: mixed.item.dirName,
444
429
  description: mixed.item.relativePath,
445
430
  };
446
- });
431
+ }
432
+ return {
433
+ value: buildAtCompletionValue(mixed.item.relativePath),
434
+ label: mixed.item.fileName,
435
+ description: mixed.item.relativePath,
436
+ };
437
+ });
447
438
  }
448
439
 
449
440
  function registerAutocompleteProvider(ctx: {
@@ -479,21 +470,11 @@ export default function fffExtension(pi: ExtensionAPI) {
479
470
  return current.getSuggestions(lines, cursorLine, cursorCol, options);
480
471
  },
481
472
  applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
482
- return current.applyCompletion(
483
- lines,
484
- cursorLine,
485
- cursorCol,
486
- item,
487
- prefix,
488
- );
473
+ return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
489
474
  },
490
475
  shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
491
476
  return (
492
- current.shouldTriggerFileCompletion?.(
493
- lines,
494
- cursorLine,
495
- cursorCol,
496
- ) ?? true
477
+ current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true
497
478
  );
498
479
  },
499
480
  };
@@ -508,14 +489,12 @@ export default function fffExtension(pi: ExtensionAPI) {
508
489
  });
509
490
 
510
491
  pi.registerFlag("fff-frecency-db", {
511
- description:
512
- "Path to the frecency database (overrides FFF_FRECENCY_DB env)",
492
+ description: "Path to the frecency database (overrides FFF_FRECENCY_DB env)",
513
493
  type: "string",
514
494
  });
515
495
 
516
496
  pi.registerFlag("fff-history-db", {
517
- description:
518
- "Path to the query history database (overrides FFF_HISTORY_DB env)",
497
+ description: "Path to the query history database (overrides FFF_HISTORY_DB env)",
519
498
  type: "string",
520
499
  });
521
500
 
@@ -575,20 +554,15 @@ export default function fffExtension(pi: ExtensionAPI) {
575
554
  context: any,
576
555
  maxLines = 15,
577
556
  ) => {
578
- const text =
579
- (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
580
- const output =
581
- result.content?.find((c) => c.type === "text")?.text?.trim() ?? "";
557
+ const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
558
+ const output = result.content?.find((c) => c.type === "text")?.text?.trim() ?? "";
582
559
  if (!output) {
583
560
  text.setText(theme.fg("muted", "No output"));
584
561
  return text;
585
562
  }
586
563
 
587
564
  const lines = output.split("\n");
588
- const displayLines = lines.slice(
589
- 0,
590
- options.expanded ? lines.length : maxLines,
591
- );
565
+ const displayLines = lines.slice(0, options.expanded ? lines.length : maxLines);
592
566
  let content = `\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`;
593
567
  if (lines.length > displayLines.length) {
594
568
  content += theme.fg(
@@ -643,10 +617,10 @@ export default function fffExtension(pi: ExtensionAPI) {
643
617
  description: `Grep file contents. Smart-case, auto-detects regex vs literal, git-aware. Results are ranked by frecency (most-accessed files first); matches within a file stay in source order. Default limit ${DEFAULT_GREP_LIMIT}.`,
644
618
  promptSnippet: "Grep contents",
645
619
  promptGuidelines: [
646
- "Prefer bare identifiers as patterns. Literal queries are most efficient.",
647
- "Use path for include ('src/', '*.ts') and exclude for noise ('test/,*.min.js').",
648
- "caseSensitive: true when you need exact case (smart-case otherwise).",
649
- "After 1-2 greps, read the top match instead of more greps.",
620
+ `${toolNames.grep}: prefer bare identifiers as patterns. Literal queries are most efficient.`,
621
+ `${toolNames.grep}: use path for include ('src/', '*.ts') and exclude for noise ('test/,*.min.js').`,
622
+ `${toolNames.grep}: caseSensitive: true when you need exact case (smart-case otherwise).`,
623
+ `${toolNames.grep}: after 1-2 greps, read the top match instead of more greps.`,
650
624
  ],
651
625
  parameters: grepSchema,
652
626
 
@@ -654,11 +628,7 @@ export default function fffExtension(pi: ExtensionAPI) {
654
628
  if (signal?.aborted) throw new Error("Operation aborted");
655
629
 
656
630
  const pattern = params.pattern;
657
- const aux = await resolveFinderForPath(
658
- params.path,
659
- pattern,
660
- params.exclude,
661
- );
631
+ const aux = await resolveFinderForPath(params.path, pattern, params.exclude);
662
632
 
663
633
  const picker = aux ? aux.finder : await ensureFinder(activeCwd);
664
634
  const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT);
@@ -669,8 +639,7 @@ export default function fffExtension(pi: ExtensionAPI) {
669
639
  // Auto-detect: regex if the pattern has regex metacharacters AND parses
670
640
  // as a valid regex, otherwise plain literal. The fuzzy fallback below
671
641
  // only kicks in for plain mode — regex queries are intentional.
672
- const hasRegexSyntax =
673
- pattern !== pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
642
+ const hasRegexSyntax = pattern !== pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
674
643
 
675
644
  let mode: GrepMode = hasRegexSyntax ? "regex" : "plain";
676
645
  if (mode === "regex") {
@@ -715,6 +684,7 @@ export default function fffExtension(pi: ExtensionAPI) {
715
684
  beforeContext: params.context ?? 0,
716
685
  afterContext: params.context ?? 0,
717
686
  classifyDefinitions: true,
687
+ timeBudgetMs: GREP_TIME_BUDGET_MS,
718
688
  });
719
689
 
720
690
  if (!grepResult.ok) throw new Error(grepResult.error);
@@ -722,9 +692,23 @@ export default function fffExtension(pi: ExtensionAPI) {
722
692
  let result = grepResult.value;
723
693
  let fuzzyNotice: string | null = null;
724
694
 
725
- // automatic fuzzy fallback allows to broad the queries and find different cases
726
- if (result.items.length === 0 && !params.cursor && mode !== "regex") {
727
- const fuzzy = picker.grep(pattern, {
695
+ // if we hit the timeout do not run the fuzzy fallback
696
+ // cause it will only consumer more time
697
+ if (
698
+ result.items.length === 0 &&
699
+ !result.nextCursor &&
700
+ !params.cursor &&
701
+ mode !== "regex"
702
+ ) {
703
+ // When the caller pinned a specific file (path has an extension), the
704
+ // fuzzy fallback broadens across the whole picker — the file may just
705
+ // be misnamed. For directory constraints (or no path), we keep the
706
+ // constrained query so the fallback does not leak matches from
707
+ // excluded / out-of-scope directories.
708
+ const lastSeg = params.path?.split(/[\\/]/).pop() ?? "";
709
+ const pathTargetsFile = /\.[a-zA-Z][a-zA-Z0-9]{0,9}$/.test(lastSeg);
710
+ const fuzzyQuery = pathTargetsFile ? pattern : query;
711
+ const fuzzy = picker.grep(fuzzyQuery, {
728
712
  mode: "fuzzy",
729
713
  smartCase,
730
714
  maxMatchesPerFile: Math.min(effectiveLimit, 50),
@@ -732,6 +716,7 @@ export default function fffExtension(pi: ExtensionAPI) {
732
716
  beforeContext: 0,
733
717
  afterContext: 0,
734
718
  classifyDefinitions: true,
719
+ timeBudgetMs: GREP_TIME_BUDGET_MS,
735
720
  });
736
721
 
737
722
  if (fuzzy.ok && fuzzy.value.items.length > 0) {
@@ -743,14 +728,10 @@ export default function fffExtension(pi: ExtensionAPI) {
743
728
  let output = formatGrepOutput(result);
744
729
  const notices: string[] = [];
745
730
  if (result.regexFallbackError) {
746
- notices.push(
747
- `Invalid regex: ${result.regexFallbackError}, used literal match`,
748
- );
731
+ notices.push(`Invalid regex: ${result.regexFallbackError}, used literal match`);
749
732
  }
750
733
  if (result.nextCursor) {
751
- notices.push(
752
- `Continue with cursor="${storeCursor(result.nextCursor)}"`,
753
- );
734
+ notices.push(`Continue with cursor="${storeCursor(result.nextCursor)}"`);
754
735
  }
755
736
 
756
737
  if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`;
@@ -766,8 +747,7 @@ export default function fffExtension(pi: ExtensionAPI) {
766
747
  },
767
748
 
768
749
  renderCall(args, theme, context) {
769
- const text =
770
- (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
750
+ const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
771
751
  const pattern = args?.pattern ?? "";
772
752
  const path = args?.path ?? ".";
773
753
  let content =
@@ -822,12 +802,12 @@ export default function fffExtension(pi: ExtensionAPI) {
822
802
  description: `Fuzzy path search and glob search. Matches against the whole repo-relative path, not just the filename. Frecency-ranked, git-aware. Multi-word = narrower (AND). Default limit ${DEFAULT_FIND_LIMIT}.`,
823
803
  promptSnippet: "Find files by path or glob",
824
804
  promptGuidelines: [
825
- "Matches the WHOLE path, not just the filename — `profile` hits `chrome/browser/profiles/x.cc` too.",
826
- "Keep queries to 1-2 terms; extra words narrow.",
827
- "Use for paths, not content. Use grep for content.",
828
- "For exact path matches use a glob in `path` — e.g. path: '**/profile.h' for exact filename, or path: 'src/**/profile.h' scoped to a subtree. Bare patterns are fuzzy.",
829
- "To list everything inside a directory, pass path: 'dir/**' with an empty or wildcard pattern instead of using pattern alone.",
830
- "Use exclude: 'test/,*.min.js' to cut noise in large repos.",
805
+ `${toolNames.find}: matches the WHOLE path, not just the filename — \`profile\` hits \`chrome/browser/profiles/x.cc\` too.`,
806
+ `${toolNames.find}: keep queries to 1-2 terms; extra words narrow.`,
807
+ `${toolNames.find}: use for paths, not content. Use ${toolNames.grep} for content.`,
808
+ `${toolNames.find}: for exact path matches use a glob in \`path\` — e.g. path: '**/profile.h' for exact filename, or path: 'src/**/profile.h' scoped to a subtree. Bare patterns are fuzzy.`,
809
+ `${toolNames.find}: to list everything inside a directory, pass path: 'dir/**' with an empty or wildcard pattern instead of using pattern alone.`,
810
+ `${toolNames.find}: use exclude: 'test/,*.min.js' to cut noise in large repos.`,
831
811
  ],
832
812
  parameters: findSchema,
833
813
 
@@ -839,16 +819,11 @@ export default function fffExtension(pi: ExtensionAPI) {
839
819
  const aux = resumed
840
820
  ? resumed.auxRoot
841
821
  ? {
842
- finder: (await auxPool.acquire(resumed.auxRoot, { exact: true }))
843
- .finder,
822
+ finder: (await auxPool.acquire(resumed.auxRoot, { exact: true })).finder,
844
823
  root: resumed.auxRoot,
845
824
  }
846
825
  : null
847
- : await resolveFinderForPath(
848
- params.path,
849
- params.pattern,
850
- params.exclude,
851
- );
826
+ : await resolveFinderForPath(params.path, params.pattern, params.exclude);
852
827
 
853
828
  const picker = aux ? aux.finder : await ensureFinder(activeCwd);
854
829
  const effectiveLimit = resumed
@@ -880,8 +855,7 @@ export default function fffExtension(pi: ExtensionAPI) {
880
855
  // shown so far there's another page to fetch.
881
856
  const shownSoFar = pageIndex * effectiveLimit + result.items.length;
882
857
  const hasMore =
883
- result.items.length >= effectiveLimit &&
884
- result.totalMatched > shownSoFar;
858
+ result.items.length >= effectiveLimit && result.totalMatched > shownSoFar;
885
859
 
886
860
  const notices: string[] = [];
887
861
  if (formatted.weak && formatted.shownCount > 0)
@@ -916,8 +890,7 @@ export default function fffExtension(pi: ExtensionAPI) {
916
890
  },
917
891
 
918
892
  renderCall(args, theme, context) {
919
- const text =
920
- (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
893
+ const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
921
894
  const pattern = args?.pattern ?? "";
922
895
  const path = args?.path ?? ".";
923
896
  let content =
@@ -950,9 +923,7 @@ export default function fffExtension(pi: ExtensionAPI) {
950
923
  constraints: Type.Optional(
951
924
  Type.String({ description: "File filter, e.g. '*.{ts,tsx} !test/'" }),
952
925
  ),
953
- context: Type.Optional(
954
- Type.Number({ description: "Context lines before+after" }),
955
- ),
926
+ context: Type.Optional(Type.Number({ description: "Context lines before+after" })),
956
927
  limit: Type.Optional(
957
928
  Type.Number({
958
929
  description: `Max matches (default ${DEFAULT_GREP_LIMIT})`,
@@ -968,9 +939,9 @@ export default function fffExtension(pi: ExtensionAPI) {
968
939
  "Search file contents for ANY of multiple literal patterns (OR, SIMD Aho-Corasick). Faster than regex alternation.",
969
940
  promptSnippet: "Multi-pattern OR content search",
970
941
  promptGuidelines: [
971
- "Use when searching for several identifiers at once.",
972
- "Include all naming-convention variants (snake/camel/Pascal).",
973
- "Patterns are literal. Use constraints for file filters.",
942
+ `${toolNames.multiGrep}: use when searching for several identifiers at once.`,
943
+ `${toolNames.multiGrep}: include all naming-convention variants (snake/camel/Pascal).`,
944
+ `${toolNames.multiGrep}: patterns are literal. Use constraints for file filters.`,
974
945
  ],
975
946
  parameters: multiGrepSchema,
976
947
 
@@ -1018,8 +989,7 @@ export default function fffExtension(pi: ExtensionAPI) {
1018
989
  },
1019
990
 
1020
991
  renderCall(args, theme, context) {
1021
- const text =
1022
- (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
992
+ const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
1023
993
  const patterns = args?.patterns ?? [];
1024
994
  const constraints = args?.constraints;
1025
995
  let content =
@@ -1041,8 +1011,7 @@ export default function fffExtension(pi: ExtensionAPI) {
1041
1011
  // --- commands ---
1042
1012
 
1043
1013
  pi.registerCommand("fff-mode", {
1044
- description:
1045
- "Show or set FFF mode: /fff-mode [tools-and-ui | tools-only | override]",
1014
+ description: "Show or set FFF mode: /fff-mode [tools-and-ui | tools-only | override]",
1046
1015
  handler: async (args, ctx) => {
1047
1016
  const arg = (args || "").trim();
1048
1017
 
@@ -1056,10 +1025,7 @@ export default function fffExtension(pi: ExtensionAPI) {
1056
1025
 
1057
1026
  // Validate and set mode
1058
1027
  if (!VALID_MODES.includes(arg as FffMode)) {
1059
- ctx.ui.notify(
1060
- `Usage: /fff-mode [${VALID_MODES.join(" | ")}]`,
1061
- "warning",
1062
- );
1028
+ ctx.ui.notify(`Usage: /fff-mode [${VALID_MODES.join(" | ")}]`, "warning");
1063
1029
  return;
1064
1030
  }
1065
1031
 
package/src/query.ts CHANGED
@@ -10,11 +10,7 @@ export function normalizePathConstraint(
10
10
  if (path.isAbsolute(trimmed)) {
11
11
  const relative = path.relative(cwd, trimmed).replaceAll(path.sep, "/");
12
12
  if (relative === "") return null;
13
- if (
14
- relative.startsWith("../") ||
15
- relative === ".." ||
16
- path.isAbsolute(relative)
17
- ) {
13
+ if (relative.startsWith("../") || relative === ".." || path.isAbsolute(relative)) {
18
14
  throw new Error(
19
15
  `Path constraint must be relative to the workspace: ${pathConstraint}`,
20
16
  );