pi-fovea 0.13.0 → 0.13.2
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 +7 -4
- package/package.json +3 -3
- package/src/core/config.ts +17 -10
- package/src/index.ts +8 -6
- package/src/ui/settings.ts +4 -3
package/README.md
CHANGED
|
@@ -172,7 +172,7 @@ details.
|
|
|
172
172
|
|
|
173
173
|
## Turn sync
|
|
174
174
|
|
|
175
|
-
Continuous sync is
|
|
175
|
+
Continuous sync is enabled and visible by default. Before an agent starts, Fovea establishes its
|
|
176
176
|
baseline or injects any outside drift ahead of the first model call. After
|
|
177
177
|
every assistant turn it compares symbols, calls, imports, literals, and anchors
|
|
178
178
|
again. Content hashes keep the unchanged fast path cheap. Edits that touch only
|
|
@@ -186,7 +186,9 @@ It lists the causal channels behind each link: calls, imports, shared literals,
|
|
|
186
186
|
tests, or co-change history. By default it also embeds the refreshed focus
|
|
187
187
|
context of the top drift target (push). With `sync.pushFocus` off, the update
|
|
188
188
|
ends with a suggested focus probe for the next call (pull). Clean turns stay
|
|
189
|
-
silent. Enable `sync.ackClean` if you want an ack for those.
|
|
189
|
+
silent. Enable `sync.ackClean` if you want an ack for those. Set `sync.mode` to
|
|
190
|
+
`"hidden"` to keep red sync context working behind the scenes without rendering it
|
|
191
|
+
in the transcript, or to `"disabled"` to turn continuous sync off.
|
|
190
192
|
|
|
191
193
|
Runtime controls:
|
|
192
194
|
|
|
@@ -195,7 +197,8 @@ Runtime controls:
|
|
|
195
197
|
- `/fovea reload`: hot-reload extensions and activate newly installed source
|
|
196
198
|
- `/fovea settings`: configure sync, budgets, and hybrid grep
|
|
197
199
|
|
|
198
|
-
|
|
200
|
+
Choose enabled, hidden, or disabled per repo or globally through settings. The
|
|
201
|
+
environment override still turns sync off with:
|
|
199
202
|
|
|
200
203
|
```sh
|
|
201
204
|
FOVEA_TURN_SYNC=off pi
|
|
@@ -209,7 +212,7 @@ with `fabric.json`.
|
|
|
209
212
|
|
|
210
213
|
| Key | Default | Meaning |
|
|
211
214
|
| --- | :-----: | ------- |
|
|
212
|
-
| `sync.
|
|
215
|
+
| `sync.mode` | `"enabled"` | `"enabled"` shows model-visible sync messages, `"hidden"` keeps them model-visible but out of the transcript, and `"disabled"` turns sync off. Legacy `sync.enabled` booleans still parse. |
|
|
213
216
|
| `sync.budget` | `512` | token cap for proactive steering context |
|
|
214
217
|
| `sync.ackClean` | `false` | toast after clean structural turns |
|
|
215
218
|
| `sync.steerThreshold` | `0.15` | total surprise (channel-weighted heat above the session sync memory) that justifies proactive model steering |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-fovea",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "Token-budgeted repo mapping for agent sessions: foveated heat diffusion over a cross-language code graph, with progressive disclosure.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"fovea": "dist/cli.mjs"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
56
|
-
"@earendil-works/pi-tui": "0.84.
|
|
55
|
+
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
56
|
+
"@earendil-works/pi-tui": "0.84.1",
|
|
57
57
|
"@types/node": "^22.0.0",
|
|
58
58
|
"esbuild": "^0.28.1",
|
|
59
59
|
"tsx": "^4.19.0",
|
package/src/core/config.ts
CHANGED
|
@@ -8,9 +8,12 @@ import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
|
8
8
|
import fs from "node:fs";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
|
|
11
|
+
export const SYNC_MODES = ["enabled", "hidden", "disabled"] as const;
|
|
12
|
+
export type SyncMode = (typeof SYNC_MODES)[number];
|
|
13
|
+
|
|
11
14
|
interface FoveaSyncConfig {
|
|
12
|
-
/**
|
|
13
|
-
|
|
15
|
+
/** Continuous sync delivery: visible, model-only, or fully disabled. */
|
|
16
|
+
mode: SyncMode;
|
|
14
17
|
/** Token budget for proactive model steering context. */
|
|
15
18
|
budget: number;
|
|
16
19
|
/** Also send a tiny model-visible ack on clean turns (default false: silent green). */
|
|
@@ -47,7 +50,7 @@ export interface FoveaConfig {
|
|
|
47
50
|
|
|
48
51
|
export const DEFAULT_FOVEA_CONFIG: FoveaConfig = {
|
|
49
52
|
sync: {
|
|
50
|
-
|
|
53
|
+
mode: "enabled",
|
|
51
54
|
budget: 512,
|
|
52
55
|
ackClean: false,
|
|
53
56
|
// Masses are heat units leaked outside the changed files (1 unit seeded
|
|
@@ -97,10 +100,8 @@ const clamp = (id: string, value: number): number => {
|
|
|
97
100
|
const boolValue = (value: unknown, fallback: boolean): boolean =>
|
|
98
101
|
typeof value === "boolean" ? value : fallback;
|
|
99
102
|
|
|
100
|
-
const
|
|
101
|
-
typeof value === "string" && (
|
|
102
|
-
? (value as GrepMode)
|
|
103
|
-
: fallback;
|
|
103
|
+
const enumValue = <T extends string>(value: unknown, values: readonly T[], fallback: T): T =>
|
|
104
|
+
typeof value === "string" && values.includes(value as T) ? value as T : fallback;
|
|
104
105
|
|
|
105
106
|
const intValue = (id: string, value: unknown, fallback: number): number =>
|
|
106
107
|
clamp(id, typeof value === "number" && Number.isFinite(value) ? value : fallback);
|
|
@@ -118,7 +119,12 @@ const applyPartial = (base: FoveaConfig, partial: unknown): FoveaConfig => {
|
|
|
118
119
|
const tools = (typeof src.tools === "object" && src.tools !== null ? src.tools : {}) as Record<string, unknown>;
|
|
119
120
|
return {
|
|
120
121
|
sync: {
|
|
121
|
-
enabled
|
|
122
|
+
// Legacy `enabled` booleans map to visible/disabled. An explicit mode wins.
|
|
123
|
+
mode: enumValue(
|
|
124
|
+
sync.mode,
|
|
125
|
+
SYNC_MODES,
|
|
126
|
+
typeof sync.enabled === "boolean" ? (sync.enabled ? "enabled" : "disabled") : base.sync.mode,
|
|
127
|
+
),
|
|
122
128
|
budget: intValue("sync.budget", sync.budget, base.sync.budget),
|
|
123
129
|
ackClean: boolValue(sync.ackClean, base.sync.ackClean),
|
|
124
130
|
steerThreshold: floatValue("sync.steerThreshold", sync.steerThreshold, base.sync.steerThreshold),
|
|
@@ -128,8 +134,9 @@ const applyPartial = (base: FoveaConfig, partial: unknown): FoveaConfig => {
|
|
|
128
134
|
defaultBudget: intValue("tools.defaultBudget", tools.defaultBudget, base.tools.defaultBudget),
|
|
129
135
|
// Legacy `replaceGrep` (v0.10): true -> "replace", false -> "off". An
|
|
130
136
|
// explicit `grepMode` always wins over the legacy key.
|
|
131
|
-
grepMode:
|
|
137
|
+
grepMode: enumValue(
|
|
132
138
|
tools.grepMode,
|
|
139
|
+
GREP_MODES,
|
|
133
140
|
typeof tools.replaceGrep === "boolean" ? (tools.replaceGrep ? "replace" : "off") : base.tools.grepMode,
|
|
134
141
|
),
|
|
135
142
|
grepAugmentBudget: intValue("tools.grepAugmentBudget", tools.grepAugmentBudget, base.tools.grepAugmentBudget),
|
|
@@ -155,7 +162,7 @@ export const loadFoveaConfig = (scopes: FoveaConfigScopes): FoveaConfig => {
|
|
|
155
162
|
}
|
|
156
163
|
// Environment override mirrors pi-fabric's PI_* precedence over stored values.
|
|
157
164
|
const off = process.env.FOVEA_TURN_SYNC;
|
|
158
|
-
if (off === "off" || off === "0" || off === "false") config = { ...config, sync: { ...config.sync,
|
|
165
|
+
if (off === "off" || off === "0" || off === "false") config = { ...config, sync: { ...config.sync, mode: "disabled" } };
|
|
159
166
|
return config;
|
|
160
167
|
};
|
|
161
168
|
|
package/src/index.ts
CHANGED
|
@@ -68,6 +68,8 @@ const requestsNativeGrep = (params: {
|
|
|
68
68
|
!ROUTE_PATH.test(params.pattern.trim()));
|
|
69
69
|
|
|
70
70
|
const text = (s: string) => ({ type: "text" as const, text: s });
|
|
71
|
+
const syncRuns = (config: FoveaConfig): boolean => config.sync.mode !== "disabled";
|
|
72
|
+
const syncDisplays = (config: FoveaConfig): boolean => config.sync.mode === "enabled";
|
|
71
73
|
|
|
72
74
|
const NODE_KINDS = new Set<NodeKind>([
|
|
73
75
|
"function", "method", "class", "interface", "type", "field", "decl", "file", "anchor",
|
|
@@ -264,7 +266,7 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
264
266
|
const rels = turnFiles
|
|
265
267
|
.map((p) => (p.startsWith(root + "/") ? p.slice(root.length + 1) : p))
|
|
266
268
|
.filter((p) => !p.startsWith("/"));
|
|
267
|
-
if (!rels.length || !cfg
|
|
269
|
+
if (!rels.length || !syncRuns(cfg)) return;
|
|
268
270
|
const files = [...rels];
|
|
269
271
|
const existing = warmTimers.get(root);
|
|
270
272
|
if (existing) clearTimeout(existing);
|
|
@@ -299,7 +301,7 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
299
301
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
300
302
|
try {
|
|
301
303
|
const cfg = configFor(ctx.cwd, ctx.isProjectTrusted());
|
|
302
|
-
if (!cfg
|
|
304
|
+
if (!syncRuns(cfg)) return;
|
|
303
305
|
const outcome = await sync(
|
|
304
306
|
ctx.cwd,
|
|
305
307
|
{ files: [], budget: cfg.sync.budget, steerThreshold: cfg.sync.steerThreshold, pushFocus: cfg.sync.pushFocus },
|
|
@@ -315,7 +317,7 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
315
317
|
message: {
|
|
316
318
|
customType: "pi-fovea-sync",
|
|
317
319
|
content: outcome.text,
|
|
318
|
-
display:
|
|
320
|
+
display: syncDisplays(cfg),
|
|
319
321
|
details: outcome.details,
|
|
320
322
|
},
|
|
321
323
|
};
|
|
@@ -337,7 +339,7 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
337
339
|
.map((p) => (p.startsWith(ctx.cwd + "/") ? p.slice(ctx.cwd.length + 1) : p))
|
|
338
340
|
.filter((p) => !p.startsWith("/"));
|
|
339
341
|
turnFiles = [];
|
|
340
|
-
if (!cfg
|
|
342
|
+
if (!syncRuns(cfg)) return;
|
|
341
343
|
const outcome = await sync(
|
|
342
344
|
ctx.cwd,
|
|
343
345
|
{ files: rels, budget: cfg.sync.budget, steerThreshold: cfg.sync.steerThreshold, pushFocus: cfg.sync.pushFocus },
|
|
@@ -352,7 +354,7 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
352
354
|
pi.sendMessage({
|
|
353
355
|
customType: "pi-fovea-sync",
|
|
354
356
|
content: outcome.text,
|
|
355
|
-
display:
|
|
357
|
+
display: syncDisplays(cfg),
|
|
356
358
|
details: outcome.details,
|
|
357
359
|
}, { deliverAs: "steer", triggerTurn: true });
|
|
358
360
|
} else if (!outcome.details.baseline && cfg.sync.ackClean && ctx.hasUI) {
|
|
@@ -549,7 +551,7 @@ export default function fovea(pi: ExtensionAPI) {
|
|
|
549
551
|
`${failedCount ? ` · !${failedCount} files failed extraction` : ""}` +
|
|
550
552
|
`${unreadableCount ? ` · !${unreadableCount} files unreadable` : ""}` +
|
|
551
553
|
`${oversizedCount ? ` · !${oversizedCount} files over size cap` : ""} · ` +
|
|
552
|
-
`sync ${cfg.sync.
|
|
554
|
+
`sync ${cfg.sync.mode} · grep ${cfg.tools.grepMode} · ` +
|
|
553
555
|
`${astGrep.code === 0 ? astGrep.stdout.trim() : "ast-grep unavailable"}`,
|
|
554
556
|
"info",
|
|
555
557
|
);
|
package/src/ui/settings.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import {
|
|
30
30
|
buildPartialFromId,
|
|
31
31
|
loadFoveaConfig,
|
|
32
|
+
SYNC_MODES,
|
|
32
33
|
saveFoveaConfig,
|
|
33
34
|
type FoveaConfig,
|
|
34
35
|
type FoveaConfigScope,
|
|
@@ -235,10 +236,10 @@ const buildItems = (
|
|
|
235
236
|
theme: Theme,
|
|
236
237
|
config: FoveaConfig,
|
|
237
238
|
): SettingItem[] => [
|
|
238
|
-
setting("sync.
|
|
239
|
+
setting("sync.mode", "Continuous sync", config.sync.mode, {
|
|
239
240
|
description:
|
|
240
|
-
"
|
|
241
|
-
values:
|
|
241
|
+
"enabled shows sync messages; hidden delivers them to the model without rendering them in the transcript; disabled turns sync off. FOVEA_TURN_SYNC=off overrides.",
|
|
242
|
+
values: SYNC_MODES,
|
|
242
243
|
}),
|
|
243
244
|
setting("sync.budget", "Sync budget", String(config.sync.budget), {
|
|
244
245
|
description: "Max tokens for proactive repository steering sent to the model.",
|