@ff-labs/pi-fff 0.10.6-nightly.f4def12 → 0.10.6
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 +45 -5
- package/package.json +5 -4
- package/pi-fff.schema.json +51 -0
- package/src/aux-finders.ts +2 -0
- package/src/config.ts +113 -0
- package/src/file-picker.ts +1 -0
- package/src/index.ts +293 -133
- package/src/paths.ts +1 -1
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ Parameters:
|
|
|
116
116
|
|
|
117
117
|
- `/fff-health` — show FFF status (indexed files, git info, frecency/history DB status)
|
|
118
118
|
- `/fff-rescan` — trigger a file rescan
|
|
119
|
-
- `/fff-mode <mode>` — switch mode (tool name
|
|
119
|
+
- `/fff-mode <mode>` — switch mode (tool name changes require `/reload`)
|
|
120
120
|
|
|
121
121
|
## Modes
|
|
122
122
|
|
|
@@ -124,10 +124,47 @@ Parameters:
|
|
|
124
124
|
- `tools-only`: additional tools only; keep pi's default `@` autocomplete
|
|
125
125
|
- `override`: replaces pi's built-in `find`, `grep` and adds `multi_grep` + FFF-backed `@` autocomplete
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
Startup mode precedence:
|
|
128
128
|
1. `--fff-mode <mode>` CLI flag
|
|
129
129
|
2. `PI_FFF_MODE=<mode>` environment variable
|
|
130
|
-
3.
|
|
130
|
+
3. `mode` in the global config file
|
|
131
|
+
4. default (`tools-and-ui`)
|
|
132
|
+
|
|
133
|
+
When a session resumes, its most recent `/fff-mode` selection takes precedence over the startup resolution above. Switching to or from `override` takes effect after `/reload`, when the tools are registered again.
|
|
134
|
+
|
|
135
|
+
## Configuration
|
|
136
|
+
|
|
137
|
+
For persistent global configuration, create `pi-fff.json` in pi's agent directory (`~/.pi/agent/pi-fff.json` by default; `PI_CODING_AGENT_DIR` is respected):
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"$schema": "https://raw.githubusercontent.com/dmtrKovalenko/fff/main/packages/pi-fff/pi-fff.schema.json",
|
|
142
|
+
"mode": "override",
|
|
143
|
+
"frecencyDbPath": "/path/to/frecency",
|
|
144
|
+
"historyDbPath": "/path/to/history",
|
|
145
|
+
"enableFsRootScanning": false,
|
|
146
|
+
"enableHomeDirScanning": true,
|
|
147
|
+
"warnOnHomeDirScan": true,
|
|
148
|
+
"followSymlinks": true
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
All fields are optional:
|
|
153
|
+
|
|
154
|
+
| Field | Type | Default |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| `$schema` | non-empty string | none |
|
|
157
|
+
| `mode` | `tools-and-ui`, `tools-only`, or `override` | `tools-and-ui` |
|
|
158
|
+
| `frecencyDbPath` | non-empty string | See [Data](#data) |
|
|
159
|
+
| `historyDbPath` | non-empty string | See [Data](#data) |
|
|
160
|
+
| `enableFsRootScanning` | boolean | `false` |
|
|
161
|
+
| `enableHomeDirScanning` | boolean | `true` |
|
|
162
|
+
| `warnOnHomeDirScan` | boolean | `true` |
|
|
163
|
+
| `followSymlinks` | boolean | `true` |
|
|
164
|
+
|
|
165
|
+
CLI flags take precedence over environment variables, which take precedence over this file. A missing file is ignored. Malformed JSON, unknown fields, and invalid values stop the extension from loading and report the file path and error. `/fff-mode` changes the current session; it does not edit this file.
|
|
166
|
+
|
|
167
|
+
The file is global only. Project-level config cannot safely control tool names because pi decides which tools an extension registers before project configuration can be trusted.
|
|
131
168
|
|
|
132
169
|
## Flags
|
|
133
170
|
|
|
@@ -136,6 +173,8 @@ Mode precedence:
|
|
|
136
173
|
- `--fff-history-db <path>` — path to query history database (also: `FFF_HISTORY_DB` env). Optional; see [Data](#data) for the default.
|
|
137
174
|
- `--fff-enable-root-scan` — allow indexing when launched from `/` (also: `FFF_ENABLE_ROOT_SCAN=1` env). FFF refuses to init at the filesystem root by default.
|
|
138
175
|
- `--fff-enable-home-scan` — index the home directory when launched from `$HOME` (also: `FFF_ENABLE_HOME_SCAN` env). Enabled by default. Disable with `--fff-enable-home-scan=false` or `FFF_ENABLE_HOME_SCAN=0` if your `$HOME` contains huge trees (toolchains, kernel sources, build outputs) that make the background index run for a long time. When launched from `$HOME` with this enabled, pi shows a warning that the whole home tree is being indexed.
|
|
176
|
+
- `--fff-warn-home-scan` — show the warning notification when `$HOME` is indexed (also: `FFF_WARN_HOME_SCAN` env). Enabled by default. Disable with `--fff-warn-home-scan=false`, `FFF_WARN_HOME_SCAN=0`, or `"warnOnHomeDirScan": false` in `pi-fff.json`. Indexing and the footer status are unaffected.
|
|
177
|
+
- `--fff-follow-symlinks` — index through directory symlinks (also: `FFF_FOLLOW_SYMLINKS` env, or `"followSymlinks"` in `pi-fff.json`). Enabled by default: trees that reach their real files through links — a git worktree whose `docs/` links back to the main checkout, or a stowed dotfiles layout — would otherwise be missing from `@`-mentions and from find/grep with no visible sign. Disable with `--fff-follow-symlinks=false` or `FFF_FOLLOW_SYMLINKS=0` to keep the walk inside the real tree, which is worth doing when a linked target pulls in a large tree outside the workspace. Symlink cycles are detected and broken by the walker.
|
|
139
178
|
|
|
140
179
|
## Data
|
|
141
180
|
|
|
@@ -147,11 +186,12 @@ Each path is resolved independently, in this order:
|
|
|
147
186
|
|
|
148
187
|
1. CLI flag — `--fff-frecency-db` / `--fff-history-db`
|
|
149
188
|
2. Env var — `FFF_FRECENCY_DB` / `FFF_HISTORY_DB`
|
|
150
|
-
3.
|
|
189
|
+
3. Global config — `frecencyDbPath` / `historyDbPath`
|
|
190
|
+
4. An existing [fff.nvim](https://github.com/dmtrKovalenko/fff.nvim) database, so pi reuses the frecency you built up in your editor:
|
|
151
191
|
- frecency: `$XDG_CACHE_HOME/nvim/fff_nvim`
|
|
152
192
|
- history: `$XDG_DATA_HOME/nvim/fff_queries`
|
|
153
193
|
- `XDG_CACHE_HOME` defaults to `~/.cache` and `XDG_DATA_HOME` to `~/.local/share`; on Windows both fall back under `%LOCALAPPDATA%\nvim-data`. Only directories count — a plain file at those paths is ignored.
|
|
154
|
-
|
|
194
|
+
5. pi-local directory, created on demand — `$PI_CODING_AGENT_DIR/fff/{frecency,history}`, defaulting to `~/.pi/agent/fff/{frecency,history}`
|
|
155
195
|
|
|
156
196
|
The extension only reads these databases; it never records the agent's own searches into your Neovim history. If a database cannot be opened, the finder starts without persistence and pi shows a warning instead of failing.
|
|
157
197
|
|
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.6
|
|
4
|
+
"version": "0.10.6",
|
|
5
5
|
"description": "pi extension: FFF-powered fuzzy file and content search",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
]
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
|
-
"src"
|
|
33
|
+
"src",
|
|
34
|
+
"pi-fff.schema.json"
|
|
34
35
|
],
|
|
35
36
|
"publishConfig": {
|
|
36
37
|
"access": "public"
|
|
@@ -40,8 +41,8 @@
|
|
|
40
41
|
"typecheck": "tsc --noEmit"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
|
-
"@ff-labs/fff-bun": "0.10.6
|
|
44
|
-
"@ff-labs/fff-node": "0.10.6
|
|
44
|
+
"@ff-labs/fff-bun": "0.10.6",
|
|
45
|
+
"@ff-labs/fff-node": "0.10.6"
|
|
45
46
|
},
|
|
46
47
|
"peerDependencies": {
|
|
47
48
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/dmtrKovalenko/fff/main/packages/pi-fff/pi-fff.schema.json",
|
|
4
|
+
"title": "pi-fff configuration",
|
|
5
|
+
"description": "Global startup configuration for the pi-fff extension.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"minLength": 1,
|
|
12
|
+
"description": "Schema URL used by editors for validation and completion."
|
|
13
|
+
},
|
|
14
|
+
"mode": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": ["tools-and-ui", "tools-only", "override"],
|
|
17
|
+
"default": "tools-and-ui",
|
|
18
|
+
"description": "Controls which FFF tools and autocomplete integrations are enabled."
|
|
19
|
+
},
|
|
20
|
+
"frecencyDbPath": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1,
|
|
23
|
+
"description": "Path to the frecency database."
|
|
24
|
+
},
|
|
25
|
+
"historyDbPath": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"minLength": 1,
|
|
28
|
+
"description": "Path to the query history database."
|
|
29
|
+
},
|
|
30
|
+
"enableFsRootScanning": {
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"default": false,
|
|
33
|
+
"description": "Allows indexing when pi is launched from the filesystem root."
|
|
34
|
+
},
|
|
35
|
+
"enableHomeDirScanning": {
|
|
36
|
+
"type": "boolean",
|
|
37
|
+
"default": true,
|
|
38
|
+
"description": "Allows indexing when pi is launched from the home directory."
|
|
39
|
+
},
|
|
40
|
+
"warnOnHomeDirScan": {
|
|
41
|
+
"type": "boolean",
|
|
42
|
+
"default": true,
|
|
43
|
+
"description": "Shows a warning notification when the home directory is indexed."
|
|
44
|
+
},
|
|
45
|
+
"followSymlinks": {
|
|
46
|
+
"type": "boolean",
|
|
47
|
+
"default": true,
|
|
48
|
+
"description": "Indexes through directory symlinks, e.g. a git worktree or stow layout whose files live behind links. Set to false to keep the walk inside the real tree."
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/aux-finders.ts
CHANGED
|
@@ -16,6 +16,7 @@ interface AuxPicker {
|
|
|
16
16
|
export interface AuxOpts {
|
|
17
17
|
enableFsRootScanning: boolean;
|
|
18
18
|
enableHomeDirScanning?: boolean;
|
|
19
|
+
followSymlinks?: boolean;
|
|
19
20
|
pickers: FilePickerFactory;
|
|
20
21
|
// Called before a newly spawned aux picker starts a scan that covers $HOME.
|
|
21
22
|
onHomeDirScan?: (root: string) => void;
|
|
@@ -104,6 +105,7 @@ export class AuxFinderPool {
|
|
|
104
105
|
basePath: root,
|
|
105
106
|
enableHomeDirScanning,
|
|
106
107
|
enableFsRootScanning: this.opts.enableFsRootScanning,
|
|
108
|
+
followSymlinks: this.opts.followSymlinks,
|
|
107
109
|
});
|
|
108
110
|
|
|
109
111
|
const entry: AuxPicker = { root, finder, lastUsed: Date.now() };
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { piDataDir } from "./paths";
|
|
4
|
+
|
|
5
|
+
export const CONFIG_FILE_NAME = "pi-fff.json";
|
|
6
|
+
export const VALID_MODES = ["tools-and-ui", "tools-only", "override"] as const;
|
|
7
|
+
|
|
8
|
+
export type FffMode = (typeof VALID_MODES)[number];
|
|
9
|
+
|
|
10
|
+
export interface FffConfig {
|
|
11
|
+
$schema?: string;
|
|
12
|
+
mode?: FffMode;
|
|
13
|
+
frecencyDbPath?: string;
|
|
14
|
+
historyDbPath?: string;
|
|
15
|
+
enableFsRootScanning?: boolean;
|
|
16
|
+
enableHomeDirScanning?: boolean;
|
|
17
|
+
warnOnHomeDirScan?: boolean;
|
|
18
|
+
followSymlinks?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const CONFIG_KEYS = new Set<keyof FffConfig>([
|
|
22
|
+
"$schema",
|
|
23
|
+
"mode",
|
|
24
|
+
"frecencyDbPath",
|
|
25
|
+
"historyDbPath",
|
|
26
|
+
"enableFsRootScanning",
|
|
27
|
+
"enableHomeDirScanning",
|
|
28
|
+
"warnOnHomeDirScan",
|
|
29
|
+
"followSymlinks",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
export function loadConfig(agentDir = piDataDir()): FffConfig {
|
|
33
|
+
const configPath = join(agentDir, CONFIG_FILE_NAME);
|
|
34
|
+
let contents: string;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
contents = readFileSync(configPath, "utf8");
|
|
38
|
+
} catch (error: unknown) {
|
|
39
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Could not read pi-fff config at ${configPath}: ${errorMessage(error)}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let parsed: unknown;
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(contents);
|
|
48
|
+
} catch (error: unknown) {
|
|
49
|
+
throw invalidConfig(configPath, `not valid JSON (${errorMessage(error)})`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!isRecord(parsed)) {
|
|
53
|
+
throw invalidConfig(configPath, "expected a JSON object");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const key of Object.keys(parsed)) {
|
|
57
|
+
if (!CONFIG_KEYS.has(key as keyof FffConfig)) {
|
|
58
|
+
throw invalidConfig(configPath, `unknown option "${key}"`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (parsed.mode !== undefined && !VALID_MODES.includes(parsed.mode as FffMode)) {
|
|
63
|
+
throw invalidConfig(configPath, `"mode" must be one of ${VALID_MODES.join(", ")}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
validateString(configPath, parsed, "$schema");
|
|
67
|
+
validateString(configPath, parsed, "frecencyDbPath");
|
|
68
|
+
validateString(configPath, parsed, "historyDbPath");
|
|
69
|
+
validateBoolean(configPath, parsed, "enableFsRootScanning");
|
|
70
|
+
validateBoolean(configPath, parsed, "enableHomeDirScanning");
|
|
71
|
+
validateBoolean(configPath, parsed, "warnOnHomeDirScan");
|
|
72
|
+
validateBoolean(configPath, parsed, "followSymlinks");
|
|
73
|
+
|
|
74
|
+
return parsed as FffConfig;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function invalidConfig(configPath: string, reason: string): Error {
|
|
78
|
+
return new Error(`Invalid pi-fff config at ${configPath}: ${reason}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function errorMessage(error: unknown): string {
|
|
82
|
+
return error instanceof Error ? error.message : String(error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
86
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function validateString(
|
|
90
|
+
configPath: string,
|
|
91
|
+
config: Record<string, unknown>,
|
|
92
|
+
key: "$schema" | "frecencyDbPath" | "historyDbPath",
|
|
93
|
+
): void {
|
|
94
|
+
const value = config[key];
|
|
95
|
+
if (value !== undefined && (typeof value !== "string" || value.length === 0)) {
|
|
96
|
+
throw invalidConfig(configPath, `"${key}" must be a non-empty string`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validateBoolean(
|
|
101
|
+
configPath: string,
|
|
102
|
+
config: Record<string, unknown>,
|
|
103
|
+
key:
|
|
104
|
+
| "enableFsRootScanning"
|
|
105
|
+
| "enableHomeDirScanning"
|
|
106
|
+
| "warnOnHomeDirScan"
|
|
107
|
+
| "followSymlinks",
|
|
108
|
+
): void {
|
|
109
|
+
const value = config[key];
|
|
110
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
111
|
+
throw invalidConfig(configPath, `"${key}" must be a boolean`);
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/file-picker.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import nodePath from "node:path";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
ExtensionAPI,
|
|
11
|
+
ExtensionContext,
|
|
12
|
+
ToolDefinition,
|
|
13
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
14
|
import {
|
|
11
15
|
type AutocompleteItem,
|
|
12
16
|
type AutocompleteProvider,
|
|
@@ -20,8 +24,9 @@ import type {
|
|
|
20
24
|
MixedItem,
|
|
21
25
|
SearchResult,
|
|
22
26
|
} from "@ff-labs/fff-node";
|
|
23
|
-
import { Type } from "@sinclair/typebox";
|
|
27
|
+
import { Type, type TSchema } from "@sinclair/typebox";
|
|
24
28
|
import { AuxFinderPool, routePathConstraint } from "./aux-finders";
|
|
29
|
+
import { type FffMode, loadConfig, VALID_MODES } from "./config";
|
|
25
30
|
import { FilePickerFactory } from "./file-picker";
|
|
26
31
|
import { isHomeDir, resolveDbPaths } from "./paths";
|
|
27
32
|
import { buildQuery } from "./query";
|
|
@@ -35,6 +40,10 @@ export { SCAN_TIMEOUT_MS } from "./sdk";
|
|
|
35
40
|
const DEFAULT_GREP_LIMIT = 20;
|
|
36
41
|
const DEFAULT_FIND_LIMIT = 30;
|
|
37
42
|
const GREP_PAGE_SIZE_MAX = 50;
|
|
43
|
+
// Per-file match cap. Must stay decoupled from pageSize: grep cursors advance by
|
|
44
|
+
// file offset, so clamping this to pageSize makes same-file overflow unreachable
|
|
45
|
+
// on later pages (#825). Matches the engine default.
|
|
46
|
+
const GREP_MAX_MATCHES_PER_FILE = 200;
|
|
38
47
|
const GREP_CONTEXT_MAX = 20;
|
|
39
48
|
const GREP_MAX_LINE_LENGTH = 500;
|
|
40
49
|
const MENTION_MAX_RESULTS = 20;
|
|
@@ -45,11 +54,8 @@ const GREP_TIME_BUDGET_MS = 10_000;
|
|
|
45
54
|
const HOME_SCAN_STATUS_KEY = "fff";
|
|
46
55
|
const HOME_SCAN_POLL_MS = 1_000;
|
|
47
56
|
const HOME_SCAN_DISABLE_HINT =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
type FffMode = "tools-and-ui" | "tools-only" | "override";
|
|
51
|
-
|
|
52
|
-
const VALID_MODES: FffMode[] = ["tools-and-ui", "tools-only", "override"];
|
|
57
|
+
'You can prevent home dir indexing with --fff-enable-home-scan=false, FFF_ENABLE_HOME_SCAN=0, or "enableHomeDirScanning": false in pi-fff.json. ' +
|
|
58
|
+
'To keep indexing but silence this warning use --fff-warn-home-scan=false, FFF_WARN_HOME_SCAN=0, or "warnOnHomeDirScan": false in pi-fff.json.';
|
|
53
59
|
|
|
54
60
|
interface ToolNames {
|
|
55
61
|
grep: string;
|
|
@@ -299,52 +305,113 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
299
305
|
let finderPromise: Promise<FileFinderApi> | null = null;
|
|
300
306
|
let activeCwd = process.cwd();
|
|
301
307
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
});
|
|
308
|
+
const config = loadConfig();
|
|
309
|
+
|
|
310
|
+
// All startup options use the same flag > env > file > fallback order.
|
|
311
|
+
function getConfigValue<T>(
|
|
312
|
+
flagName: string,
|
|
313
|
+
envName: string,
|
|
314
|
+
fileValue: T | undefined,
|
|
315
|
+
fallback: T,
|
|
316
|
+
parse: (value: unknown) => T | undefined = (value) => value as T,
|
|
317
|
+
): T {
|
|
318
|
+
const flagValue = pi.getFlag(flagName);
|
|
319
|
+
if (flagValue !== undefined) {
|
|
320
|
+
const value = parse(flagValue);
|
|
321
|
+
if (value !== undefined) return value;
|
|
322
|
+
}
|
|
318
323
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (env === "0" || env === "false") return false;
|
|
327
|
-
return fallback;
|
|
324
|
+
const envValue = process.env[envName];
|
|
325
|
+
if (envValue !== undefined) {
|
|
326
|
+
const value = parse(envValue);
|
|
327
|
+
if (value !== undefined) return value;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return fileValue ?? fallback;
|
|
328
331
|
}
|
|
329
|
-
// Root scanning opt-in: FFF refuses to init at / unless this is set.
|
|
330
|
-
const enableFsRootScanning = resolveBoolOpt(
|
|
331
|
-
"fff-enable-root-scan",
|
|
332
|
-
"FFF_ENABLE_ROOT_SCAN",
|
|
333
|
-
);
|
|
334
|
-
// Home dir scanning is on by default (launching pi from $HOME is a normal
|
|
335
|
-
// flow), but configurable so users with huge $HOME trees can opt out.
|
|
336
|
-
const enableHomeDirScanning = resolveBoolOpt(
|
|
337
|
-
"fff-enable-home-scan",
|
|
338
|
-
"FFF_ENABLE_HOME_SCAN",
|
|
339
|
-
true,
|
|
340
|
-
);
|
|
341
332
|
|
|
342
|
-
function
|
|
343
|
-
return
|
|
333
|
+
function parseBoolean(value: unknown): boolean | undefined {
|
|
334
|
+
if (typeof value === "boolean") return value;
|
|
335
|
+
if (value === "1" || value === "true") return true;
|
|
336
|
+
if (value === "0" || value === "false") return false;
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function parseMode(value: unknown): FffMode | undefined {
|
|
341
|
+
return typeof value === "string" && VALID_MODES.includes(value as FffMode)
|
|
342
|
+
? (value as FffMode)
|
|
343
|
+
: undefined;
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
+
let currentMode: FffMode = "tools-and-ui";
|
|
347
|
+
let toolNames = resolveToolNames(currentMode);
|
|
348
|
+
let resolvedDbPaths: ReturnType<typeof resolveDbPaths>;
|
|
349
|
+
let enableFsRootScanning = false;
|
|
350
|
+
let enableHomeDirScanning = true;
|
|
351
|
+
let warnOnHomeDirScan = true;
|
|
352
|
+
let followSymlinks = true;
|
|
353
|
+
|
|
346
354
|
function setMode(mode: FffMode): void {
|
|
347
355
|
currentMode = mode;
|
|
356
|
+
toolNames = resolveToolNames(mode);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function resolveStartupConfig(): void {
|
|
360
|
+
setMode(
|
|
361
|
+
getConfigValue("fff-mode", "PI_FFF_MODE", config.mode, "tools-and-ui", parseMode),
|
|
362
|
+
);
|
|
363
|
+
resolvedDbPaths = resolveDbPaths({
|
|
364
|
+
frecency: getConfigValue(
|
|
365
|
+
"fff-frecency-db",
|
|
366
|
+
"FFF_FRECENCY_DB",
|
|
367
|
+
config.frecencyDbPath,
|
|
368
|
+
undefined,
|
|
369
|
+
),
|
|
370
|
+
history: getConfigValue(
|
|
371
|
+
"fff-history-db",
|
|
372
|
+
"FFF_HISTORY_DB",
|
|
373
|
+
config.historyDbPath,
|
|
374
|
+
undefined,
|
|
375
|
+
),
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Root scanning opt-in: FFF refuses to init at / unless this is set.
|
|
379
|
+
enableFsRootScanning = getConfigValue(
|
|
380
|
+
"fff-enable-root-scan",
|
|
381
|
+
"FFF_ENABLE_ROOT_SCAN",
|
|
382
|
+
config.enableFsRootScanning,
|
|
383
|
+
false,
|
|
384
|
+
parseBoolean,
|
|
385
|
+
);
|
|
386
|
+
// Home dir scanning is on by default (launching pi from $HOME is a normal
|
|
387
|
+
// flow), but configurable so users with huge $HOME trees can opt out.
|
|
388
|
+
enableHomeDirScanning = getConfigValue(
|
|
389
|
+
"fff-enable-home-scan",
|
|
390
|
+
"FFF_ENABLE_HOME_SCAN",
|
|
391
|
+
config.enableHomeDirScanning,
|
|
392
|
+
true,
|
|
393
|
+
parseBoolean,
|
|
394
|
+
);
|
|
395
|
+
warnOnHomeDirScan = getConfigValue(
|
|
396
|
+
"fff-warn-home-scan",
|
|
397
|
+
"FFF_WARN_HOME_SCAN",
|
|
398
|
+
config.warnOnHomeDirScan,
|
|
399
|
+
true,
|
|
400
|
+
parseBoolean,
|
|
401
|
+
);
|
|
402
|
+
// On by default: worktree and stow layouts reach their files through links,
|
|
403
|
+
// and an agent silently missing them is worse than the extra walk.
|
|
404
|
+
followSymlinks = getConfigValue(
|
|
405
|
+
"fff-follow-symlinks",
|
|
406
|
+
"FFF_FOLLOW_SYMLINKS",
|
|
407
|
+
config.followSymlinks,
|
|
408
|
+
true,
|
|
409
|
+
parseBoolean,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function getMode(): FffMode {
|
|
414
|
+
return currentMode;
|
|
348
415
|
}
|
|
349
416
|
|
|
350
417
|
function shouldEnableMentions(): boolean {
|
|
@@ -362,28 +429,36 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
362
429
|
let homeScanTimer: ReturnType<typeof setInterval> | null = null;
|
|
363
430
|
|
|
364
431
|
function warnHomeDirScan(root: string): void {
|
|
432
|
+
if (!warnOnHomeDirScan) return;
|
|
365
433
|
uiCtx?.ui.notify(
|
|
366
434
|
`(fff): Your cwd (${root}) is too large. Indexing will take additional time and resources.\n${HOME_SCAN_DISABLE_HINT}`,
|
|
367
435
|
"warning",
|
|
368
436
|
);
|
|
369
437
|
}
|
|
370
438
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
historyDbPath: resolvedDbPaths.history,
|
|
374
|
-
onDbFailure: (error) =>
|
|
375
|
-
uiCtx?.ui.notify(
|
|
376
|
-
`(fff): Failed to open frecency/history database (${error}). Continuing without frecency persistence.`,
|
|
377
|
-
"error",
|
|
378
|
-
),
|
|
379
|
-
});
|
|
439
|
+
let pickers: FilePickerFactory | null = null;
|
|
440
|
+
let auxPool: AuxFinderPool | null = null;
|
|
380
441
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
442
|
+
function initializeFinderFactories(): void {
|
|
443
|
+
if (pickers) return;
|
|
444
|
+
|
|
445
|
+
pickers = new FilePickerFactory({
|
|
446
|
+
frecencyDbPath: resolvedDbPaths.frecency,
|
|
447
|
+
historyDbPath: resolvedDbPaths.history,
|
|
448
|
+
onDbFailure: (error) =>
|
|
449
|
+
uiCtx?.ui.notify(
|
|
450
|
+
`(fff): Failed to open frecency/history database (${error}). Continuing without frecency persistence.`,
|
|
451
|
+
"error",
|
|
452
|
+
),
|
|
453
|
+
});
|
|
454
|
+
auxPool = new AuxFinderPool({
|
|
455
|
+
enableFsRootScanning,
|
|
456
|
+
enableHomeDirScanning,
|
|
457
|
+
followSymlinks,
|
|
458
|
+
onHomeDirScan: warnHomeDirScan,
|
|
459
|
+
pickers,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
387
462
|
|
|
388
463
|
// in case cwd changes we need to figure this out
|
|
389
464
|
function ensureFinder(cwd: string): Promise<FileFinderApi> {
|
|
@@ -401,10 +476,12 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
401
476
|
|
|
402
477
|
// if the dbs can't be opened the factory falls back to a db-less picker,
|
|
403
478
|
// e.g. when some other process corrupts the lock
|
|
479
|
+
if (!pickers) throw new Error("FFF picker factory is not initialized");
|
|
404
480
|
mainFinder = await pickers.create({
|
|
405
481
|
basePath: cwd,
|
|
406
482
|
enableHomeDirScanning,
|
|
407
483
|
enableFsRootScanning,
|
|
484
|
+
followSymlinks,
|
|
408
485
|
});
|
|
409
486
|
finderCwd = cwd;
|
|
410
487
|
return mainFinder;
|
|
@@ -455,9 +532,9 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
455
532
|
finderCwd = null;
|
|
456
533
|
}
|
|
457
534
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
535
|
+
auxPool?.destroy();
|
|
536
|
+
auxPool = null;
|
|
537
|
+
pickers = null;
|
|
461
538
|
}
|
|
462
539
|
|
|
463
540
|
async function resolveFinderForPath(
|
|
@@ -467,6 +544,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
467
544
|
): Promise<{ finder: FileFinderApi; query: string; root: string } | null> {
|
|
468
545
|
const route = routePathConstraint(pathParam, activeCwd);
|
|
469
546
|
if (!route) return null;
|
|
547
|
+
if (!auxPool) throw new Error("FFF auxiliary finder pool is not initialized");
|
|
470
548
|
const aux = await auxPool.acquire(route.root);
|
|
471
549
|
// A broader covering picker may have been reused; rebase the suffix so the
|
|
472
550
|
// constraint stays relative to the picker's actual root.
|
|
@@ -547,6 +625,45 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
547
625
|
});
|
|
548
626
|
}
|
|
549
627
|
|
|
628
|
+
type PendingToolDefinition<
|
|
629
|
+
TParams extends TSchema,
|
|
630
|
+
TDetails = unknown,
|
|
631
|
+
TState = any,
|
|
632
|
+
> = Omit<
|
|
633
|
+
ToolDefinition<TParams, TDetails, TState>,
|
|
634
|
+
"name" | "label" | "promptGuidelines"
|
|
635
|
+
> & {
|
|
636
|
+
promptGuidelines?: (names: ToolNames) => string[];
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const pendingTools: (() => string)[] = [];
|
|
640
|
+
let toolsRegistered = false;
|
|
641
|
+
|
|
642
|
+
function queueTool<TParams extends TSchema, TDetails = unknown, TState = any>(
|
|
643
|
+
resolveName: () => string,
|
|
644
|
+
definition: PendingToolDefinition<TParams, TDetails, TState>,
|
|
645
|
+
): void {
|
|
646
|
+
pendingTools.push(() => {
|
|
647
|
+
const { promptGuidelines, ...tool } = definition;
|
|
648
|
+
const resolvedName = resolveName();
|
|
649
|
+
pi.registerTool({
|
|
650
|
+
...tool,
|
|
651
|
+
name: resolvedName,
|
|
652
|
+
label: resolvedName,
|
|
653
|
+
promptGuidelines: promptGuidelines?.(toolNames),
|
|
654
|
+
});
|
|
655
|
+
return resolvedName;
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function registerPendingTools(): void {
|
|
660
|
+
if (toolsRegistered) return;
|
|
661
|
+
|
|
662
|
+
const registeredNames = pendingTools.map((register) => register());
|
|
663
|
+
pi.setActiveTools([...new Set([...pi.getActiveTools(), ...registeredNames])]);
|
|
664
|
+
toolsRegistered = true;
|
|
665
|
+
}
|
|
666
|
+
|
|
550
667
|
// --- Flags / lifecycle ---
|
|
551
668
|
|
|
552
669
|
pi.registerFlag("fff-mode", {
|
|
@@ -576,34 +693,60 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
576
693
|
type: "boolean",
|
|
577
694
|
});
|
|
578
695
|
|
|
579
|
-
pi.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
696
|
+
pi.registerFlag("fff-follow-symlinks", {
|
|
697
|
+
description:
|
|
698
|
+
"Index through directory symlinks, e.g. a git worktree or stow layout (default true; disable with --fff-follow-symlinks=false or FFF_FOLLOW_SYMLINKS=0)",
|
|
699
|
+
type: "boolean",
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
pi.registerFlag("fff-warn-home-scan", {
|
|
703
|
+
description:
|
|
704
|
+
"Warn when indexing $HOME (default true; silence with --fff-warn-home-scan=false or FFF_WARN_HOME_SCAN=0)",
|
|
705
|
+
type: "boolean",
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
function reportInitFailure(ctx: ExtensionContext, error: unknown): void {
|
|
709
|
+
ctx.ui.notify(
|
|
710
|
+
`FFF init failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
711
|
+
"error",
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function prepareSession(ctx: ExtensionContext): void {
|
|
716
|
+
activeCwd = ctx.cwd;
|
|
717
|
+
uiCtx = ctx;
|
|
718
|
+
if (toolsRegistered) return;
|
|
719
|
+
|
|
720
|
+
// Pi populates extension flag values after loading extensions.
|
|
721
|
+
resolveStartupConfig();
|
|
722
|
+
|
|
723
|
+
// Restore persisted mode before registering tools so a saved override
|
|
724
|
+
// can safely change their names after /reload or session resume.
|
|
725
|
+
const entries = ctx.sessionManager?.getEntries();
|
|
726
|
+
if (entries) {
|
|
727
|
+
const modeEntry = [...entries]
|
|
728
|
+
.reverse()
|
|
729
|
+
.find(
|
|
730
|
+
(e: { type: string; customType?: string }) =>
|
|
731
|
+
e.type === "custom" && e.customType === "fff-mode",
|
|
732
|
+
);
|
|
733
|
+
if (
|
|
734
|
+
modeEntry &&
|
|
735
|
+
typeof (modeEntry as any).data?.mode === "string" &&
|
|
736
|
+
VALID_MODES.includes((modeEntry as any).data.mode as FffMode)
|
|
737
|
+
) {
|
|
738
|
+
const restored = (modeEntry as any).data.mode as FffMode;
|
|
739
|
+
if (restored !== currentMode) setMode(restored);
|
|
605
740
|
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
initializeFinderFactories();
|
|
744
|
+
registerPendingTools();
|
|
745
|
+
}
|
|
606
746
|
|
|
747
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
748
|
+
try {
|
|
749
|
+
prepareSession(ctx);
|
|
607
750
|
registerAutocompleteProvider(ctx);
|
|
608
751
|
await ensureFinder(activeCwd);
|
|
609
752
|
|
|
@@ -621,11 +764,19 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
621
764
|
// waitForScan() also resolves on timeout, so poll until the scan really
|
|
622
765
|
// settles before clearing the footer.
|
|
623
766
|
if (atHome) trackHomeScanStatus();
|
|
624
|
-
} catch (
|
|
625
|
-
ctx
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
767
|
+
} catch (error: unknown) {
|
|
768
|
+
reportInitFailure(ctx, error);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// SDK callers can prompt without binding session_start. Prepare on the first
|
|
773
|
+
// agent turn as a fallback so the tools still reach that turn's tool set.
|
|
774
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
775
|
+
if (toolsRegistered) return;
|
|
776
|
+
try {
|
|
777
|
+
prepareSession(ctx);
|
|
778
|
+
} catch (error: unknown) {
|
|
779
|
+
reportInitFailure(ctx, error);
|
|
629
780
|
}
|
|
630
781
|
});
|
|
631
782
|
|
|
@@ -701,16 +852,14 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
701
852
|
),
|
|
702
853
|
});
|
|
703
854
|
|
|
704
|
-
|
|
705
|
-
name: toolNames.grep,
|
|
706
|
-
label: toolNames.grep,
|
|
855
|
+
queueTool(() => toolNames.grep, {
|
|
707
856
|
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}.`,
|
|
708
857
|
promptSnippet: "Grep contents",
|
|
709
|
-
promptGuidelines: [
|
|
710
|
-
`${
|
|
711
|
-
`${
|
|
712
|
-
`${
|
|
713
|
-
`${
|
|
858
|
+
promptGuidelines: (names) => [
|
|
859
|
+
`${names.grep}: prefer bare identifiers as patterns. Literal queries are most efficient.`,
|
|
860
|
+
`${names.grep}: use path for include ('src/', '*.ts') and exclude for noise ('test/,*.min.js').`,
|
|
861
|
+
`${names.grep}: caseSensitive: true when you need exact case (smart-case otherwise).`,
|
|
862
|
+
`${names.grep}: after 1-2 greps, read the top match instead of more greps.`,
|
|
714
863
|
],
|
|
715
864
|
parameters: grepSchema,
|
|
716
865
|
|
|
@@ -722,8 +871,9 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
722
871
|
|
|
723
872
|
const picker = aux ? aux.finder : await ensureFinder(activeCwd);
|
|
724
873
|
const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT);
|
|
725
|
-
// pageSize caps TOTAL matches across all files
|
|
726
|
-
//
|
|
874
|
+
// pageSize caps TOTAL matches across all files (soft cap: the current file
|
|
875
|
+
// is always finished first). maxMatchesPerFile stays decoupled at the engine
|
|
876
|
+
// default so same-file overflow remains reachable via cursor (#825).
|
|
727
877
|
const pageSize = Math.min(effectiveLimit, GREP_PAGE_SIZE_MAX);
|
|
728
878
|
const context = clampContext(params.context);
|
|
729
879
|
const query = aux
|
|
@@ -773,7 +923,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
773
923
|
const grepResult = picker.grep(query, {
|
|
774
924
|
mode,
|
|
775
925
|
smartCase,
|
|
776
|
-
maxMatchesPerFile:
|
|
926
|
+
maxMatchesPerFile: GREP_MAX_MATCHES_PER_FILE,
|
|
777
927
|
pageSize,
|
|
778
928
|
cursor: (params.cursor ? getCursor(params.cursor) : null) ?? null,
|
|
779
929
|
beforeContext: context,
|
|
@@ -806,7 +956,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
806
956
|
const fuzzy = picker.grep(fuzzyQuery, {
|
|
807
957
|
mode: "fuzzy",
|
|
808
958
|
smartCase,
|
|
809
|
-
maxMatchesPerFile:
|
|
959
|
+
maxMatchesPerFile: GREP_MAX_MATCHES_PER_FILE,
|
|
810
960
|
pageSize,
|
|
811
961
|
cursor: null,
|
|
812
962
|
beforeContext: 0,
|
|
@@ -892,18 +1042,16 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
892
1042
|
),
|
|
893
1043
|
});
|
|
894
1044
|
|
|
895
|
-
|
|
896
|
-
name: toolNames.find,
|
|
897
|
-
label: toolNames.find,
|
|
1045
|
+
queueTool(() => toolNames.find, {
|
|
898
1046
|
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}.`,
|
|
899
1047
|
promptSnippet: "Find files by path or glob",
|
|
900
|
-
promptGuidelines: [
|
|
901
|
-
`${
|
|
902
|
-
`${
|
|
903
|
-
`${
|
|
904
|
-
`${
|
|
905
|
-
`${
|
|
906
|
-
`${
|
|
1048
|
+
promptGuidelines: (names) => [
|
|
1049
|
+
`${names.find}: matches the WHOLE path, not just the filename — \`profile\` hits \`chrome/browser/profiles/x.cc\` too.`,
|
|
1050
|
+
`${names.find}: keep queries to 1-2 terms; extra words narrow.`,
|
|
1051
|
+
`${names.find}: use for paths, not content. Use ${names.grep} for content.`,
|
|
1052
|
+
`${names.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.`,
|
|
1053
|
+
`${names.find}: to list everything inside a directory, pass path: 'dir/**' with an empty or wildcard pattern instead of using pattern alone.`,
|
|
1054
|
+
`${names.find}: use exclude: 'test/,*.min.js' to cut noise in large repos.`,
|
|
907
1055
|
],
|
|
908
1056
|
parameters: findSchema,
|
|
909
1057
|
|
|
@@ -912,10 +1060,12 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
912
1060
|
|
|
913
1061
|
// if resumed we use the same picker as before
|
|
914
1062
|
const resumed = params.cursor ? getFindCursor(params.cursor) : undefined;
|
|
1063
|
+
const pool = auxPool;
|
|
1064
|
+
if (!pool) throw new Error("FFF auxiliary finder pool is not initialized");
|
|
915
1065
|
const aux = resumed
|
|
916
1066
|
? resumed.auxRoot
|
|
917
1067
|
? {
|
|
918
|
-
finder: (await
|
|
1068
|
+
finder: (await pool.acquire(resumed.auxRoot, { exact: true })).finder,
|
|
919
1069
|
root: resumed.auxRoot,
|
|
920
1070
|
}
|
|
921
1071
|
: null
|
|
@@ -1032,16 +1182,14 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1032
1182
|
cursor: Type.Optional(Type.String({ description: "Pagination cursor" })),
|
|
1033
1183
|
});
|
|
1034
1184
|
|
|
1035
|
-
|
|
1036
|
-
name: toolNames.multiGrep,
|
|
1037
|
-
label: toolNames.multiGrep,
|
|
1185
|
+
queueTool(() => toolNames.multiGrep, {
|
|
1038
1186
|
description:
|
|
1039
1187
|
"Search file contents for ANY of multiple literal patterns (OR, SIMD Aho-Corasick). Faster than regex alternation.",
|
|
1040
1188
|
promptSnippet: "Multi-pattern OR content search",
|
|
1041
|
-
promptGuidelines: [
|
|
1042
|
-
`${
|
|
1043
|
-
`${
|
|
1044
|
-
`${
|
|
1189
|
+
promptGuidelines: (names) => [
|
|
1190
|
+
`${names.multiGrep}: use when searching for several identifiers at once.`,
|
|
1191
|
+
`${names.multiGrep}: include all naming-convention variants (snake/camel/Pascal).`,
|
|
1192
|
+
`${names.multiGrep}: patterns are literal. Use constraints for file filters.`,
|
|
1045
1193
|
],
|
|
1046
1194
|
parameters: multiGrepSchema,
|
|
1047
1195
|
|
|
@@ -1058,7 +1206,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1058
1206
|
const grepResult = f.multiGrep({
|
|
1059
1207
|
patterns: params.patterns,
|
|
1060
1208
|
constraints: params.constraints,
|
|
1061
|
-
maxMatchesPerFile:
|
|
1209
|
+
maxMatchesPerFile: GREP_MAX_MATCHES_PER_FILE,
|
|
1062
1210
|
pageSize,
|
|
1063
1211
|
smartCase: true,
|
|
1064
1212
|
cursor: (params.cursor ? getCursor(params.cursor) : null) ?? null,
|
|
@@ -1116,6 +1264,15 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1116
1264
|
pi.registerCommand("fff-mode", {
|
|
1117
1265
|
description: "Show or set FFF mode: /fff-mode [tools-and-ui | tools-only | override]",
|
|
1118
1266
|
handler: async (args, ctx) => {
|
|
1267
|
+
if (!toolsRegistered) {
|
|
1268
|
+
try {
|
|
1269
|
+
prepareSession(ctx);
|
|
1270
|
+
} catch (error: unknown) {
|
|
1271
|
+
reportInitFailure(ctx, error);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1119
1276
|
const arg = (args || "").trim();
|
|
1120
1277
|
|
|
1121
1278
|
// No args - show current mode
|
|
@@ -1134,15 +1291,18 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1134
1291
|
|
|
1135
1292
|
const newMode = arg as FffMode;
|
|
1136
1293
|
const oldMode = getMode();
|
|
1137
|
-
setMode(newMode);
|
|
1138
|
-
|
|
1139
1294
|
pi.appendEntry("fff-mode", { mode: newMode });
|
|
1140
1295
|
|
|
1141
|
-
|
|
1142
|
-
(
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1296
|
+
if ((oldMode === "override") !== (newMode === "override")) {
|
|
1297
|
+
ctx.ui.notify(
|
|
1298
|
+
`Mode '${newMode}' saved. Run /reload to apply the tool name change.`,
|
|
1299
|
+
"info",
|
|
1300
|
+
);
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
setMode(newMode);
|
|
1305
|
+
ctx.ui.notify(`Mode changed: '${oldMode}' → '${newMode}'`, "info");
|
|
1146
1306
|
},
|
|
1147
1307
|
});
|
|
1148
1308
|
|
package/src/paths.ts
CHANGED
|
@@ -52,7 +52,7 @@ function nvimDataDir(): string {
|
|
|
52
52
|
return path.join(HOME_DIR, ".local", "share", "nvim");
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function piDataDir(): string {
|
|
55
|
+
export function piDataDir(): string {
|
|
56
56
|
return process.env.PI_CODING_AGENT_DIR ?? path.join(HOME_DIR, ".pi", "agent");
|
|
57
57
|
}
|
|
58
58
|
|