pi-fovea 0.13.2 → 0.13.4
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 +8 -3
- package/dist/cli.mjs +10 -0
- package/package.json +1 -1
- package/skills/pi-fovea/SKILL.md +1 -1
- package/src/core/config.ts +18 -2
- package/src/core/git.ts +12 -0
- package/src/core/ops.ts +17 -1
- package/src/core/sync.ts +16 -0
- package/src/ui/settings.ts +54 -32
package/README.md
CHANGED
|
@@ -185,8 +185,10 @@ update names the changed files, the route deltas, and the newly relevant files.
|
|
|
185
185
|
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
|
-
ends with a suggested focus probe for the next call (pull).
|
|
189
|
-
|
|
188
|
+
ends with a suggested focus probe for the next call (pull). Switching
|
|
189
|
+
branches re-baselines silently instead of steering: a `git checkout`
|
|
190
|
+
re-materializes the worktree, but the branch diff is not authored drift —
|
|
191
|
+
commits, pulls, and rebases still report. Clean turns stay silent. Enable `sync.ackClean` if you want an ack for those. Set `sync.mode` to
|
|
190
192
|
`"hidden"` to keep red sync context working behind the scenes without rendering it
|
|
191
193
|
in the transcript, or to `"disabled"` to turn continuous sync off.
|
|
192
194
|
|
|
@@ -208,7 +210,10 @@ FOVEA_TURN_SYNC=off pi
|
|
|
208
210
|
|
|
209
211
|
Global settings live in `~/.pi/agent/fovea.json`. A trusted repo-level override
|
|
210
212
|
sits in `<repo>/.pi/fovea.json`. These are the same two scopes pi-fabric uses
|
|
211
|
-
with `fabric.json`.
|
|
213
|
+
with `fabric.json`. In `/fovea settings`, the configured external-editor key
|
|
214
|
+
(`Ctrl+G` by default) switches both the displayed values and save destination
|
|
215
|
+
between project overrides and global defaults. A project override can remain
|
|
216
|
+
effective while its global default is being edited.
|
|
212
217
|
|
|
213
218
|
| Key | Default | Meaning |
|
|
214
219
|
| --- | :-----: | ------- |
|
package/dist/cli.mjs
CHANGED
|
@@ -306,6 +306,11 @@ var gitHead = async (root) => {
|
|
|
306
306
|
const head = out?.trim();
|
|
307
307
|
return head ? head : void 0;
|
|
308
308
|
};
|
|
309
|
+
var gitReflogAction = async (root) => {
|
|
310
|
+
const out = await gitOut(root, ["reflog", "-1", "--format=%gs"]);
|
|
311
|
+
const line = out?.trim();
|
|
312
|
+
return line ? line : void 0;
|
|
313
|
+
};
|
|
309
314
|
var gitPrefixes = /* @__PURE__ */ new Map();
|
|
310
315
|
var gitPrefix = async (root) => {
|
|
311
316
|
if (gitPrefixes.has(root)) {
|
|
@@ -2926,6 +2931,7 @@ var refreshState = async (state, hints = [], force = false) => {
|
|
|
2926
2931
|
let files = state.files;
|
|
2927
2932
|
const changed = [];
|
|
2928
2933
|
const deleted = [];
|
|
2934
|
+
let checkout = false;
|
|
2929
2935
|
const hinted = [.../* @__PURE__ */ new Set([...filterSupported(hints, routeRes), ...hints.filter((h) => store.facts.has(h))])];
|
|
2930
2936
|
changed.push(...hinted);
|
|
2931
2937
|
if (state.gitKind === "git") {
|
|
@@ -2933,6 +2939,9 @@ var refreshState = async (state, hints = [], force = false) => {
|
|
|
2933
2939
|
if (probe) {
|
|
2934
2940
|
const headMoved = probe.head !== state.head;
|
|
2935
2941
|
state.head = probe.head;
|
|
2942
|
+
if (headMoved) {
|
|
2943
|
+
checkout = (await gitReflogAction(state.root))?.startsWith("checkout:") ?? false;
|
|
2944
|
+
}
|
|
2936
2945
|
const needsList = probe.relist || probe.changes.some((c) => c.path.endsWith("/"));
|
|
2937
2946
|
if (needsList) {
|
|
2938
2947
|
files = await listFiles(state.root, routeRes);
|
|
@@ -2991,6 +3000,7 @@ var refreshState = async (state, hints = [], force = false) => {
|
|
|
2991
3000
|
return state;
|
|
2992
3001
|
}
|
|
2993
3002
|
const fresh = await assembleState(state.root, files, store, report, state.gitKind, state.head, state.dirty);
|
|
3003
|
+
if (checkout) fresh.checkout = true;
|
|
2994
3004
|
states.set(state.root, fresh);
|
|
2995
3005
|
return fresh;
|
|
2996
3006
|
};
|
package/package.json
CHANGED
package/skills/pi-fovea/SKILL.md
CHANGED
|
@@ -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
|
|
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.
|
package/src/core/config.ts
CHANGED
|
@@ -155,9 +155,12 @@ const readConfigFile = (file: string): Record<string, unknown> => {
|
|
|
155
155
|
}
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
-
|
|
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 (
|
|
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)) {
|
package/src/core/git.ts
CHANGED
|
@@ -41,6 +41,18 @@ export const gitHead = async (root: string): Promise<string | undefined> => {
|
|
|
41
41
|
return head ? head : undefined;
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Subject of the most recent HEAD reflog entry, e.g. "checkout: moving from
|
|
46
|
+
* main to feature". Tells branch switches (re-baseline quietly) apart from
|
|
47
|
+
* commits/pulls/rebases (foreign drift worth reporting). Undefined when the
|
|
48
|
+
* reflog is unavailable or disabled — callers fall back to the loud path.
|
|
49
|
+
*/
|
|
50
|
+
export const gitReflogAction = async (root: string): Promise<string | undefined> => {
|
|
51
|
+
const out = await gitOut(root, ["reflog", "-1", "--format=%gs"]);
|
|
52
|
+
const line = out?.trim();
|
|
53
|
+
return line ? line : undefined;
|
|
54
|
+
};
|
|
55
|
+
|
|
44
56
|
export interface WorktreeChange {
|
|
45
57
|
/** X+Y status columns, e.g. " M", "??", "D ". */
|
|
46
58
|
code: string;
|
package/src/core/ops.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
type FactStore,
|
|
20
20
|
type FileFacts,
|
|
21
21
|
} from "./build.js";
|
|
22
|
-
import { gitProbe, prFiles, uncommittedFiles } from "./git.js";
|
|
22
|
+
import { gitProbe, gitReflogAction, prFiles, uncommittedFiles } from "./git.js";
|
|
23
23
|
import { ROOT_CACHE_LIMIT, envInt, yieldToLoop } from "./asyncutil.js";
|
|
24
24
|
import { loadRepoRules } from "./anchors.js";
|
|
25
25
|
import { buildCsr, chebyshevVectors, chooseOrder, heatField, type Csr } from "./heat.js";
|
|
@@ -62,6 +62,12 @@ export interface RepoState {
|
|
|
62
62
|
* porcelain-clean with unmoved HEAD would otherwise keep serving its dirty
|
|
63
63
|
* facts until the next edit. */
|
|
64
64
|
dirty: Set<string>;
|
|
65
|
+
/** Set when this generation materialized from a `git checkout` (HEAD moved
|
|
66
|
+
* with reflog action "checkout:…"): sync re-baselines silently instead of
|
|
67
|
+
* cascading over the branch diff. Lives exactly one generation — the next
|
|
68
|
+
* fact-moving refresh builds a fresh state without it, so the quiet path
|
|
69
|
+
* cannot hide authored drift. */
|
|
70
|
+
checkout?: boolean;
|
|
65
71
|
/** Past joint-edit affinity (raw conductance + last joint commit). History
|
|
66
72
|
* is NOT structure: impact re-seeds these partners at recency-decayed
|
|
67
73
|
* strength whenever a change lands, so old co-work cools like any heat. */
|
|
@@ -194,6 +200,12 @@ const refreshState = async (state: RepoState, hints: string[] = [], force = fals
|
|
|
194
200
|
let files = state.files;
|
|
195
201
|
const changed: string[] = [];
|
|
196
202
|
const deleted: string[] = [];
|
|
203
|
+
// A checkout re-materializes the worktree from another ref; flag the
|
|
204
|
+
// rebuilt generation so sync re-baselines quietly. Only a HEAD move whose
|
|
205
|
+
// latest reflog action is "checkout:…" qualifies — pulls/rebases merge
|
|
206
|
+
// foreign work and keep the loud drift path, and reflog-less repos stay
|
|
207
|
+
// conservative (undefined -> false).
|
|
208
|
+
let checkout = false;
|
|
197
209
|
const hinted = [...new Set([...filterSupported(hints, routeRes), ...hints.filter((h) => store.facts.has(h))])];
|
|
198
210
|
changed.push(...hinted);
|
|
199
211
|
|
|
@@ -202,6 +214,9 @@ const refreshState = async (state: RepoState, hints: string[] = [], force = fals
|
|
|
202
214
|
if (probe) {
|
|
203
215
|
const headMoved = probe.head !== state.head;
|
|
204
216
|
state.head = probe.head;
|
|
217
|
+
if (headMoved) {
|
|
218
|
+
checkout = (await gitReflogAction(state.root))?.startsWith("checkout:") ?? false;
|
|
219
|
+
}
|
|
205
220
|
// Untracked directories appear collapsed ("dir/") in porcelain; adds
|
|
206
221
|
// inside them only surface through a relist. Relist moments are rare.
|
|
207
222
|
const needsList = probe.relist || probe.changes.some((c) => c.path.endsWith("/"));
|
|
@@ -274,6 +289,7 @@ const refreshState = async (state: RepoState, hints: string[] = [], force = fals
|
|
|
274
289
|
return state;
|
|
275
290
|
}
|
|
276
291
|
const fresh = await assembleState(state.root, files, store, report, state.gitKind, state.head, state.dirty);
|
|
292
|
+
if (checkout) fresh.checkout = true;
|
|
277
293
|
states.set(state.root, fresh);
|
|
278
294
|
return fresh;
|
|
279
295
|
};
|
package/src/core/sync.ts
CHANGED
|
@@ -163,6 +163,9 @@ export const warmSync = async (root: string, params: WarmParams, state?: RepoSta
|
|
|
163
163
|
const cur = state ?? (await ensureState(root, { hints: params.files ?? [], force: false }));
|
|
164
164
|
const prev = getBaseline(root);
|
|
165
165
|
if (!prev || prev.version === cur.version) return;
|
|
166
|
+
// A checkout generation re-baselines silently in sync: no cascade to
|
|
167
|
+
// prepare, and precomputing one over the branch diff would be waste.
|
|
168
|
+
if (cur.checkout) return;
|
|
166
169
|
const files = semanticDrift(cur, prev);
|
|
167
170
|
if (!files.length) return;
|
|
168
171
|
const key = filesKey(files);
|
|
@@ -294,6 +297,19 @@ export const sync = async (
|
|
|
294
297
|
};
|
|
295
298
|
}
|
|
296
299
|
|
|
300
|
+
// Branch-switch re-baseline. A checkout moves HEAD and re-materializes the
|
|
301
|
+
// worktree without authored edits; cascading over the branch diff would
|
|
302
|
+
// fire a loud, useless steer on every switch, so the baseline just follows
|
|
303
|
+
// the ref (details.baseline keeps the hooks' ack-clean notify silent too).
|
|
304
|
+
// Heat memory belongs to the old ref's field and does not cross over.
|
|
305
|
+
if (state.checkout) {
|
|
306
|
+
setBaseline(root, await snapshot(state));
|
|
307
|
+
return {
|
|
308
|
+
structural: true, red: false, tokens: 0,
|
|
309
|
+
details: { version: state.version, checkout: true, baseline: "established", anchors: state.graph.anchors.length },
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
297
313
|
// Version drifted: measure what moved. Implicit (tier-3 discovered) hubs
|
|
298
314
|
// churn is reported but NEVER escalates alone — hypotheses with no first-class
|
|
299
315
|
// backing don't get to wake the model with a red verdict.
|
package/src/ui/settings.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
this.addChild(this.
|
|
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
|
-
?
|
|
198
|
-
:
|
|
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", "
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|