pi-fovea 0.3.2 → 0.3.3
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 +23 -0
- package/package.json +1 -1
- package/skills/pi-fovea/SKILL.md +4 -3
- package/src/core/config.ts +4 -0
- package/src/index.ts +59 -3
- package/src/ui/settings.ts +13 -2
package/README.md
CHANGED
|
@@ -29,6 +29,28 @@ After each assistant turn the map re-syncs incrementally. Detection reads conten
|
|
|
29
29
|
| `fovea_focus` | what is this? | centered on a symbol, route path, or env key: hot nodes as signatures, neighbors as one-liners |
|
|
30
30
|
| `fovea_dwell` | what else? | diffuses the field one step further and returns the delta |
|
|
31
31
|
| `fovea_impact` | what does this touch? | warms everything a file, symbol, or PR base reaches across languages |
|
|
32
|
+
| `grep` *(default override)* | where does this concept lead? | the same graph-backed focus through grep's familiar `pattern/path/glob/...` signature |
|
|
33
|
+
|
|
34
|
+
The **Replace grep** toggle makes Fovea own Pi's `grep` tool slot. It is on by default. Familiar calls such as `grep({ pattern: "CreateUser", path: "src" })` navigate the code graph first; use `bash` with `rg` only when you need exact matching lines. Disable the toggle to restore the previous grep implementation. Changing the toggle reloads extensions so pi-fabric captures the same override and `pi.grep(...)` follows it inside `fabric_exec`.
|
|
35
|
+
|
|
36
|
+
### pi-fabric
|
|
37
|
+
|
|
38
|
+
Captured extension tools live under Fabric's `extensions` provider. Use the direct proxy when the action is known:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const result = await extensions.fovea_focus({ query: "CreateUserHandler", maxTokens: 2000 });
|
|
42
|
+
return result.text;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For dynamic discovery, pass an object to `tools.search` and keep the returned namespaced ref:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const [action] = await tools.search({ query: "fovea_focus", limit: 5 });
|
|
49
|
+
if (!action) return "Fovea is not captured";
|
|
50
|
+
return tools.call({ ref: action.ref, args: { query: "CreateUserHandler" } });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The stable explicit ref is `extensions.fovea_focus`, not bare `fovea_focus` or `fovea.fovea_focus`.
|
|
32
54
|
|
|
33
55
|
Two slash commands on top:
|
|
34
56
|
|
|
@@ -99,6 +121,7 @@ Global settings live in `~/.pi/agent/fovea.json`. A trusted repo-level override
|
|
|
99
121
|
| `sync.ackClean` | `false` | toast after clean structural turns |
|
|
100
122
|
| `sync.warmFileThreshold` | `2` | warmed files unseen by the model that justify turning red |
|
|
101
123
|
| `tools.defaultBudget` | `2000` | fallback maxTokens for the fovea_* tools |
|
|
124
|
+
| `tools.replaceGrep` | `true` | replace Pi's grep slot with graph-backed Fovea navigation |
|
|
102
125
|
|
|
103
126
|
## How routes are found
|
|
104
127
|
|
package/package.json
CHANGED
package/skills/pi-fovea/SKILL.md
CHANGED
|
@@ -32,8 +32,9 @@ Sync is **mutation-path agnostic**: pi's edit/write tools, a pi-fabric `fabric_e
|
|
|
32
32
|
|
|
33
33
|
When writing or editing code **inside a `fabric_exec` program**, the fovea tools exist but the fabric sandbox has no built-in knowledge of them (it lazy-loads tools). Key points:
|
|
34
34
|
|
|
35
|
-
- Inside `fabric_exec`,
|
|
36
|
-
-
|
|
35
|
+
- Inside `fabric_exec`, captured extension tools use the `extensions` provider. For a known action call `await extensions.fovea_focus({ query: "CreateUserHandler", maxTokens: 6000 })`.
|
|
36
|
+
- For dynamic discovery, use `const hits = await tools.search({ query: "fovea_focus" })`, then call the returned namespaced ref with `tools.call({ ref: hits[0].ref, args: { query: "CreateUserHandler", maxTokens: 6000 } })`. The stable explicit ref is `extensions.fovea_focus`; bare `fovea_focus` and `fovea.fovea_focus` are invalid.
|
|
37
|
+
- Prefer a single `extensions.fovea_impact(...)` call over hand-rolled grep fan-outs when computing what an edit touches — the graph already resolved imports/calls across Go, TypeScript, Python, and Java.
|
|
37
38
|
- Any file mutation performed by the program (including `pi.edit`/`pi.write` calls inside the sandbox) is picked up by turn sync automatically, so post-edit verification does not need a re-sketch.
|
|
38
39
|
- The sketch `details` field carries counts (`files`, `nodes`, `anchors`); the hot-node list is the graph's highest-value entry points. On an unfamiliar repo, fetch it once and reuse instead of rediscovering entry points per call.
|
|
39
40
|
|
|
@@ -43,4 +44,4 @@ The same engine runs headlessly as the `fovea` binary (repo root scan, plus JSON
|
|
|
43
44
|
|
|
44
45
|
## Settings
|
|
45
46
|
|
|
46
|
-
`/fovea settings` in the TUI, or `fovea.
|
|
47
|
+
`/fovea settings` in the TUI, or `fovea.json` under `~/.pi/agent/` or a trusted repo's `.pi/` directory. Relevant knobs: `sync.enabled`, `sync.budget`, `sync.warmFileThreshold` (files that must escape before a red sync fires), `tools.defaultBudget`, and `tools.replaceGrep` (default on; installs a grep-compatible Fovea override and reloads extensions).
|
package/src/core/config.ts
CHANGED
|
@@ -22,6 +22,8 @@ interface FoveaSyncConfig {
|
|
|
22
22
|
interface FoveaToolsConfig {
|
|
23
23
|
/** Budget applied when a fovea_* tool call omits maxTokens. */
|
|
24
24
|
defaultBudget: number;
|
|
25
|
+
/** Replace Pi's grep slot with a graph-backed fovea_focus adapter. */
|
|
26
|
+
replaceGrep: boolean;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export interface FoveaConfig {
|
|
@@ -38,6 +40,7 @@ export const DEFAULT_FOVEA_CONFIG: FoveaConfig = {
|
|
|
38
40
|
},
|
|
39
41
|
tools: {
|
|
40
42
|
defaultBudget: 2000,
|
|
43
|
+
replaceGrep: true,
|
|
41
44
|
},
|
|
42
45
|
};
|
|
43
46
|
|
|
@@ -81,6 +84,7 @@ const applyPartial = (base: FoveaConfig, partial: unknown): FoveaConfig => {
|
|
|
81
84
|
},
|
|
82
85
|
tools: {
|
|
83
86
|
defaultBudget: intValue("tools.defaultBudget", tools.defaultBudget, base.tools.defaultBudget),
|
|
87
|
+
replaceGrep: boolValue(tools.replaceGrep, base.tools.replaceGrep),
|
|
84
88
|
},
|
|
85
89
|
};
|
|
86
90
|
};
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,15 @@ const BudgetParam = Type.Optional(
|
|
|
17
17
|
const RootParam = Type.Optional(
|
|
18
18
|
Type.String({ description: "Repo root to map. Defaults to the session working directory." }),
|
|
19
19
|
);
|
|
20
|
+
const GrepParams = Type.Object({
|
|
21
|
+
pattern: Type.String({ description: "Symbol, route, environment key, or file query for the Fovea code graph." }),
|
|
22
|
+
path: Type.Optional(Type.String({ description: "Compatibility path hint. Used as a fallback graph seed when pattern finds no node." })),
|
|
23
|
+
glob: Type.Optional(Type.String({ description: "Accepted for grep-call compatibility; Fovea navigation is graph-based rather than glob-filtered." })),
|
|
24
|
+
ignoreCase: Type.Optional(Type.Boolean({ description: "Accepted for grep-call compatibility; Fovea symbol matching is already case-insensitive." })),
|
|
25
|
+
literal: Type.Optional(Type.Boolean({ description: "Accepted for grep-call compatibility; the pattern is interpreted as a graph query." })),
|
|
26
|
+
context: Type.Optional(Type.Number({ description: "Accepted for grep-call compatibility; graph neighbors replace line context." })),
|
|
27
|
+
limit: Type.Optional(Type.Number({ description: "Accepted for grep-call compatibility; output is controlled by tools.defaultBudget." })),
|
|
28
|
+
});
|
|
20
29
|
|
|
21
30
|
const text = (s: string) => ({ type: "text" as const, text: s });
|
|
22
31
|
|
|
@@ -31,11 +40,54 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
31
40
|
return cfg;
|
|
32
41
|
};
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
let grepOverrideRegistered = false;
|
|
44
|
+
const registerGrepOverride = (): void => {
|
|
45
|
+
if (grepOverrideRegistered) return;
|
|
46
|
+
grepOverrideRegistered = true;
|
|
47
|
+
pi.registerTool({
|
|
48
|
+
name: "grep",
|
|
49
|
+
label: "grep (Fovea)",
|
|
50
|
+
description:
|
|
51
|
+
"Navigate the pi-fovea code graph through grep's familiar argument shape. Finds symbols, routes, environment keys, files, and their warm dependencies; it does not perform literal line matching. Use bash with rg only when exact text or regex matches are required.",
|
|
52
|
+
promptSnippet: "Navigate the Fovea code graph with a grep-compatible query",
|
|
53
|
+
promptGuidelines: [
|
|
54
|
+
"Use grep for graph-backed repository navigation before exact text search; when the Fovea override is active, grep centers the code graph on pattern and returns warm dependencies rather than matching lines.",
|
|
55
|
+
"Use bash with rg only when an exact literal or regular-expression text match is required after the Fovea-backed grep result.",
|
|
56
|
+
],
|
|
57
|
+
parameters: GrepParams,
|
|
58
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
59
|
+
const root = ctx.cwd;
|
|
60
|
+
const budget = configFor(root, ctx.isProjectTrusted()).tools.defaultBudget;
|
|
61
|
+
const pattern = params.pattern.trim();
|
|
62
|
+
const pathHint = params.path?.replace(/^@/, "").trim();
|
|
63
|
+
let query = pattern || pathHint || params.pattern;
|
|
64
|
+
try {
|
|
65
|
+
let result = focus(root, query, budget);
|
|
66
|
+
if (Number(result.details.seeds ?? 0) === 0 && pathHint && pathHint !== "." && pathHint !== query) {
|
|
67
|
+
query = pathHint;
|
|
68
|
+
result = focus(root, query, budget);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
content: [text(result.text.replace(/^fovea focus/, "fovea grep"))],
|
|
72
|
+
details: { ...result.details, backend: "fovea", query },
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
content: [text(String(error instanceof Error ? error.message : error))],
|
|
77
|
+
details: { backend: "fovea", query },
|
|
78
|
+
isError: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
pi.on("session_start", async (event, ctx) => {
|
|
35
86
|
if (event.reason === "new" || event.reason === "fork") {
|
|
36
87
|
resetSessions();
|
|
37
88
|
resetSyncBaselines();
|
|
38
89
|
}
|
|
90
|
+
if (configFor(ctx.cwd, ctx.isProjectTrusted()).tools.replaceGrep) registerGrepOverride();
|
|
39
91
|
});
|
|
40
92
|
|
|
41
93
|
// Turn-sync loop. The tracker below is a hint accumulator only: pi's
|
|
@@ -179,14 +231,18 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
179
231
|
handler: async (args, ctx) => {
|
|
180
232
|
const sub = args.trim().split(/\s+/)[0] ?? "status";
|
|
181
233
|
if (sub === "settings") {
|
|
182
|
-
await openFoveaSettings(ctx, { onConfigApplied: () => configs.clear() });
|
|
234
|
+
const result = await openFoveaSettings(ctx, { onConfigApplied: () => configs.clear() });
|
|
235
|
+
if (result.grepRegistrationChanged) {
|
|
236
|
+
ctx.ui.notify("Reloading extensions to apply the grep tool change…", "info");
|
|
237
|
+
await ctx.reload();
|
|
238
|
+
}
|
|
183
239
|
return;
|
|
184
240
|
}
|
|
185
241
|
try {
|
|
186
242
|
const s = sketch(ctx.cwd, 256);
|
|
187
243
|
const cfg = configFor(ctx.cwd, ctx.isProjectTrusted());
|
|
188
244
|
ctx.ui.notify(
|
|
189
|
-
`pi-fovea: ${s.details.files ?? 0} files, ${s.details.nodes ?? 0} nodes, ${s.details.anchors ?? 0} anchors · sync ${cfg.sync.enabled ? "on" : "off"}`,
|
|
245
|
+
`pi-fovea: ${s.details.files ?? 0} files, ${s.details.nodes ?? 0} nodes, ${s.details.anchors ?? 0} anchors · sync ${cfg.sync.enabled ? "on" : "off"} · grep ${cfg.tools.replaceGrep ? "fovea" : "native"}`,
|
|
190
246
|
"info",
|
|
191
247
|
);
|
|
192
248
|
} catch (e) {
|
package/src/ui/settings.ts
CHANGED
|
@@ -194,6 +194,11 @@ const buildItems = (
|
|
|
194
194
|
"How many undisclosed files must warm up during sync to justify a red message. Higher = fewer interruptions; route anchor shifts always escalate.",
|
|
195
195
|
submenu: numericSubmenu(theme, THRESHOLDS, "Warm file threshold", "Disclosed-file warming count that escalates sync to red."),
|
|
196
196
|
}),
|
|
197
|
+
setting("tools.replaceGrep", "Replace grep", config.tools.replaceGrep ? "true" : "false", {
|
|
198
|
+
description:
|
|
199
|
+
"Register a grep-compatible tool backed by fovea_focus instead of literal text search. Default on; changing it reloads extensions so Pi and Fabric see the new tool slot.",
|
|
200
|
+
values: BOOLEANS,
|
|
201
|
+
}),
|
|
197
202
|
setting("tools.defaultBudget", "Default tool budget", String(config.tools.defaultBudget), {
|
|
198
203
|
description: "Token budget applied when a fovea_* tool call omits maxTokens.",
|
|
199
204
|
submenu: numericSubmenu(theme, BUDGETS, "Default tool budget", "Fallback maxTokens for fovea tools."),
|
|
@@ -206,13 +211,17 @@ export interface FoveaSettingsDeps {
|
|
|
206
211
|
onConfigApplied?: () => void;
|
|
207
212
|
}
|
|
208
213
|
|
|
214
|
+
export interface FoveaSettingsResult {
|
|
215
|
+
grepRegistrationChanged: boolean;
|
|
216
|
+
}
|
|
217
|
+
|
|
209
218
|
export const openFoveaSettings = async (
|
|
210
219
|
context: ExtensionContext,
|
|
211
220
|
deps: FoveaSettingsDeps = {},
|
|
212
|
-
): Promise<
|
|
221
|
+
): Promise<FoveaSettingsResult> => {
|
|
213
222
|
if (context.mode !== "tui") {
|
|
214
223
|
context.ui.notify("Fovea settings are available in TUI mode", "warning");
|
|
215
|
-
return;
|
|
224
|
+
return { grepRegistrationChanged: false };
|
|
216
225
|
}
|
|
217
226
|
const agentDir = getAgentDir();
|
|
218
227
|
const scopes = {
|
|
@@ -221,6 +230,7 @@ export const openFoveaSettings = async (
|
|
|
221
230
|
projectTrusted: context.isProjectTrusted(),
|
|
222
231
|
};
|
|
223
232
|
let config = loadFoveaConfig(scopes);
|
|
233
|
+
const initialReplaceGrep = config.tools.replaceGrep;
|
|
224
234
|
let dirty = false;
|
|
225
235
|
|
|
226
236
|
const apply = (id: string, value: unknown): void => {
|
|
@@ -249,6 +259,7 @@ export const openFoveaSettings = async (
|
|
|
249
259
|
});
|
|
250
260
|
|
|
251
261
|
if (dirty) context.ui.notify("Fovea settings saved.", "info");
|
|
262
|
+
return { grepRegistrationChanged: config.tools.replaceGrep !== initialReplaceGrep };
|
|
252
263
|
};
|
|
253
264
|
|
|
254
265
|
// Local in-place merge so subsequent edits in the same overlay start from the
|