pi-fovea 0.13.2 → 0.13.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 CHANGED
@@ -208,7 +208,10 @@ FOVEA_TURN_SYNC=off pi
208
208
 
209
209
  Global settings live in `~/.pi/agent/fovea.json`. A trusted repo-level override
210
210
  sits in `<repo>/.pi/fovea.json`. These are the same two scopes pi-fabric uses
211
- with `fabric.json`.
211
+ with `fabric.json`. In `/fovea settings`, the configured external-editor key
212
+ (`Ctrl+G` by default) switches both the displayed values and save destination
213
+ between project overrides and global defaults. A project override can remain
214
+ effective while its global default is being edited.
212
215
 
213
216
  | Key | Default | Meaning |
214
217
  | --- | :-----: | ------- |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fovea",
3
- "version": "0.13.2",
3
+ "version": "0.13.3",
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",
@@ -45,6 +45,6 @@ The same engine runs headlessly as the `fovea` binary (repo root scan, plus JSON
45
45
 
46
46
  ## Settings
47
47
 
48
- Use `/fovea status` for loaded version and index coverage, `/fovea reset` for fresh state, `/fovea reload` after updates, and `/fovea settings` for configuration. Files live under `~/.pi/agent/fovea.json` or trusted `.pi/fovea.json`. `tools.grepMode` controls the grep integration: `"augment"` (default) keeps native grep semantics and appends a Fovea graph section to symbol-query results — including `pi.grep` calls inside fabric_exec — `"replace"` keeps the legacy bare-query takeover, `"off"` is native only.
48
+ Use `/fovea status` for loaded version and index coverage, `/fovea reset` for fresh state, `/fovea reload` after updates, and `/fovea settings` for configuration. Files live under `~/.pi/agent/fovea.json` or trusted `.pi/fovea.json`; the external-editor key (`Ctrl+G` by default) switches the displayed and saved layer between global defaults and project overrides. `tools.grepMode` controls the grep integration: `"augment"` (default) keeps native grep semantics and appends a Fovea graph section to symbol-query results — including `pi.grep` calls inside fabric_exec — `"replace"` keeps the legacy bare-query takeover, `"off"` is native only.
49
49
 
50
50
  Budget overflow is not a dead end: any `… more results collapsed or outside budget` footer names a tmp artifact (`/tmp/pi-fovea-<op>-<hash>.txt`) holding the FULL list — read or grep that file for the remainder. Reach for `fovea_dwell` when you want a wider neighborhood, not just more of the same list.
@@ -155,9 +155,12 @@ const readConfigFile = (file: string): Record<string, unknown> => {
155
155
  }
156
156
  };
157
157
 
