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 CHANGED
@@ -172,7 +172,7 @@ details.
172
172
 
173
173
  ## Turn sync
174
174
 
175
- Continuous sync is on by default. Before an agent starts, Fovea establishes its
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
- Turn sync off per repo or globally through settings, or with:
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.enabled` | `true` | pre-agent and post-turn continuous 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.0",
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.0",
56
- "@earendil-works/pi-tui": "0.84.0",
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",
@@ -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
- /** turn_end feedback loop on/off (the default-on, opt-out knob). */
13
- enabled: boolean;
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
- enabled: true,
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 grepModeValue = (value: unknown, fallback: GrepMode): GrepMode =>
101
- typeof value === "string" && (GREP_MODES as readonly string[]).includes(value)
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: boolValue(sync.enabled, base.sync.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: grepModeValue(
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, enabled: false } };
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.sync.enabled) return;
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.sync.enabled) return;
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: true,
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.sync.enabled) return;
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: true,
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.enabled ? "continuous" : "off"} · grep ${cfg.tools.grepMode} · ` +
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
  );
@@ -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.enabled", "Continuous sync", config.sync.enabled ? "true" : "false", {
239
+ setting("sync.mode", "Continuous sync", config.sync.mode, {
239
240
  description:
240
- "Check semantic drift before agent start and after every turn; inject or steer before the model continues. FOVEA_TURN_SYNC=off overrides.",
241
- values: BOOLEANS,
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.",