pi-better-model-select 0.1.4 → 0.3.0
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 +55 -0
- package/README.md +58 -93
- package/docs/configuration.md +111 -0
- package/docs/development.md +104 -0
- package/extensions/bms.ts +100 -79
- package/extensions/lib/agents.ts +60 -0
- package/extensions/lib/defaults-models.ts +15 -0
- package/extensions/lib/defaults-store.ts +108 -0
- package/extensions/lib/defaults.ts +237 -0
- package/extensions/lib/effort-editor.ts +84 -0
- package/extensions/lib/flows.ts +129 -469
- package/extensions/lib/meta.ts +21 -7
- package/extensions/lib/picker-dialog.ts +13 -0
- package/extensions/lib/picker.ts +97 -154
- package/extensions/lib/store.ts +4 -1
- package/extensions/lib/tui.ts +320 -0
- package/package.json +9 -2
package/extensions/bms.ts
CHANGED
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
// with add / update / delete wired to ctrl-chords so plain typing always goes
|
|
5
5
|
// to the search box.
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { defaultsFlow, piPreference, setMainDefault } from "./lib/defaults.ts";
|
|
9
|
+
import { readSettings, settingsPath } from "./lib/defaults-store.ts";
|
|
10
|
+
import { pickerDialog } from "./lib/picker-dialog.ts";
|
|
11
|
+
import { confirmAction, editForm, tableSelect } from "./lib/tui.ts";
|
|
9
12
|
import { addFlow, deleteFlow, updateFlow, type FlowCtx } from "./lib/flows.ts";
|
|
10
|
-
import {
|
|
13
|
+
import { rankModels } from "./lib/picker.ts";
|
|
11
14
|
import {
|
|
12
15
|
loadConfig,
|
|
13
16
|
loadConfigFile,
|
|
@@ -76,51 +79,50 @@ function syncLocalConfig(pi: ExtensionAPI, ctx: any): ModelsConfig | null {
|
|
|
76
79
|
return effective;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
/**
|
|
82
|
+
/** Stage option changes until the user explicitly saves. */
|
|
80
83
|
async function optionsFlow(ctx: any, config: ModelsConfig): Promise<void> {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
84
|
+
const settings = loadSettings();
|
|
85
|
+
const providers = Object.keys(config.providers ?? {}).sort();
|
|
86
|
+
const writeLabel = () => settings.writeMode === "overlay" ? "BMS overlay (models.bms.json)" : "Pi config (models.json)";
|
|
87
|
+
await editForm(ctx, {
|
|
88
|
+
title: "Models / Options",
|
|
89
|
+
fields: [
|
|
90
|
+
{ key: "placement", label: "Result placement", value: settings.resultPlacement === "above" ? "Above prompt" : "Below prompt",
|
|
91
|
+
description: "The best match stays nearest the search line. Arrow keys follow the displayed direction.",
|
|
92
|
+
edit: async () => {
|
|
93
|
+
const value = await tableSelect(ctx, { title: "Models / Options / Result placement", initialValue: settings.resultPlacement,
|
|
94
|
+
rows: [{ value: "below", label: "Below prompt", description: "Pi-style results. Down moves into the list." }, { value: "above", label: "Above prompt", description: "Results grow upward. Up moves into the list." }] });
|
|
95
|
+
if (!value) return;
|
|
96
|
+
settings.resultPlacement = value as "above" | "below";
|
|
97
|
+
return value === "above" ? "Above prompt" : "Below prompt";
|
|
98
|
+
} },
|
|
99
|
+
{ key: "write", label: "Write mode", value: writeLabel(),
|
|
100
|
+
description: "Overlay keeps BMS edits separate from models.json. Direct mode edits Pi's model configuration.",
|
|
101
|
+
edit: async () => {
|
|
102
|
+
const value = await tableSelect(ctx, { title: "Models / Options / Write mode", initialValue: settings.writeMode,
|
|
103
|
+
rows: [{ value: "overlay", label: "BMS overlay", description: "models.bms.json" }, { value: "direct", label: "Pi config", description: "models.json" }] });
|
|
104
|
+
if (!value) return;
|
|
105
|
+
settings.writeMode = value as "overlay" | "direct";
|
|
106
|
+
return writeLabel();
|
|
107
|
+
} },
|
|
108
|
+
{ key: "providers", label: "Providers", value: `${providers.filter(p => !settings.disabledProviders.includes(p)).length} enabled`,
|
|
109
|
+
description: "Choose a provider to toggle visibility. Esc returns to options; Ctrl+S saves the complete draft.",
|
|
110
|
+
edit: async () => {
|
|
111
|
+
let selected: string | undefined;
|
|
112
|
+
for (;;) {
|
|
113
|
+
selected = await tableSelect(ctx, { title: "Models / Options / Providers", initialValue: selected, search: true, placement: settings.resultPlacement,
|
|
114
|
+
rows: providers.map(provider => ({ value: provider, label: `${settings.disabledProviders.includes(provider) ? "[ ]" : "[x]"} ${provider}` })),
|
|
115
|
+
hint: "↑↓ move Enter toggle Esc back" });
|
|
116
|
+
if (!selected) break;
|
|
117
|
+
const disabled = new Set(settings.disabledProviders);
|
|
118
|
+
if (disabled.has(selected)) disabled.delete(selected); else disabled.add(selected);
|
|
119
|
+
settings.disabledProviders = [...disabled].sort();
|
|
120
|
+
}
|
|
121
|
+
return `${providers.filter(p => !settings.disabledProviders.includes(p)).length} enabled`;
|
|
122
|
+
} },
|
|
123
|
+
],
|
|
124
|
+
onSave: async () => { saveSettings(settings); ctx.ui.notify("BMS options saved.", "info"); return true; },
|
|
125
|
+
});
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
async function loadModel(
|
|
@@ -165,6 +167,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
165
167
|
syncLocalConfig(pi, ctx);
|
|
166
168
|
});
|
|
167
169
|
|
|
170
|
+
const report = async (ctx: any, action: () => Promise<void>) => {
|
|
171
|
+
try { await action(); } catch (error) { ctx.ui.notify(`BMS: ${error}`, "error"); }
|
|
172
|
+
};
|
|
173
|
+
const addModel = async (ctx: any) => addFlow(ctx, async (provider, modelId) => {
|
|
174
|
+
if (syncLocalConfig(pi, ctx)) await report(ctx, () => setMainDefault(pi, ctx, provider, modelId));
|
|
175
|
+
});
|
|
176
|
+
pi.registerCommand("bms-options", {
|
|
177
|
+
description: "Configure BMS result placement, write mode, and provider visibility",
|
|
178
|
+
handler: async (_args, ctx: any) => {
|
|
179
|
+
if (ctx.mode !== "tui") { ctx.ui.notify("/bms-options needs an interactive terminal.", "warning"); return; }
|
|
180
|
+
await report(ctx, async () => {
|
|
181
|
+
const config = syncLocalConfig(pi, ctx);
|
|
182
|
+
if (config) await optionsFlow(ctx, config);
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
pi.registerCommand("bms-defaults", {
|
|
187
|
+
description: "Configure Pi and subagent default models and effort levels",
|
|
188
|
+
handler: async (_args, ctx: any) => {
|
|
189
|
+
if (ctx.mode !== "tui") { ctx.ui.notify("/bms-defaults needs an interactive terminal.", "warning"); return; }
|
|
190
|
+
await report(ctx, async () => {
|
|
191
|
+
const config = syncLocalConfig(pi, ctx);
|
|
192
|
+
if (config) await defaultsFlow(pi, ctx, config);
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
168
197
|
pi.registerCommand("bms", {
|
|
169
198
|
description: "Better model select — searchable, usage-ranked model picker",
|
|
170
199
|
handler: async (_args, ctx: any) => {
|
|
@@ -173,7 +202,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
173
202
|
return;
|
|
174
203
|
}
|
|
175
204
|
|
|
176
|
-
//
|
|
205
|
+
// Preserve search and selection while opening submenus.
|
|
206
|
+
const pickerState: { query?: string; selectedValue?: string } = {};
|
|
177
207
|
for (;;) {
|
|
178
208
|
const config = syncLocalConfig(pi, ctx);
|
|
179
209
|
if (!config) return;
|
|
@@ -200,41 +230,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
200
230
|
"The effective model configuration has no models. Add one now?",
|
|
201
231
|
);
|
|
202
232
|
if (!add) return;
|
|
203
|
-
await
|
|
233
|
+
await report(ctx, () => addModel(ctx));
|
|
204
234
|
continue;
|
|
205
235
|
}
|
|
206
236
|
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const picker = new ModelPicker(
|
|
216
|
-
rows,
|
|
217
|
-
theme,
|
|
218
|
-
() => tui.requestRender(),
|
|
219
|
-
(a: PickerAction) => done(a),
|
|
220
|
-
);
|
|
221
|
-
picker.focused = true;
|
|
222
|
-
container.addChild(picker);
|
|
223
|
-
container.addChild(new DynamicBorder(accent));
|
|
224
|
-
|
|
225
|
-
return {
|
|
226
|
-
render: (w: number) => container.render(w),
|
|
227
|
-
invalidate: () => container.invalidate(),
|
|
228
|
-
handleInput: (data: string) => picker.handleInput(data),
|
|
229
|
-
};
|
|
230
|
-
});
|
|
237
|
+
try {
|
|
238
|
+
const preference = piPreference(readSettings(settingsPath(ctx.cwd, "user")), ctx.isProjectTrusted?.() ? readSettings(settingsPath(ctx.cwd, "project")) : {});
|
|
239
|
+
for (const row of rows) {
|
|
240
|
+
row.current = ctx.model?.provider === row.provider && ctx.model?.id === row.modelId;
|
|
241
|
+
row.default = preference.provider === row.provider && preference.model === row.modelId;
|
|
242
|
+
}
|
|
243
|
+
} catch (error) { ctx.ui.notify(`Cannot read defaults: ${error}`, "warning"); }
|
|
244
|
+
const action = await pickerDialog(ctx, rows, "Models", false, { placement: settings.resultPlacement, state: pickerState });
|
|
231
245
|
|
|
232
246
|
switch (action?.kind) {
|
|
233
247
|
case "load":
|
|
234
248
|
await loadModel(pi, ctx, action.provider, action.modelId);
|
|
235
249
|
return;
|
|
250
|
+
case "set-default":
|
|
251
|
+
await report(ctx, () => setMainDefault(pi, ctx, action.provider, action.modelId));
|
|
252
|
+
break;
|
|
253
|
+
case "defaults":
|
|
254
|
+
await report(ctx, () => defaultsFlow(pi, ctx, config));
|
|
255
|
+
break;
|
|
236
256
|
case "add":
|
|
237
|
-
await
|
|
257
|
+
await report(ctx, () => addModel(ctx));
|
|
238
258
|
break;
|
|
239
259
|
case "update":
|
|
240
260
|
await updateFlow(
|
|
@@ -256,10 +276,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
256
276
|
await optionsFlow(ctx, config);
|
|
257
277
|
break;
|
|
258
278
|
case "reset": {
|
|
259
|
-
const ok = await ctx
|
|
260
|
-
"Reset usage
|
|
261
|
-
"
|
|
262
|
-
|
|
279
|
+
const ok = await confirmAction(ctx, {
|
|
280
|
+
title: "Models / Reset usage",
|
|
281
|
+
lines: ["Clear all BMS model selection counts and last-used times.", "Ranking returns to alphabetical order. Models and defaults are unchanged."],
|
|
282
|
+
confirmLabel: "Reset usage",
|
|
283
|
+
});
|
|
263
284
|
if (ok) {
|
|
264
285
|
saveUsage({ version: 1, counts: {}, lastUsed: {} });
|
|
265
286
|
ctx.ui.notify("Usage statistics cleared.", "info");
|
|
@@ -276,6 +297,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
276
297
|
// Standalone add, kept for muscle memory and non-interactive convenience.
|
|
277
298
|
pi.registerCommand("bms-add", {
|
|
278
299
|
description: "Add a model from a provider's endpoint using the configured write mode",
|
|
279
|
-
handler: async (_args, ctx: any) =>
|
|
300
|
+
handler: async (_args, ctx: any) => report(ctx, () => addModel(ctx)),
|
|
280
301
|
});
|
|
281
302
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Optional, isolated adapter: pi-subagents currently has no public discovery RPC.
|
|
2
|
+
// Never vendor a second agent-discovery implementation or execute an agent to list roles.
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
export interface AgentRole {
|
|
10
|
+
name: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
thinking?: string | false;
|
|
13
|
+
runner?: unknown;
|
|
14
|
+
source?: string;
|
|
15
|
+
modelSource?: unknown;
|
|
16
|
+
}
|
|
17
|
+
export interface AgentDiscovery {
|
|
18
|
+
roles: AgentRole[];
|
|
19
|
+
projectRoot: string;
|
|
20
|
+
warning?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Resolve the owning package, not the directory of its possibly bundled entry point. */
|
|
23
|
+
export function subagentsPackageRoot(entry: string): string {
|
|
24
|
+
let directory = dirname(entry);
|
|
25
|
+
for (;;) {
|
|
26
|
+
try {
|
|
27
|
+
const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
28
|
+
if (manifest.name === "pi-subagents") return directory;
|
|
29
|
+
} catch { /* keep looking above a bundle or other entry-point directory */ }
|
|
30
|
+
const parent = dirname(directory);
|
|
31
|
+
if (parent === directory) throw new Error("Cannot locate pi-subagents package root");
|
|
32
|
+
directory = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function discoverRoles(cwd: string, pi: any, provider?: string, trusted = false): Promise<AgentDiscovery> {
|
|
36
|
+
try {
|
|
37
|
+
// Resolve only from this extension or the user's trusted Pi package directory.
|
|
38
|
+
const roots = [import.meta.url, pathToFileURL(join(getAgentDir(), "npm", "package.json")).href];
|
|
39
|
+
let entry: string | undefined;
|
|
40
|
+
for (const root of roots) {
|
|
41
|
+
try { entry = createRequire(root).resolve("pi-subagents"); break; } catch { /* optional package */ }
|
|
42
|
+
}
|
|
43
|
+
if (!entry) throw new Error("pi-subagents is not resolvable from BMS or the Pi npm package directory");
|
|
44
|
+
const base = subagentsPackageRoot(entry);
|
|
45
|
+
const api = await import(pathToFileURL(join(base, "src/agents/agents.js")).href);
|
|
46
|
+
if (typeof api.discoverAgents !== "function") throw new Error("Unsupported pi-subagents discovery API");
|
|
47
|
+
api.clearAgentDiscoveryCache?.();
|
|
48
|
+
const result = api.discoverAgents(cwd, trusted ? "both" : "user", provider, { projectTrusted: trusted });
|
|
49
|
+
let roles: AgentRole[] = result.agents;
|
|
50
|
+
try {
|
|
51
|
+
const runtime = await import(pathToFileURL(join(base, "src/agents/runtime-agent-registry.js")).href);
|
|
52
|
+
roles = runtime.mergeRuntimeAgents(pi, result, undefined, { cwd, scope: trusted ? "both" : "user", preferredModelProvider: provider, projectTrusted: trusted }).agents;
|
|
53
|
+
} catch { /* older versions have filesystem agents only */ }
|
|
54
|
+
// Native Pi and subagents may choose different roots. Use the subagents settings root for agents.
|
|
55
|
+
const projectRoot = api.findConfiguredProjectRoot?.(cwd) ?? cwd;
|
|
56
|
+
return { roles: [...roles].sort((a, b) => a.name.localeCompare(b.name)), projectRoot };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return { roles: [], projectRoot: cwd, warning: `Agent discovery unavailable: ${error}. Pi defaults remain available.` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { rankModels, type RankedModel } from "./picker.ts";
|
|
2
|
+
import type { ModelsConfig } from "./store.ts";
|
|
3
|
+
|
|
4
|
+
/** Assignment menus include authenticated registry models, not only custom models.json entries. */
|
|
5
|
+
export function rankModelsForDefaults(config: ModelsConfig, ctx: any): RankedModel[] {
|
|
6
|
+
const rows = rankModels(config);
|
|
7
|
+
const seen = new Set(rows.map(row => `${row.provider}/${row.modelId}`));
|
|
8
|
+
for (const model of ctx.modelRegistry.getAvailable?.() ?? []) {
|
|
9
|
+
const key = `${model.provider}/${model.id}`;
|
|
10
|
+
if (seen.has(key)) continue;
|
|
11
|
+
seen.add(key);
|
|
12
|
+
rows.push({ provider: model.provider, modelId: model.id, label: model.name ?? model.id, count: 0, lastUsed: 0, reasoning: model.reasoning, contextWindow: model.contextWindow });
|
|
13
|
+
}
|
|
14
|
+
return rows;
|
|
15
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Defaults use native Pi settings. BMS capability hints live separately from models.json.
|
|
2
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
export const EFFORTS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
8
|
+
export type Effort = typeof EFFORTS[number];
|
|
9
|
+
export type Scope = "user" | "project";
|
|
10
|
+
export type JsonObject = Record<string, any>;
|
|
11
|
+
export function isEffort(value: unknown): value is Effort {
|
|
12
|
+
return typeof value === "string" && (EFFORTS as readonly string[]).includes(value);
|
|
13
|
+
}
|
|
14
|
+
export function object(value: unknown): JsonObject {
|
|
15
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a JSON object");
|
|
16
|
+
return value as JsonObject;
|
|
17
|
+
}
|
|
18
|
+
export function readSettings(path: string): JsonObject {
|
|
19
|
+
if (!existsSync(path)) return {};
|
|
20
|
+
return object(JSON.parse(readFileSync(path, "utf8")));
|
|
21
|
+
}
|
|
22
|
+
/** Fail closed on malformed files; serialize BMS writers and re-read immediately before patching. */
|
|
23
|
+
export function patchSettings(path: string, patch: (settings: JsonObject) => void): void {
|
|
24
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
25
|
+
const lock = `${path}.bms-lock`;
|
|
26
|
+
const fd = openSync(lock, "wx", 0o600);
|
|
27
|
+
const tmp = `${path}.bms-${randomUUID()}`;
|
|
28
|
+
try {
|
|
29
|
+
const settings = readSettings(path);
|
|
30
|
+
patch(settings);
|
|
31
|
+
writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n", { mode: 0o600 });
|
|
32
|
+
renameSync(tmp, path);
|
|
33
|
+
} finally {
|
|
34
|
+
if (existsSync(tmp)) unlinkSync(tmp);
|
|
35
|
+
closeSync(fd);
|
|
36
|
+
unlinkSync(lock);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function settingsPath(cwd: string, scope: Scope, agentDir = getAgentDir()): string {
|
|
40
|
+
return scope === "user" ? join(agentDir, "settings.json") : join(cwd, ".pi", "settings.json");
|
|
41
|
+
}
|
|
42
|
+
export interface Capability {
|
|
43
|
+
levels: Effort[];
|
|
44
|
+
source: "provider" | "manual" | "pi";
|
|
45
|
+
}
|
|
46
|
+
export function metadataPath(): string { return join(getAgentDir(), "bms-model-metadata.json"); }
|
|
47
|
+
export function normalizeEfforts(raw: unknown): Effort[] | undefined {
|
|
48
|
+
if (!Array.isArray(raw) || !raw.length || !raw.every(isEffort)) return undefined;
|
|
49
|
+
return EFFORTS.filter(level => raw.includes(level));
|
|
50
|
+
}
|
|
51
|
+
export function capabilities(key: string, model?: { reasoning?: boolean; thinkingLevelMap?: Record<string, unknown> }, path = metadataPath()): { levels: Effort[]; source: string } {
|
|
52
|
+
const stored = readSettings(path)[key];
|
|
53
|
+
const levels = normalizeEfforts(stored?.levels);
|
|
54
|
+
if (levels) return { levels, source: stored.source === "provider" ? "Provider metadata" : stored.source === "manual" ? "Manual override" : "Pi standard levels; provider support unverified" };
|
|
55
|
+
if (model?.reasoning === false) return { levels: ["off"], source: "Non-reasoning model" };
|
|
56
|
+
// Native maps explicitly mark unsupported Pi levels with null.
|
|
57
|
+
if (model?.thinkingLevelMap) {
|
|
58
|
+
const supported = EFFORTS.filter(level => model.thinkingLevelMap![level] !== null);
|
|
59
|
+
if (supported.length) return { levels: supported, source: "Pi model capability map" };
|
|
60
|
+
}
|
|
61
|
+
return { levels: [...EFFORTS], source: "Pi standard levels; provider support unverified" };
|
|
62
|
+
}
|
|
63
|
+
export function saveCapability(key: string, capability: Capability, path = metadataPath()): void {
|
|
64
|
+
if (!normalizeEfforts(capability.levels)) throw new Error("Choose at least one valid effort level");
|
|
65
|
+
patchSettings(path, data => { data[key] = capability; });
|
|
66
|
+
}
|
|
67
|
+
export function setPiDefault(data: JsonObject, provider: string, model: string, effort: Effort | undefined): void {
|
|
68
|
+
data.defaultProvider = provider;
|
|
69
|
+
data.defaultModel = model;
|
|
70
|
+
if (effort === undefined) delete data.defaultThinkingLevel;
|
|
71
|
+
else data.defaultThinkingLevel = effort;
|
|
72
|
+
// A per-model startup preference would otherwise beat the newly chosen default.
|
|
73
|
+
if (data.modelThinkingLevels !== undefined) {
|
|
74
|
+
const levels = object(data.modelThinkingLevels);
|
|
75
|
+
delete levels[`${provider}/${model}`];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function setRoleField(data: JsonObject, role: string | undefined, field: "model" | "thinking", value: string | false | undefined): void {
|
|
79
|
+
if (role && ["__proto__", "constructor", "prototype"].includes(role)) throw new Error("Invalid agent name");
|
|
80
|
+
const sub = data.subagents === undefined ? data.subagents = {} : object(data.subagents);
|
|
81
|
+
if (!role) {
|
|
82
|
+
const key = field === "model" ? "defaultModel" : "defaultThinking";
|
|
83
|
+
if (value === undefined) delete sub[key];
|
|
84
|
+
else sub[key] = value;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const overrides = sub.agentOverrides === undefined ? sub.agentOverrides = {} : object(sub.agentOverrides);
|
|
88
|
+
const entry = Object.hasOwn(overrides, role) ? object(overrides[role]) : overrides[role] = {};
|
|
89
|
+
if (value === undefined) delete entry[field];
|
|
90
|
+
else entry[field] = value;
|
|
91
|
+
if (!Object.keys(entry).length) delete overrides[role];
|
|
92
|
+
}
|
|
93
|
+
/** Settings-only projection for the menu, not a claim about a running child. */
|
|
94
|
+
export function roleField(user: JsonObject, project: JsonObject, provider: string | undefined, role: string | undefined, field: "model" | "thinking"): { value: unknown; source: string } {
|
|
95
|
+
for (const [scope, data] of [["Project", project], ["User", user]] as const) {
|
|
96
|
+
const sub = data.subagents;
|
|
97
|
+
if (role) {
|
|
98
|
+
const scoped = provider && sub?.agentOverridesByProvider?.[provider]?.[role];
|
|
99
|
+
if (scoped && Object.hasOwn(scoped, field)) return { value: scoped[field], source: `${scope}, provider override` };
|
|
100
|
+
const regular = sub?.agentOverrides?.[role];
|
|
101
|
+
if (regular && Object.hasOwn(regular, field)) return { value: regular[field], source: `${scope} role override` };
|
|
102
|
+
} else {
|
|
103
|
+
const key = field === "model" ? "defaultModel" : "defaultThinking";
|
|
104
|
+
if (sub && Object.hasOwn(sub, key)) return { value: sub[key], source: `${scope} fallback` };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { value: undefined, source: role ? "Agent-defined / fallback" : "Parent session" };
|
|
108
|
+
}
|