@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.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/CHANGELOG.md +128 -0
- package/dist/cli.js +4006 -3966
- package/dist/types/advisor/advise-tool.d.ts +5 -2
- package/dist/types/advisor/config.d.ts +14 -6
- package/dist/types/advisor/runtime.d.ts +20 -11
- package/dist/types/autolearn/controller.d.ts +1 -0
- package/dist/types/cli-commands.d.ts +14 -1
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +32 -7
- package/dist/types/config/settings.d.ts +50 -0
- package/dist/types/cursor.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +5 -2
- package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +18 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
- package/dist/types/lib/xai-http.d.ts +11 -0
- package/dist/types/mcp/transports/stdio.d.ts +13 -1
- package/dist/types/modes/acp/acp-agent.d.ts +3 -0
- package/dist/types/modes/acp/acp-mode.d.ts +6 -2
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/assistant-message.d.ts +2 -0
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +23 -1
- package/dist/types/modes/components/hook-editor.d.ts +16 -2
- package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +4 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
- package/dist/types/modes/print-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
- package/dist/types/modes/warp-events.d.ts +24 -0
- package/dist/types/modes/warp-events.test.d.ts +1 -0
- package/dist/types/plan-mode/model-transition.d.ts +47 -0
- package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
- package/dist/types/registry/agent-lifecycle.d.ts +26 -1
- package/dist/types/sdk.d.ts +13 -2
- package/dist/types/session/agent-session.d.ts +43 -11
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +14 -0
- package/dist/types/session/streaming-output.d.ts +2 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/slash-commands/types.d.ts +4 -4
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/task/label.d.ts +1 -1
- package/dist/types/telemetry-export.d.ts +34 -9
- package/dist/types/tools/approval.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/browser/registry.d.ts +7 -3
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/essential-tools.d.ts +29 -0
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/manage-skill.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/utils/title-generator.d.ts +2 -1
- package/dist/types/web/search/index.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +3 -0
- package/dist/types/web/search/providers/kimi.d.ts +4 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +21 -16
- package/src/advisor/__tests__/advisor.test.ts +1358 -86
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/advise-tool.ts +7 -3
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli/bench-cli.ts +4 -1
- package/src/cli/grep-cli.ts +2 -1
- package/src/cli-commands.ts +57 -23
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +34 -6
- package/src/config/model-resolver.ts +57 -12
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +43 -6
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/codex.ts +14 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/discovery/substitute-plugin-root.ts +23 -7
- package/src/exec/bash-executor.ts +14 -5
- package/src/export/html/template.js +2 -0
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/types.ts +18 -0
- package/src/extensibility/extensions/wrapper.ts +2 -1
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
- package/src/extensibility/plugins/manager.ts +2 -2
- package/src/extensibility/shared-events.ts +6 -0
- package/src/extensibility/utils.ts +91 -25
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
- package/src/internal-urls/agent-protocol.ts +73 -39
- package/src/irc/bus.ts +22 -3
- package/src/launch/broker.ts +3 -2
- package/src/launch/client.ts +2 -2
- package/src/launch/presence.ts +2 -2
- package/src/lib/xai-http.ts +28 -2
- package/src/lsp/client.ts +1 -1
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/transports/stdio.ts +103 -23
- package/src/modes/acp/acp-agent.ts +28 -9
- package/src/modes/acp/acp-event-mapper.ts +8 -0
- package/src/modes/acp/acp-mode.ts +18 -4
- package/src/modes/components/advisor-config.ts +65 -3
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/assistant-message.ts +35 -14
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
- package/src/modes/components/custom-editor.ts +62 -1
- package/src/modes/components/hook-editor.ts +35 -5
- package/src/modes/components/login-dialog.test.ts +56 -0
- package/src/modes/components/login-dialog.ts +7 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/plan-review-overlay.ts +51 -10
- package/src/modes/components/session-selector.ts +10 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/component.ts +2 -4
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/components/tool-execution.test.ts +101 -0
- package/src/modes/components/tool-execution.ts +117 -9
- package/src/modes/components/transcript-container.ts +30 -2
- package/src/modes/controllers/command-controller-shared.ts +1 -1
- package/src/modes/controllers/command-controller.ts +168 -48
- package/src/modes/controllers/event-controller.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -22
- package/src/modes/controllers/input-controller.ts +12 -12
- package/src/modes/controllers/selector-controller.ts +208 -46
- package/src/modes/interactive-mode.ts +205 -65
- package/src/modes/noninteractive-dispose.test.ts +60 -0
- package/src/modes/print-mode.ts +10 -5
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +31 -7
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +10 -0
- package/src/modes/utils/interactive-context-helpers.ts +3 -1
- package/src/modes/utils/transcript-render-helpers.ts +3 -2
- package/src/modes/warp-events.test.ts +794 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/plan-mode/model-transition.test.ts +60 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/sdk.ts +227 -44
- package/src/session/agent-session.ts +1364 -382
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-listing.ts +6 -5
- package/src/session/session-loader.ts +9 -0
- package/src/session/session-manager.ts +48 -0
- package/src/session/session-persistence.ts +11 -0
- package/src/session/streaming-output.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +9 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/slash-commands/types.ts +4 -4
- package/src/system-prompt.ts +2 -2
- package/src/task/executor.ts +1 -1
- package/src/task/label.ts +2 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- package/src/tools/browser/registry.ts +9 -2
- package/src/tools/browser/tab-supervisor.ts +65 -10
- package/src/tools/browser.ts +4 -3
- package/src/tools/essential-tools.ts +45 -0
- package/src/tools/eval.ts +3 -0
- package/src/tools/gh.ts +169 -2
- package/src/tools/image-gen.ts +624 -513
- package/src/tools/index.ts +10 -6
- package/src/tools/manage-skill.ts +5 -3
- package/src/tools/output-meta.ts +7 -0
- package/src/tools/path-utils.ts +8 -5
- package/src/tools/read.ts +48 -3
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/utils/title-generator.ts +15 -4
- package/src/web/search/index.ts +21 -6
- package/src/web/search/providers/base.ts +3 -0
- package/src/web/search/providers/kimi.ts +18 -12
- package/src/web/search/providers/xai.ts +40 -9
- package/src/web/search/types.ts +6 -1
|
@@ -67,6 +67,8 @@ import {
|
|
|
67
67
|
// Schema Definition Types
|
|
68
68
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
69
69
|
|
|
70
|
+
export type ModelRoleStorage = "global" | "project";
|
|
71
|
+
|
|
70
72
|
export type SettingTab =
|
|
71
73
|
| "appearance"
|
|
72
74
|
| "model"
|
|
@@ -509,6 +511,30 @@ export const SETTINGS_SCHEMA = {
|
|
|
509
511
|
|
|
510
512
|
disabledExtensions: { type: "array", default: EMPTY_STRING_ARRAY },
|
|
511
513
|
|
|
514
|
+
modelRoleStorage: {
|
|
515
|
+
type: "enum",
|
|
516
|
+
values: ["global", "project"] as const,
|
|
517
|
+
default: "global",
|
|
518
|
+
ui: {
|
|
519
|
+
tab: "model",
|
|
520
|
+
group: "Prompt",
|
|
521
|
+
label: "Model Role Storage",
|
|
522
|
+
description: "Where model selector role assignments are saved",
|
|
523
|
+
options: [
|
|
524
|
+
{
|
|
525
|
+
value: "global",
|
|
526
|
+
label: "Global",
|
|
527
|
+
description: "Save role models in the active profile config (current behavior)",
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
value: "project",
|
|
531
|
+
label: "Per-project",
|
|
532
|
+
description: "Save project role models in .omp/config.yml; missing project roles use global defaults",
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
|
|
512
538
|
modelRoles: { type: "record", default: EMPTY_STRING_RECORD },
|
|
513
539
|
|
|
514
540
|
modelTags: { type: "record", default: EMPTY_MODEL_TAGS_RECORD },
|
|
@@ -1439,7 +1465,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1439
1465
|
group: "Retry & Fallback",
|
|
1440
1466
|
label: "Retry Fallback Chains",
|
|
1441
1467
|
description:
|
|
1442
|
-
'JSON object mapping model roles, model selectors ("provider/model-id"), or provider wildcards ("provider/*") to ordered fallback selectors, e.g. {"default":["openai/gpt-4o-mini"],"google-antigravity/*":["google/*","google-vertex/*"]}. Model-oriented keys apply whenever that model/provider is active, regardless of role; a "provider/*" entry keeps the failing model\'s id and swaps the provider.',
|
|
1468
|
+
'JSON object mapping model roles, model selectors ("provider/model-id"), or provider wildcards ("provider/*") to ordered fallback selectors, e.g. {"default":["openai/gpt-4o-mini"],"google-antigravity/*":["google/*","google-vertex/*"]}. Model-oriented keys apply whenever that model/provider is active, regardless of role; a "provider/*" entry keeps the failing model\'s id and swaps the provider. An id-prefixed wildcard ("openrouter/google/*") re-prefixes the failing model\'s bare id (google-antigravity/gemini-x -> openrouter/google/gemini-x) and, used as a key, matches only that provider\'s ids under the prefix.',
|
|
1443
1469
|
},
|
|
1444
1470
|
},
|
|
1445
1471
|
"retry.fallbackRevertPolicy": {
|
|
@@ -2466,7 +2492,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
2466
2492
|
group: "Auto-Learn",
|
|
2467
2493
|
label: "Auto-run capture at stop",
|
|
2468
2494
|
description:
|
|
2469
|
-
"When on, auto-run one capture turn at stop (uses extra tokens).
|
|
2495
|
+
"When on, auto-run one private capture turn at stop (uses extra tokens). When off, only standing auto-learn guidance remains.",
|
|
2470
2496
|
condition: "autolearnActive",
|
|
2471
2497
|
},
|
|
2472
2498
|
},
|
|
@@ -3616,7 +3642,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
3616
3642
|
},
|
|
3617
3643
|
"generate_image.enabled": {
|
|
3618
3644
|
type: "boolean",
|
|
3619
|
-
default:
|
|
3645
|
+
default: false,
|
|
3620
3646
|
ui: {
|
|
3621
3647
|
tab: "tools",
|
|
3622
3648
|
group: "Available Tools",
|
|
@@ -4468,7 +4494,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
4468
4494
|
},
|
|
4469
4495
|
"providers.image": {
|
|
4470
4496
|
type: "enum",
|
|
4471
|
-
values: ["auto", "openai", "antigravity", "xai", "gemini", "openrouter"] as const,
|
|
4497
|
+
values: ["auto", "openai", "openai-codex", "antigravity", "xai", "gemini", "openrouter"] as const,
|
|
4472
4498
|
default: "auto",
|
|
4473
4499
|
ui: {
|
|
4474
4500
|
tab: "providers",
|
|
@@ -4479,9 +4505,20 @@ export const SETTINGS_SCHEMA = {
|
|
|
4479
4505
|
{
|
|
4480
4506
|
value: "auto",
|
|
4481
4507
|
label: "Auto",
|
|
4482
|
-
description:
|
|
4508
|
+
description:
|
|
4509
|
+
"Priority: per-request provider > configured provider > active session provider > GPT model image tool > Codex subscription > Antigravity > xAI > OpenRouter > Gemini",
|
|
4510
|
+
},
|
|
4511
|
+
{
|
|
4512
|
+
value: "openai",
|
|
4513
|
+
label: "OpenAI",
|
|
4514
|
+
description:
|
|
4515
|
+
"OPENAI_API_KEY (gpt-image-2) or active GPT model; falls back to a connected Codex subscription",
|
|
4516
|
+
},
|
|
4517
|
+
{
|
|
4518
|
+
value: "openai-codex",
|
|
4519
|
+
label: "OpenAI Codex (ChatGPT)",
|
|
4520
|
+
description: "Uses a connected Codex / ChatGPT subscription — no OPENAI_API_KEY needed",
|
|
4483
4521
|
},
|
|
4484
|
-
{ value: "openai", label: "OpenAI", description: "Uses the active GPT Responses/Codex model" },
|
|
4485
4522
|
{
|
|
4486
4523
|
value: "antigravity",
|
|
4487
4524
|
label: "Antigravity",
|
package/src/config/settings.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
setWorktreesDir,
|
|
28
28
|
} from "@oh-my-pi/pi-utils";
|
|
29
29
|
import { JSONC, YAML } from "bun";
|
|
30
|
+
import { invalidate as invalidateCapabilityFsCache } from "../capability/fs";
|
|
30
31
|
import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
|
|
31
32
|
import type { ModelRole } from "../config/model-roles";
|
|
32
33
|
import { loadCapability } from "../discovery";
|
|
@@ -172,6 +173,83 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
172
173
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Migrate a v17 leaf rename that used to nest under a boolean parent path
|
|
178
|
+
* (`dev.autoqa.consent` → `dev.autoqaConsent`, `todo.reminders.max` →
|
|
179
|
+
* `todo.remindersMax`). Pre-rename configs left the leaf beneath the parent,
|
|
180
|
+
* so the parent path resolved to an object and truthy checks like
|
|
181
|
+
* `isAutoQaEnabled` treated a consent-only container as "enabled".
|
|
182
|
+
*
|
|
183
|
+
* Handles nested (`{ parent: { leaf } }`) and quoted-dotted (`"parent.leaf"`)
|
|
184
|
+
* legacy sources. An explicit new key always wins; a separately configured
|
|
185
|
+
* boolean parent is preserved; an irrecoverable object-valued parent (only ever
|
|
186
|
+
* a container for the old leaf) is dropped so the schema default applies.
|
|
187
|
+
*/
|
|
188
|
+
function migrateNestedLeafRename(
|
|
189
|
+
raw: RawSettings,
|
|
190
|
+
root: string,
|
|
191
|
+
parent: string,
|
|
192
|
+
oldLeaf: string,
|
|
193
|
+
newLeaf: string,
|
|
194
|
+
isLeafValue: (value: unknown) => boolean,
|
|
195
|
+
): void {
|
|
196
|
+
const rootObj = isRecord(raw[root]) ? (raw[root] as Record<string, unknown>) : undefined;
|
|
197
|
+
const nestedParent = rootObj?.[parent];
|
|
198
|
+
const flatParent = raw[`${root}.${parent}`];
|
|
199
|
+
const oldParentPath = `${root}.${parent}`;
|
|
200
|
+
|
|
201
|
+
const candidates = [
|
|
202
|
+
rootObj?.[newLeaf],
|
|
203
|
+
raw[`${root}.${newLeaf}`],
|
|
204
|
+
isRecord(nestedParent) ? nestedParent[oldLeaf] : undefined,
|
|
205
|
+
raw[`${oldParentPath}.${oldLeaf}`],
|
|
206
|
+
];
|
|
207
|
+
const resolvedLeaf = candidates.find(isLeafValue);
|
|
208
|
+
|
|
209
|
+
const recoveredParent =
|
|
210
|
+
typeof nestedParent === "boolean" ? nestedParent : typeof flatParent === "boolean" ? flatParent : undefined;
|
|
211
|
+
|
|
212
|
+
const ensureRoot = (): Record<string, unknown> => {
|
|
213
|
+
const current = raw[root];
|
|
214
|
+
if (isRecord(current)) return current;
|
|
215
|
+
const created: Record<string, unknown> = {};
|
|
216
|
+
raw[root] = created;
|
|
217
|
+
return created;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (resolvedLeaf !== undefined) {
|
|
221
|
+
const target = ensureRoot();
|
|
222
|
+
if (!isLeafValue(target[newLeaf])) {
|
|
223
|
+
target[newLeaf] = resolvedLeaf;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Strip legacy leaf sources (nested + flat dotted).
|
|
228
|
+
delete raw[`${oldParentPath}.${oldLeaf}`];
|
|
229
|
+
delete raw[`${root}.${newLeaf}`];
|
|
230
|
+
if (isRecord(raw[root]) && isRecord((raw[root] as Record<string, unknown>)[parent])) {
|
|
231
|
+
const parentObj = (raw[root] as Record<string, unknown>)[parent] as Record<string, unknown>;
|
|
232
|
+
delete parentObj[oldLeaf];
|
|
233
|
+
if (Object.keys(parentObj).length === 0) {
|
|
234
|
+
delete (raw[root] as Record<string, unknown>)[parent];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// The parent path must be a boolean or absent — never a leftover object.
|
|
239
|
+
if (recoveredParent !== undefined) {
|
|
240
|
+
const target = ensureRoot();
|
|
241
|
+
if (typeof target[parent] !== "boolean") {
|
|
242
|
+
target[parent] = recoveredParent;
|
|
243
|
+
}
|
|
244
|
+
} else if (isRecord(raw[root]) && isRecord((raw[root] as Record<string, unknown>)[parent])) {
|
|
245
|
+
delete (raw[root] as Record<string, unknown>)[parent];
|
|
246
|
+
}
|
|
247
|
+
delete raw[oldParentPath];
|
|
248
|
+
if (isRecord(raw[root]) && Object.keys(raw[root] as Record<string, unknown>).length === 0) {
|
|
249
|
+
delete raw[root];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
175
253
|
function modelRoleValueFromUnknown(value: unknown): string | undefined {
|
|
176
254
|
if (typeof value === "string") return value;
|
|
177
255
|
if (!Array.isArray(value)) return undefined;
|
|
@@ -250,6 +328,16 @@ export class Settings {
|
|
|
250
328
|
|
|
251
329
|
/** Paths modified during this session (for partial save) */
|
|
252
330
|
#modified = new Set<string>();
|
|
331
|
+
/** Individual project model roles modified during this session */
|
|
332
|
+
#modifiedProjectModelRoles = new Set<string>();
|
|
333
|
+
/**
|
|
334
|
+
* Original process-wide model-role overrides captured before a project edit
|
|
335
|
+
* temporarily replaced them via `#updateRuntimeModelRoleOverride`. Restored
|
|
336
|
+
* on `reloadForCwd` / `cloneForCwd` so destination projects never inherit the
|
|
337
|
+
* source-project value. Maps role → original override value (`undefined`
|
|
338
|
+
* when the role had no runtime override).
|
|
339
|
+
*/
|
|
340
|
+
#savedRuntimeModelRoleOverrides = new Map<string, string | undefined>();
|
|
253
341
|
|
|
254
342
|
/** Legacy `lastChangelogVersion` captured from config.yml during migration (now a marker file). */
|
|
255
343
|
#legacyLastChangelogVersion?: string;
|
|
@@ -257,6 +345,8 @@ export class Settings {
|
|
|
257
345
|
/** Pending save (debounced) */
|
|
258
346
|
#saveTimer?: NodeJS.Timeout;
|
|
259
347
|
#savePromise?: Promise<void>;
|
|
348
|
+
#projectSaveTimer?: NodeJS.Timeout;
|
|
349
|
+
#projectSavePromise?: Promise<void>;
|
|
260
350
|
|
|
261
351
|
/** Whether to persist changes */
|
|
262
352
|
#persist: boolean;
|
|
@@ -400,6 +490,9 @@ export class Settings {
|
|
|
400
490
|
* Apply runtime overrides (not persisted).
|
|
401
491
|
*/
|
|
402
492
|
override<P extends SettingPath>(path: P, value: SettingValue<P>): void {
|
|
493
|
+
if (path === "modelRoles") {
|
|
494
|
+
this.#savedRuntimeModelRoleOverrides.clear();
|
|
495
|
+
}
|
|
403
496
|
const prev = this.get(path);
|
|
404
497
|
const segments = path.split(".");
|
|
405
498
|
setByPath(this.#overrides, segments, value);
|
|
@@ -411,6 +504,9 @@ export class Settings {
|
|
|
411
504
|
* Clear a runtime override.
|
|
412
505
|
*/
|
|
413
506
|
clearOverride(path: SettingPath): void {
|
|
507
|
+
if (path === "modelRoles") {
|
|
508
|
+
this.#savedRuntimeModelRoleOverrides.clear();
|
|
509
|
+
}
|
|
414
510
|
const prev = this.get(path);
|
|
415
511
|
const segments = path.split(".");
|
|
416
512
|
let current = this.#overrides;
|
|
@@ -443,12 +539,22 @@ export class Settings {
|
|
|
443
539
|
clearTimeout(this.#saveTimer);
|
|
444
540
|
this.#saveTimer = undefined;
|
|
445
541
|
}
|
|
542
|
+
if (this.#projectSaveTimer) {
|
|
543
|
+
clearTimeout(this.#projectSaveTimer);
|
|
544
|
+
this.#projectSaveTimer = undefined;
|
|
545
|
+
}
|
|
446
546
|
if (this.#savePromise) {
|
|
447
547
|
await this.#savePromise;
|
|
448
548
|
}
|
|
549
|
+
if (this.#projectSavePromise) {
|
|
550
|
+
await this.#projectSavePromise;
|
|
551
|
+
}
|
|
449
552
|
if (this.#modified.size > 0) {
|
|
450
553
|
await this.#saveNow();
|
|
451
554
|
}
|
|
555
|
+
if (this.#modifiedProjectModelRoles.size > 0) {
|
|
556
|
+
await this.#saveProjectNow();
|
|
557
|
+
}
|
|
452
558
|
}
|
|
453
559
|
|
|
454
560
|
async cloneForCwd(cwd: string): Promise<Settings> {
|
|
@@ -463,7 +569,7 @@ export class Settings {
|
|
|
463
569
|
cloned.#project = this.#persist ? await cloned.#loadProjectSettings() : structuredClone(this.#project);
|
|
464
570
|
cloned.#configFiles = [...this.#configFiles];
|
|
465
571
|
cloned.#configOverlay = structuredClone(this.#configOverlay);
|
|
466
|
-
cloned.#overrides =
|
|
572
|
+
cloned.#overrides = this.#buildOriginalOverrides();
|
|
467
573
|
cloned.#rebuildMerged();
|
|
468
574
|
cloned.#fireAllHooks();
|
|
469
575
|
return cloned;
|
|
@@ -484,6 +590,8 @@ export class Settings {
|
|
|
484
590
|
async reloadForCwd(cwd: string): Promise<void> {
|
|
485
591
|
const normalized = path.normalize(cwd);
|
|
486
592
|
if (normalized === this.#cwd) return;
|
|
593
|
+
await this.flush();
|
|
594
|
+
this.#restoreRuntimeModelRoleOverrides();
|
|
487
595
|
const prevModelRoles = this.get("modelRoles");
|
|
488
596
|
this.#cwd = normalized;
|
|
489
597
|
if (this.#persist) {
|
|
@@ -602,35 +710,168 @@ export class Settings {
|
|
|
602
710
|
return roles;
|
|
603
711
|
}
|
|
604
712
|
|
|
713
|
+
#modelRoleLayerOwns(layer: RawSettings, role: ModelRole | string): boolean {
|
|
714
|
+
const value = getByPath(layer, ["modelRoles"]);
|
|
715
|
+
if (!isRecord(value)) return false;
|
|
716
|
+
return Object.hasOwn(value, role);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Set the full `modelRoles` map on the runtime override layer without
|
|
721
|
+
* routing through the public {@link override} method. Internal callers
|
|
722
|
+
* (project edits, global fallback updates) use this so they can control
|
|
723
|
+
* capture invalidation independently of the whole-map replacement
|
|
724
|
+
* semantics that `override("modelRoles", …)` carries.
|
|
725
|
+
*/
|
|
726
|
+
#setRuntimeModelRoleOverrides(next: Record<string, string>): void {
|
|
727
|
+
const prev = this.get("modelRoles");
|
|
728
|
+
setByPath(this.#overrides, ["modelRoles"], next);
|
|
729
|
+
this.#rebuildMerged();
|
|
730
|
+
this.#fireEffectiveSettingChanged("modelRoles", this.get("modelRoles"), prev);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
#updateRuntimeModelRoleOverride(role: ModelRole | string, modelId: string | undefined): void {
|
|
734
|
+
const runtimeOverrides = getByPath(this.#overrides, ["modelRoles"]);
|
|
735
|
+
if (!isRecord(runtimeOverrides) || !Object.hasOwn(runtimeOverrides, role)) return;
|
|
736
|
+
|
|
737
|
+
const nextRuntimeOverride = this.#modelRolesFromLayer(this.#overrides);
|
|
738
|
+
if (modelId === undefined) {
|
|
739
|
+
delete nextRuntimeOverride[role];
|
|
740
|
+
} else {
|
|
741
|
+
nextRuntimeOverride[role] = modelId;
|
|
742
|
+
}
|
|
743
|
+
this.#setRuntimeModelRoleOverrides(nextRuntimeOverride);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Capture the original process-wide override for `role` the first time a
|
|
748
|
+
* project edit temporarily replaces it, so the original can be restored on
|
|
749
|
+
* cwd changes. Subsequent edits in the same cwd must not overwrite the
|
|
750
|
+
* first captured value.
|
|
751
|
+
*/
|
|
752
|
+
#captureRuntimeModelRoleOverride(role: ModelRole | string): void {
|
|
753
|
+
if (this.#savedRuntimeModelRoleOverrides.has(role)) return;
|
|
754
|
+
const runtimeOverrides = getByPath(this.#overrides, ["modelRoles"]);
|
|
755
|
+
if (!isRecord(runtimeOverrides) || !Object.hasOwn(runtimeOverrides, role)) return;
|
|
756
|
+
this.#savedRuntimeModelRoleOverrides.set(role, this.#modelRolesFromLayer(this.#overrides)[role]);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Restore original process-wide model-role overrides that were temporarily
|
|
761
|
+
* replaced by project edits, mutating `#overrides` in place without
|
|
762
|
+
* rebuilding. All remaining captures are valid because superseding
|
|
763
|
+
* operations (late `overrideModelRoles`, global-mode `setModelRole`,
|
|
764
|
+
* whole-map `override`/`clearOverride`) invalidate the affected captures
|
|
765
|
+
* at the point of supersession. Caller is responsible for `#rebuildMerged()`.
|
|
766
|
+
*/
|
|
767
|
+
#restoreRuntimeModelRoleOverrides(): void {
|
|
768
|
+
if (this.#savedRuntimeModelRoleOverrides.size === 0) return;
|
|
769
|
+
const runtimeRoles = getByPath(this.#overrides, ["modelRoles"]);
|
|
770
|
+
if (!isRecord(runtimeRoles)) {
|
|
771
|
+
this.#savedRuntimeModelRoleOverrides.clear();
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
for (const [role, originalValue] of this.#savedRuntimeModelRoleOverrides) {
|
|
775
|
+
if (originalValue === undefined) {
|
|
776
|
+
delete runtimeRoles[role];
|
|
777
|
+
} else {
|
|
778
|
+
runtimeRoles[role] = originalValue;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
this.#savedRuntimeModelRoleOverrides.clear();
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Produce a deep copy of `#overrides` with original process-wide model-role
|
|
786
|
+
* overrides restored, for use by {@link cloneForCwd}. All remaining
|
|
787
|
+
* captures are valid (see {@link #restoreRuntimeModelRoleOverrides}).
|
|
788
|
+
* Does not mutate the current instance's `#overrides`.
|
|
789
|
+
*/
|
|
790
|
+
#buildOriginalOverrides(): RawSettings {
|
|
791
|
+
if (this.#savedRuntimeModelRoleOverrides.size === 0) {
|
|
792
|
+
return structuredClone(this.#overrides);
|
|
793
|
+
}
|
|
794
|
+
const overrides = structuredClone(this.#overrides);
|
|
795
|
+
const runtimeRoles = getByPath(overrides, ["modelRoles"]);
|
|
796
|
+
if (!isRecord(runtimeRoles)) return overrides;
|
|
797
|
+
for (const [role, originalValue] of this.#savedRuntimeModelRoleOverrides) {
|
|
798
|
+
if (originalValue === undefined) {
|
|
799
|
+
delete runtimeRoles[role];
|
|
800
|
+
} else {
|
|
801
|
+
runtimeRoles[role] = originalValue;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return overrides;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
#setProjectModelRoleValue(role: ModelRole | string, modelId: string | null): void {
|
|
808
|
+
const prev = this.get("modelRoles");
|
|
809
|
+
const projectRoles = getByPath(this.#project, ["modelRoles"]);
|
|
810
|
+
const current: Record<string, unknown> = isRecord(projectRoles) ? { ...projectRoles } : {};
|
|
811
|
+
current[role] = modelId;
|
|
812
|
+
setByPath(this.#project, ["modelRoles"], current);
|
|
813
|
+
this.#modifiedProjectModelRoles.add(role);
|
|
814
|
+
this.#rebuildMerged();
|
|
815
|
+
this.#fireEffectiveSettingChanged("modelRoles", this.get("modelRoles"), prev);
|
|
816
|
+
this.#queueProjectSave();
|
|
817
|
+
}
|
|
818
|
+
|
|
605
819
|
/**
|
|
606
820
|
* Set a model role (helper for modelRoles record). Passing `undefined`
|
|
607
821
|
* clears the role from the persisted record and any runtime override.
|
|
822
|
+
*
|
|
823
|
+
* In project storage mode, when a project edit has temporarily replaced
|
|
824
|
+
* the process-wide runtime override for `role` and that override is still
|
|
825
|
+
* active (the runtime slot currently matches the project value), the
|
|
826
|
+
* global-layer write must not rewrite that runtime slot — otherwise the
|
|
827
|
+
* global fallback would immediately shadow the still-configured project
|
|
828
|
+
* role. The global layer is still persisted; only the runtime override is
|
|
829
|
+
* left untouched. The guard is precise so that a later clear, a late
|
|
830
|
+
* `overrideModelRoles`, or a storage-mode transition does not leave a
|
|
831
|
+
* stale skip in place.
|
|
608
832
|
*/
|
|
609
833
|
setModelRole(role: ModelRole | string, modelId: string | undefined): void {
|
|
610
834
|
const current = this.#modelRolesFromLayer(this.#global);
|
|
611
|
-
const runtimeOverrides = getByPath(this.#overrides, ["modelRoles"]);
|
|
612
|
-
const updateRuntimeOverride =
|
|
613
|
-
!!runtimeOverrides &&
|
|
614
|
-
typeof runtimeOverrides === "object" &&
|
|
615
|
-
!Array.isArray(runtimeOverrides) &&
|
|
616
|
-
Object.hasOwn(runtimeOverrides, role);
|
|
617
|
-
|
|
618
835
|
if (modelId === undefined) {
|
|
619
836
|
delete current[role];
|
|
620
837
|
} else {
|
|
621
838
|
current[role] = modelId;
|
|
622
839
|
}
|
|
623
840
|
this.set("modelRoles", current);
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const nextRuntimeOverride = this.#modelRolesFromLayer(this.#overrides);
|
|
627
|
-
if (modelId === undefined) {
|
|
628
|
-
delete nextRuntimeOverride[role];
|
|
629
|
-
} else {
|
|
630
|
-
nextRuntimeOverride[role] = modelId;
|
|
631
|
-
}
|
|
632
|
-
this.override("modelRoles", nextRuntimeOverride);
|
|
841
|
+
if (this.isProjectModelRoleRuntimeOverrideActive(role)) {
|
|
842
|
+
return;
|
|
633
843
|
}
|
|
844
|
+
this.#savedRuntimeModelRoleOverrides.delete(role);
|
|
845
|
+
this.#updateRuntimeModelRoleOverride(role, modelId);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Whether `role`'s runtime override slot currently holds the temporary
|
|
850
|
+
* project-scoped value installed by a prior `setProjectModelRole`. Returns
|
|
851
|
+
* `false` when storage is not project-mode, no capture exists, or the
|
|
852
|
+
* project role was cleared. With explicit provenance invalidation, a
|
|
853
|
+
* surviving capture implies no external supersession occurred.
|
|
854
|
+
*/
|
|
855
|
+
isProjectModelRoleRuntimeOverrideActive(role: ModelRole | string): boolean {
|
|
856
|
+
if (this.get("modelRoleStorage") !== "project") return false;
|
|
857
|
+
if (!this.#savedRuntimeModelRoleOverrides.has(role)) return false;
|
|
858
|
+
return !!this.getProjectModelRole(role);
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Set a model role in the current project's settings layer.
|
|
862
|
+
*/
|
|
863
|
+
setProjectModelRole(role: ModelRole | string, modelId: string): void {
|
|
864
|
+
this.#setProjectModelRoleValue(role, modelId);
|
|
865
|
+
this.#captureRuntimeModelRoleOverride(role);
|
|
866
|
+
this.#updateRuntimeModelRoleOverride(role, modelId);
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Clear a model role from the current project's settings layer.
|
|
870
|
+
*/
|
|
871
|
+
clearProjectModelRole(role: ModelRole | string): void {
|
|
872
|
+
this.#setProjectModelRoleValue(role, null);
|
|
873
|
+
this.#captureRuntimeModelRoleOverride(role);
|
|
874
|
+
this.#updateRuntimeModelRoleOverride(role, undefined);
|
|
634
875
|
}
|
|
635
876
|
|
|
636
877
|
/**
|
|
@@ -641,6 +882,49 @@ export class Settings {
|
|
|
641
882
|
if (!isRecord(roles)) return undefined;
|
|
642
883
|
return modelRoleValueFromUnknown(roles[role]);
|
|
643
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* Get a model role from only the global settings layer.
|
|
887
|
+
*/
|
|
888
|
+
getGlobalModelRole(role: ModelRole | string): string | undefined {
|
|
889
|
+
const modelId = this.#modelRolesFromLayer(this.#global)[role];
|
|
890
|
+
return modelId || undefined;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Get a model role from only the current project settings layer.
|
|
895
|
+
*/
|
|
896
|
+
getProjectModelRole(role: ModelRole | string): string | undefined {
|
|
897
|
+
const modelId = this.#modelRolesFromLayer(this.#project)[role];
|
|
898
|
+
return modelId || undefined;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Report which layer actually supplies the effective model role across
|
|
903
|
+
* full merge precedence (runtime override → config overlay → project →
|
|
904
|
+
* global → default). Unlike {@link getModelRoleSource}, this accounts
|
|
905
|
+
* for runtime and config-overlay layers and detects ownership by key
|
|
906
|
+
* presence rather than normalized value, so a `null` tombstone in the
|
|
907
|
+
* overlay or runtime layer correctly blocks lower layers. The project
|
|
908
|
+
* layer is checked through {@link #projectSettingsForMerge} because a
|
|
909
|
+
* project null is a cleared value (falls back to global), not a
|
|
910
|
+
* tombstone.
|
|
911
|
+
*/
|
|
912
|
+
getModelRoleProvenance(role: ModelRole | string): "runtime" | "overlay" | "project" | "global" | "default" {
|
|
913
|
+
if (this.#modelRoleLayerOwns(this.#overrides, role)) return "runtime";
|
|
914
|
+
if (this.#modelRoleLayerOwns(this.#configOverlay, role)) return "overlay";
|
|
915
|
+
if (this.#modelRoleLayerOwns(this.#projectSettingsForMerge(), role)) return "project";
|
|
916
|
+
if (this.#modelRoleLayerOwns(this.#global, role)) return "global";
|
|
917
|
+
return "default";
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Get the persisted layer supplying a model role (project/global/default only).
|
|
922
|
+
*/
|
|
923
|
+
getModelRoleSource(role: ModelRole | string): "project" | "global" | "default" {
|
|
924
|
+
if (this.getProjectModelRole(role)) return "project";
|
|
925
|
+
if (this.getGlobalModelRole(role)) return "global";
|
|
926
|
+
return "default";
|
|
927
|
+
}
|
|
644
928
|
|
|
645
929
|
/**
|
|
646
930
|
* Get all model roles (helper for modelRoles record).
|
|
@@ -668,9 +952,10 @@ export class Settings {
|
|
|
668
952
|
for (const [role, modelId] of Object.entries(roles)) {
|
|
669
953
|
if (modelId) {
|
|
670
954
|
next[role] = modelId;
|
|
955
|
+
this.#savedRuntimeModelRoleOverrides.delete(role);
|
|
671
956
|
}
|
|
672
957
|
}
|
|
673
|
-
this
|
|
958
|
+
this.#setRuntimeModelRoleOverrides(next);
|
|
674
959
|
}
|
|
675
960
|
|
|
676
961
|
/**
|
|
@@ -778,6 +1063,11 @@ export class Settings {
|
|
|
778
1063
|
merged = this.#deepMerge(merged, item.data as RawSettings);
|
|
779
1064
|
}
|
|
780
1065
|
}
|
|
1066
|
+
const nativeProject = await this.#loadYaml(path.join(this.#cwd, ".omp", "config.yml"));
|
|
1067
|
+
const nativeModelRoles = getByPath(nativeProject, ["modelRoles"]);
|
|
1068
|
+
if (nativeModelRoles !== undefined) {
|
|
1069
|
+
merged = this.#deepMerge(merged, { modelRoles: nativeModelRoles });
|
|
1070
|
+
}
|
|
781
1071
|
return this.#migrateRawSettings(merged);
|
|
782
1072
|
} catch {
|
|
783
1073
|
return {};
|
|
@@ -1253,6 +1543,26 @@ export class Settings {
|
|
|
1253
1543
|
if (tierTouched) raw.tier = tierObj;
|
|
1254
1544
|
delete raw.fastModeScope;
|
|
1255
1545
|
|
|
1546
|
+
// v17 renames that used to nest under a boolean parent path:
|
|
1547
|
+
// dev.autoqa.consent -> dev.autoqaConsent
|
|
1548
|
+
// todo.reminders.max -> todo.remindersMax
|
|
1549
|
+
migrateNestedLeafRename(
|
|
1550
|
+
raw,
|
|
1551
|
+
"dev",
|
|
1552
|
+
"autoqa",
|
|
1553
|
+
"consent",
|
|
1554
|
+
"autoqaConsent",
|
|
1555
|
+
value => value === "unset" || value === "granted" || value === "denied",
|
|
1556
|
+
);
|
|
1557
|
+
migrateNestedLeafRename(
|
|
1558
|
+
raw,
|
|
1559
|
+
"todo",
|
|
1560
|
+
"reminders",
|
|
1561
|
+
"max",
|
|
1562
|
+
"remindersMax",
|
|
1563
|
+
value => typeof value === "number" && Number.isFinite(value),
|
|
1564
|
+
);
|
|
1565
|
+
|
|
1256
1566
|
// BM25 tool discovery removal: tools.discoveryMode / tools.essentialOverride /
|
|
1257
1567
|
// mcp.discoveryMode / mcp.discoveryDefaultServers are gone with no
|
|
1258
1568
|
// replacement (`tools.xdev` stays at its own default). Dead keys are
|
|
@@ -1304,14 +1614,20 @@ export class Settings {
|
|
|
1304
1614
|
if (!this.#persist || !this.#configPath) return;
|
|
1305
1615
|
|
|
1306
1616
|
// Debounce: wait 100ms for more changes
|
|
1307
|
-
|
|
1308
|
-
clearTimeout(this.#saveTimer);
|
|
1309
|
-
}
|
|
1617
|
+
clearTimeout(this.#saveTimer);
|
|
1310
1618
|
this.#saveTimer = setTimeout(() => {
|
|
1311
1619
|
this.#saveTimer = undefined;
|
|
1312
|
-
this.#saveNow()
|
|
1313
|
-
|
|
1314
|
-
|
|
1620
|
+
const savePromise = this.#saveNow();
|
|
1621
|
+
this.#savePromise = savePromise;
|
|
1622
|
+
savePromise
|
|
1623
|
+
.catch(err => {
|
|
1624
|
+
logger.warn("Settings: background save failed", { error: String(err) });
|
|
1625
|
+
})
|
|
1626
|
+
.finally(() => {
|
|
1627
|
+
if (this.#savePromise === savePromise) {
|
|
1628
|
+
this.#savePromise = undefined;
|
|
1629
|
+
}
|
|
1630
|
+
});
|
|
1315
1631
|
}, 100);
|
|
1316
1632
|
}
|
|
1317
1633
|
|
|
@@ -1348,13 +1664,77 @@ export class Settings {
|
|
|
1348
1664
|
|
|
1349
1665
|
this.#rebuildMerged();
|
|
1350
1666
|
}
|
|
1667
|
+
#queueProjectSave(): void {
|
|
1668
|
+
if (!this.#persist) return;
|
|
1669
|
+
|
|
1670
|
+
clearTimeout(this.#projectSaveTimer);
|
|
1671
|
+
this.#projectSaveTimer = setTimeout(() => {
|
|
1672
|
+
this.#projectSaveTimer = undefined;
|
|
1673
|
+
const savePromise = this.#saveProjectNow();
|
|
1674
|
+
this.#projectSavePromise = savePromise;
|
|
1675
|
+
savePromise
|
|
1676
|
+
.catch(err => {
|
|
1677
|
+
logger.warn("Settings: background project save failed", { error: String(err) });
|
|
1678
|
+
})
|
|
1679
|
+
.finally(() => {
|
|
1680
|
+
if (this.#projectSavePromise === savePromise) {
|
|
1681
|
+
this.#projectSavePromise = undefined;
|
|
1682
|
+
}
|
|
1683
|
+
});
|
|
1684
|
+
}, 100);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
async #saveProjectNow(): Promise<void> {
|
|
1688
|
+
if (!this.#persist || this.#modifiedProjectModelRoles.size === 0) return;
|
|
1689
|
+
|
|
1690
|
+
const projectConfigPath = path.join(this.#cwd, ".omp", "config.yml");
|
|
1691
|
+
const modifiedModelRoles = [...this.#modifiedProjectModelRoles];
|
|
1692
|
+
this.#modifiedProjectModelRoles.clear();
|
|
1693
|
+
|
|
1694
|
+
try {
|
|
1695
|
+
await fs.promises.mkdir(path.dirname(projectConfigPath), { recursive: true });
|
|
1696
|
+
await withFileLock(projectConfigPath, async () => {
|
|
1697
|
+
const projectSettings = await this.#loadYaml(projectConfigPath);
|
|
1698
|
+
|
|
1699
|
+
const projectRoles = getByPath(this.#project, ["modelRoles"]);
|
|
1700
|
+
for (const role of modifiedModelRoles) {
|
|
1701
|
+
const value = isRecord(projectRoles) ? projectRoles[role] : undefined;
|
|
1702
|
+
setByPath(projectSettings, ["modelRoles", role], value);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
await Bun.write(projectConfigPath, YAML.stringify(projectSettings, null, 2));
|
|
1706
|
+
});
|
|
1707
|
+
invalidateCapabilityFsCache(projectConfigPath);
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
for (const role of modifiedModelRoles) {
|
|
1710
|
+
this.#modifiedProjectModelRoles.add(role);
|
|
1711
|
+
}
|
|
1712
|
+
throw error;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
this.#rebuildMerged();
|
|
1716
|
+
}
|
|
1351
1717
|
|
|
1352
1718
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1353
1719
|
// Utilities
|
|
1354
1720
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1355
1721
|
|
|
1722
|
+
#projectSettingsForMerge(): RawSettings {
|
|
1723
|
+
const projectRoles = getByPath(this.#project, ["modelRoles"]);
|
|
1724
|
+
if (!isRecord(projectRoles)) return this.#project;
|
|
1725
|
+
|
|
1726
|
+
let filteredRoles: Record<string, unknown> | undefined;
|
|
1727
|
+
for (const role in projectRoles) {
|
|
1728
|
+
if (!Object.hasOwn(projectRoles, role) || modelRoleValueFromUnknown(projectRoles[role]) !== undefined)
|
|
1729
|
+
continue;
|
|
1730
|
+
filteredRoles ??= { ...projectRoles };
|
|
1731
|
+
delete filteredRoles[role];
|
|
1732
|
+
}
|
|
1733
|
+
return filteredRoles ? { ...this.#project, modelRoles: filteredRoles } : this.#project;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1356
1736
|
#rebuildMerged(): void {
|
|
1357
|
-
this.#merged = this.#deepMerge(this.#deepMerge({}, this.#global), this.#
|
|
1737
|
+
this.#merged = this.#deepMerge(this.#deepMerge({}, this.#global), this.#projectSettingsForMerge());
|
|
1358
1738
|
this.#merged = this.#deepMerge(this.#merged, this.#configOverlay);
|
|
1359
1739
|
this.#merged = this.#deepMerge(this.#merged, this.#overrides);
|
|
1360
1740
|
this.#resolvedCache.clear();
|