158
- export const loadFoveaConfig = (scopes: FoveaConfigScopes): FoveaConfig => {
158
+ const resolveFoveaConfig = (
159
+ scopes: FoveaConfigScopes,
160
+ includeProject: boolean,
161
+ ): FoveaConfig => {
159
162
  let config = applyPartial(DEFAULT_FOVEA_CONFIG, readConfigFile(globalFoveaConfigPath(scopes.agentDir)));
160
- if (scopes.projectTrusted) {
163
+ if (includeProject) {
161
164
  config = applyPartial(config, readConfigFile(projectFoveaConfigPath(scopes.cwd)));
162
165
  }
163
166
  // Environment override mirrors pi-fabric's PI_* precedence over stored values.
@@ -166,6 +169,19 @@ export const loadFoveaConfig = (scopes: FoveaConfigScopes): FoveaConfig => {
166
169
  return config;
167
170
  };
168
171
 
172
+ export const loadFoveaConfigForScope = (
173
+ scopes: FoveaConfigScopes,
174
+ scope: FoveaConfigScope,
175
+ ): FoveaConfig => {
176
+ if (scope === "project" && !scopes.projectTrusted) {
177
+ throw new Error("Cannot load project Fovea configuration for an untrusted project");
178
+ }
179
+ return resolveFoveaConfig(scopes, scope === "project");
180
+ };
181
+
182
+ export const loadFoveaConfig = (scopes: FoveaConfigScopes): FoveaConfig =>
183
+ resolveFoveaConfig(scopes, scopes.projectTrusted);
184
+
169
185
  const mergeDeep = (existing: Record<string, unknown>, partial: Record<string, unknown>): Record<string, unknown> => {
170
186
  const out = { ...existing };
171
187
  for (const [key, value] of Object.entries(partial)) {
@@ -3,7 +3,7 @@
3
3
  // SelectList submenus, dotted-id coercion) so both extensions feel like one
4
4
  // settings surface. Persistence: fovea.json under the pi agent dir, with a
5
5
  // project override beneath pi's configured project resource directory; the
6
- // external-editor keybinding toggles which scope the next change saves to.
6
+ // external-editor keybinding switches the configuration layer being viewed and saved.
7
7
 
8
8
  import {
9
9
  CONFIG_DIR_NAME,
@@ -29,6 +29,7 @@ import {
29
29
  import {
30
30
  buildPartialFromId,
31
31
  loadFoveaConfig,
32
+ loadFoveaConfigForScope,
32
33
  SYNC_MODES,
33
34
  saveFoveaConfig,
34
35
  type FoveaConfig,
@@ -133,15 +134,20 @@ export interface FoveaSettingsComponentOptions {
133
134
  initialSaveScope?: FoveaConfigScope;
134
135
  projectScopeAvailable?: boolean;
135
136
  onSaveScopeChange?: (scope: FoveaConfigScope) => void;
137
+ itemsForSaveScope?: (scope: FoveaConfigScope) => SettingItem[];
136
138
  }
137
139
 
138
140
  export class FoveaSettingsComponent extends Container {
139
- readonly settingsList: SettingsList;
141
+ settingsList: SettingsList;
140
142
  private readonly theme: Theme;
141
143
  private readonly keybindings: Pick<KeybindingsManager, "matches" | "getKeys"> | undefined;
142
144
  private readonly saveScopeText: Text;
145
+ private readonly settingsListContainer: Container;
143
146
  private readonly projectScopeAvailable: boolean;
147
+ private readonly onChange: (id: string, newValue: string) => void;
148
+ private readonly onCancel: () => void;
144
149
  private readonly onSaveScopeChange: (scope: FoveaConfigScope) => void;
150
+ private readonly itemsForSaveScope: ((scope: FoveaConfigScope) => SettingItem[]) | undefined;
145
151
  private saveScope: FoveaConfigScope;
146
152
 
147
153
  constructor(
@@ -158,16 +164,19 @@ export class FoveaSettingsComponent extends Container {
158
164
  this.saveScope = options.initialSaveScope === "global" || !this.projectScopeAvailable
159
165
  ? "global"
160
166
  : "project";
167
+ this.onChange = onChange;
168
+ this.onCancel = onCancel;
161
169
  this.onSaveScopeChange = options.onSaveScopeChange ?? (() => {});
170
+ this.itemsForSaveScope = options.itemsForSaveScope;
162
171
  this.addChild(new DynamicBorder((text) => theme.fg("border", text)));
163
172
  this.saveScopeText = new Text("", 1, 0);
164
173
  this.updateSaveScopeText();
165
174
  this.addChild(this.saveScopeText);
166
175
  this.addChild(new Spacer(1));
167
- this.settingsList = new SettingsList(items, 10, settingsListTheme(theme), onChange, onCancel, {
168
- enableSearch: true,
169
- });
170
- this.addChild(this.settingsList);
176
+ this.settingsListContainer = new Container();
177
+ this.settingsList = this.createSettingsList(items);
178
+ this.settingsListContainer.addChild(this.settingsList);
179
+ this.addChild(this.settingsListContainer);
171
180
  this.addChild(new DynamicBorder((text) => theme.fg("border", text)));
172
181
  }
173
182
 
@@ -177,6 +186,12 @@ export class FoveaSettingsComponent extends Container {
177
186
  this.saveScope = this.saveScope === "project" ? "global" : "project";
178
187
  this.updateSaveScopeText();
179
188
  this.onSaveScopeChange(this.saveScope);
189
+ const nextItems = this.itemsForSaveScope?.(this.saveScope);
190
+ if (nextItems) {
191
+ this.settingsListContainer.clear();
192
+ this.settingsList = this.createSettingsList(nextItems);
193
+ this.settingsListContainer.addChild(this.settingsList);
194
+ }
180
195
  return;
181
196
  }
182
197
  this.settingsList.handleInput(data);
@@ -187,17 +202,32 @@ export class FoveaSettingsComponent extends Container {
187
202
  this.updateSaveScopeText();
188
203
  }
189
204
 
205
+ private createSettingsList(items: SettingItem[]): SettingsList {
206
+ return new SettingsList(
207
+ items,
208
+ 10,
209
+ settingsListTheme(this.theme),
210
+ this.onChange,
211
+ this.onCancel,
212
+ { enableSearch: true },
213
+ );
214
+ }
215
+
190
216
  private updateSaveScopeText(): void {
191
217
  const destination = this.saveScope === "project"
192
- ? `Project (${CONFIG_DIR_NAME}/fovea.json)`
193
- : `Global (~/${CONFIG_DIR_NAME}/agent/fovea.json)`;
218
+ ? `Project overrides (${CONFIG_DIR_NAME}/fovea.json)`
219
+ : `Global defaults (~/${CONFIG_DIR_NAME}/agent/fovea.json)`;
194
220
  const keys = this.keybindings?.getKeys("app.editor.external") ?? [];
195
221
  const shortcut = keys.map(formatKey).join("/");
196
- const hint = this.projectScopeAvailable
197
- ? shortcut ? ` · ${shortcut} toggles save scope` : " · scope toggle key unavailable"
198
- : " · project scope unavailable for untrusted projects";
222
+ const hint = !this.projectScopeAvailable
223
+ ? " · project scope unavailable for untrusted projects"
224
+ : !shortcut
225
+ ? " · scope switch key unavailable"
226
+ : this.saveScope === "global"
227
+ ? ` · ${shortcut} switches scope · project overrides may remain active here`
228
+ : ` · ${shortcut} switches scope`;
199
229
  this.saveScopeText.setText(
200
- this.theme.fg("muted", "Save scope: ") +
230
+ this.theme.fg("muted", "Editing: ") +
201
231
  this.theme.fg("accent", destination) +
202
232
  this.theme.fg("dim", hint),
203
233
  );
@@ -299,7 +329,7 @@ export const openFoveaSettings = async (
299
329
  projectTrusted: context.isProjectTrusted(),
300
330
  };
301
331
  let saveScope: FoveaConfigScope = scopes.projectTrusted ? "project" : "global";
302
- let config = loadFoveaConfig(scopes);
332
+ let config = loadFoveaConfigForScope(scopes, saveScope);
303
333
  const initialGrepRegistration = config.tools.grepMode === "replace";
304
334
  let dirty = false;
305
335
 
@@ -313,16 +343,19 @@ export const openFoveaSettings = async (
313
343
  );
314
344
  return;
315
345
  }
316
- config = applyPartialLocal(config, id, value);
346
+ config = loadFoveaConfigForScope(scopes, saveScope);
317
347
  dirty = true;
318
348
  deps.onConfigApplied?.();
319
349
  };
320
350
 
321
351
  await context.ui.custom<void>((tui, theme, keybindings, done) => {
322
- const items = buildItems(theme, config);
352
+ const itemsForScope = (scope: FoveaConfigScope): SettingItem[] => {
353
+ config = loadFoveaConfigForScope(scopes, scope);
354
+ return buildItems(theme, config);
355
+ };
323
356
  return new FoveaSettingsComponent(
324
357
  theme,
325
- items,
358
+ itemsForScope(saveScope),
326
359
  (id, newValue) => apply(id, coerceValue(id, newValue, config)),
327
360
  () => done(),
328
361
  {
@@ -333,27 +366,16 @@ export const openFoveaSettings = async (
333
366
  saveScope = scope;
334
367
  tui.requestRender();
335
368
  },
369
+ itemsForSaveScope: itemsForScope,
336
370
  },
337
371
  );
338
372
  });
339
373
 
340
374
  if (dirty) context.ui.notify("Fovea settings saved.", "info");
341
- return { grepRegistrationChanged: (config.tools.grepMode === "replace") !== initialGrepRegistration };
342
- };
343
-
344
- // Local in-place merge so subsequent edits in the same overlay start from the
345
- // already-saved value (config is reloaded from disk on open).
346
- const applyPartialLocal = (config: FoveaConfig, id: string, value: unknown): FoveaConfig => {
347
- const next: FoveaConfig = {
348
- sync: { ...config.sync },
349
- tools: { ...config.tools },
375
+ const effectiveConfig = loadFoveaConfig(scopes);
376
+ return {
377
+ grepRegistrationChanged:
378
+ (effectiveConfig.tools.grepMode === "replace") !== initialGrepRegistration,
350
379
  };
351
- const segments = id.split(".");
352
- let target: Record<string, unknown> = next as unknown as Record<string, unknown>;
353
- for (let i = 0; i < segments.length - 1; i++) {
354
- target = target[segments[i]!] as Record<string, unknown>;
355
- }
356
- target[segments[segments.length - 1]!] = value;
357
- return next;
358
380
  };
359
381