@oh-my-pi/pi-coding-agent 15.10.1 → 15.10.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 +67 -0
- package/dist/types/cli/startup-cwd.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-provider-priority.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/config/settings.d.ts +7 -2
- package/dist/types/debug/report-bundle.d.ts +3 -0
- package/dist/types/edit/file-snapshot-store.d.ts +18 -10
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -1
- package/dist/types/lsp/client.d.ts +10 -0
- package/dist/types/main.d.ts +3 -9
- package/dist/types/mcp/tool-bridge.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/controllers/event-controller.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/magic-keywords.d.ts +1 -1
- package/dist/types/modes/markdown-prose.d.ts +1 -1
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/workflow.d.ts +3 -3
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +5 -2
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/tools/eval.d.ts +8 -0
- package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
- package/dist/types/tools/github-cache.d.ts +12 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/yield.d.ts +8 -0
- package/package.json +9 -9
- package/src/cli/args.ts +3 -1
- package/src/cli/dry-balance-cli.ts +2 -4
- package/src/cli/startup-cwd.ts +68 -0
- package/src/commands/launch.ts +3 -0
- package/src/commit/model-selection.ts +3 -2
- package/src/config/model-provider-priority.ts +55 -0
- package/src/config/model-registry.ts +4 -22
- package/src/config/model-resolver.ts +39 -7
- package/src/config/settings.ts +86 -41
- package/src/debug/index.ts +8 -0
- package/src/debug/raw-sse-buffer.ts +7 -4
- package/src/debug/report-bundle.ts +9 -0
- package/src/edit/file-snapshot-store.ts +33 -1
- package/src/edit/hashline/filesystem.ts +2 -1
- package/src/eval/__tests__/llm-bridge.test.ts +20 -0
- package/src/eval/js/context-manager.ts +32 -15
- package/src/eval/llm-bridge.ts +14 -3
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/executor.ts +23 -11
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/lsp/client.ts +23 -11
- package/src/lsp/config.ts +11 -1
- package/src/lsp/index.ts +61 -9
- package/src/main.ts +91 -65
- package/src/mcp/tool-bridge.ts +2 -0
- package/src/memories/index.ts +2 -2
- package/src/modes/components/custom-editor.ts +143 -111
- package/src/modes/components/model-selector.ts +59 -13
- package/src/modes/components/oauth-selector.ts +33 -7
- package/src/modes/components/status-line.ts +19 -4
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/user-message.ts +1 -1
- package/src/modes/controllers/event-controller.ts +26 -0
- package/src/modes/controllers/input-controller.ts +46 -7
- package/src/modes/interactive-mode.ts +107 -20
- package/src/modes/magic-keywords.ts +1 -1
- package/src/modes/markdown-prose.ts +1 -1
- package/src/modes/theme/shimmer.ts +20 -9
- package/src/modes/types.ts +3 -0
- package/src/modes/workflow.ts +10 -10
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/eval.md +2 -1
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +26 -9
- package/src/session/agent-session.ts +37 -12
- package/src/session/auth-storage.ts +2 -0
- package/src/session/session-manager.ts +96 -23
- package/src/task/executor.ts +71 -36
- package/src/task/render.ts +3 -4
- package/src/tools/bash.ts +7 -0
- package/src/tools/browser/tab-supervisor.ts +13 -1
- package/src/tools/browser/tab-worker.ts +33 -4
- package/src/tools/eval.ts +13 -2
- package/src/tools/find.ts +7 -0
- package/src/tools/gh-cache-invalidation.ts +200 -0
- package/src/tools/github-cache.ts +25 -0
- package/src/tools/inspect-image.ts +2 -2
- package/src/tools/path-utils.ts +28 -2
- package/src/tools/plan-mode-guard.ts +52 -7
- package/src/tools/read.ts +25 -12
- package/src/tools/search.ts +38 -3
- package/src/tools/write.ts +2 -2
- package/src/tools/yield.ts +10 -1
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/enhanced-paste.ts +30 -2
- package/src/web/search/providers/codex.ts +37 -8
package/src/cli/args.ts
CHANGED
|
@@ -109,6 +109,8 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
|
|
|
109
109
|
result.version = true;
|
|
110
110
|
} else if (arg === "--allow-home") {
|
|
111
111
|
result.allowHome = true;
|
|
112
|
+
} else if (arg === "--cwd" && i + 1 < args.length) {
|
|
113
|
+
result.cwd = args[++i];
|
|
112
114
|
} else if (arg === "--mode" && i + 1 < args.length) {
|
|
113
115
|
const mode = args[++i];
|
|
114
116
|
if (mode === "text" || mode === "json" || mode === "rpc" || mode === "acp" || mode === "rpc-ui") {
|
|
@@ -258,7 +260,7 @@ export function getExtraHelpText(): string {
|
|
|
258
260
|
NODE_EXTRA_CA_CERTS - CA bundle path (or inline PEM) for server certificate validation
|
|
259
261
|
OPENAI_API_KEY - OpenAI GPT models
|
|
260
262
|
GEMINI_API_KEY - Google Gemini models
|
|
261
|
-
|
|
263
|
+
COPILOT_GITHUB_TOKEN - GitHub Copilot
|
|
262
264
|
|
|
263
265
|
${chalk.dim("# Additional LLM Providers")}
|
|
264
266
|
AZURE_OPENAI_API_KEY - Azure OpenAI models
|
|
@@ -19,7 +19,7 @@ import type { CanonicalModelVariant } from "../config/model-equivalence";
|
|
|
19
19
|
import { type CanonicalModelQueryOptions, ModelRegistry } from "../config/model-registry";
|
|
20
20
|
import {
|
|
21
21
|
formatModelString,
|
|
22
|
-
|
|
22
|
+
getModelMatchPreferences,
|
|
23
23
|
resolveAllowedModels,
|
|
24
24
|
resolveCliModel,
|
|
25
25
|
resolveModelRoleValue,
|
|
@@ -542,9 +542,7 @@ async function resolveDryBalanceModel(
|
|
|
542
542
|
settings: Settings | undefined,
|
|
543
543
|
randomSessionId: () => string,
|
|
544
544
|
): Promise<{ model: Model<Api>; warning?: string }> {
|
|
545
|
-
const preferences
|
|
546
|
-
usageOrder: settings?.getStorage()?.getModelUsageOrder(),
|
|
547
|
-
};
|
|
545
|
+
const preferences = getModelMatchPreferences(settings);
|
|
548
546
|
if (modelSelector) {
|
|
549
547
|
const resolved = resolveCliModel({
|
|
550
548
|
cliModel: modelSelector,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { getProjectDir, normalizePathForComparison, setProjectDir } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import type { Args } from "./args";
|
|
6
|
+
|
|
7
|
+
async function maybeAutoChdir(parsed: Args): Promise<void> {
|
|
8
|
+
if (parsed.allowHome || parsed.cwd) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const home = os.homedir();
|
|
13
|
+
if (!home) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const normalizePath = normalizePathForComparison;
|
|
18
|
+
|
|
19
|
+
const cwd = normalizePath(getProjectDir());
|
|
20
|
+
const normalizedHome = normalizePath(home);
|
|
21
|
+
if (cwd !== normalizedHome) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isDirectory = async (p: string) => {
|
|
26
|
+
try {
|
|
27
|
+
const s = await fs.stat(p);
|
|
28
|
+
return s.isDirectory();
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const candidates = [path.join(home, "tmp"), "/tmp", "/var/tmp"];
|
|
35
|
+
for (const candidate of candidates) {
|
|
36
|
+
try {
|
|
37
|
+
if (!(await isDirectory(candidate))) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
setProjectDir(candidate);
|
|
41
|
+
return;
|
|
42
|
+
} catch {
|
|
43
|
+
// Try next candidate.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const fallback = os.tmpdir();
|
|
49
|
+
if (fallback && normalizePath(fallback) !== cwd && (await isDirectory(fallback))) {
|
|
50
|
+
setProjectDir(fallback);
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// Ignore fallback errors.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function applyStartupCwd(parsed: Args): Promise<void> {
|
|
58
|
+
if (parsed.cwd) {
|
|
59
|
+
setProjectDir(parsed.cwd);
|
|
60
|
+
// setProjectDir resolves the (possibly relative) target against the launch
|
|
61
|
+
// cwd and chdirs into it. Re-sync parsed.cwd to the resolved absolute path
|
|
62
|
+
// so downstream consumers (buildSessionOptions, settings/discovery, session
|
|
63
|
+
// persistence) don't re-resolve a relative string against the new cwd.
|
|
64
|
+
parsed.cwd = getProjectDir();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await maybeAutoChdir(parsed);
|
|
68
|
+
}
|
package/src/commands/launch.ts
CHANGED
|
@@ -49,6 +49,9 @@ export default class Index extends Command {
|
|
|
49
49
|
"allow-home": Flags.boolean({
|
|
50
50
|
description: "Allow starting in ~ without auto-switching to a temp dir",
|
|
51
51
|
}),
|
|
52
|
+
cwd: Flags.string({
|
|
53
|
+
description: "Directory to start in (overrides the launch cwd)",
|
|
54
|
+
}),
|
|
52
55
|
mode: Flags.string({
|
|
53
56
|
description: "Output mode: text (default), json, rpc, or rpc-ui",
|
|
54
57
|
options: ["text", "json", "rpc", "acp", "rpc-ui"],
|
|
@@ -3,6 +3,7 @@ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
|
|
|
3
3
|
import type { ApiKeyResolverRegistry } from "../config/api-key-resolver";
|
|
4
4
|
import { MODEL_ROLE_IDS } from "../config/model-registry";
|
|
5
5
|
import {
|
|
6
|
+
getModelMatchPreferences,
|
|
6
7
|
type ModelLookupRegistry,
|
|
7
8
|
parseModelPattern,
|
|
8
9
|
resolveModelRoleValue,
|
|
@@ -33,7 +34,7 @@ export async function resolvePrimaryModel(
|
|
|
33
34
|
modelRegistry: CommitModelRegistry,
|
|
34
35
|
): Promise<ResolvedCommitModel> {
|
|
35
36
|
const available = modelRegistry.getAvailable();
|
|
36
|
-
const matchPreferences =
|
|
37
|
+
const matchPreferences = getModelMatchPreferences(settings);
|
|
37
38
|
const resolved = override
|
|
38
39
|
? resolveModelRoleValue(override, available, { settings, matchPreferences, modelRegistry })
|
|
39
40
|
: resolveRoleSelection(["commit", "smol", ...MODEL_ROLE_IDS], settings, available, modelRegistry);
|
|
@@ -73,7 +74,7 @@ export async function resolveSmolModel(
|
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
const matchPreferences =
|
|
77
|
+
const matchPreferences = getModelMatchPreferences(settings);
|
|
77
78
|
for (const pattern of MODEL_PRIO.smol) {
|
|
78
79
|
const candidate = parseModelPattern(pattern, available, matchPreferences, { modelRegistry }).model;
|
|
79
80
|
if (!candidate) continue;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const DEFAULT_MODEL_PROVIDER_ORDER = [
|
|
2
|
+
// First-party / native account providers. Prefer these over relays when the
|
|
3
|
+
// same upstream model is available in more than one place.
|
|
4
|
+
"openai-codex",
|
|
5
|
+
"anthropic",
|
|
6
|
+
"openai",
|
|
7
|
+
"google-gemini-cli",
|
|
8
|
+
"google",
|
|
9
|
+
"google-vertex",
|
|
10
|
+
"kimi-code",
|
|
11
|
+
"moonshot",
|
|
12
|
+
"qwen-portal",
|
|
13
|
+
"zai",
|
|
14
|
+
"xai-oauth",
|
|
15
|
+
"xai",
|
|
16
|
+
"mistral",
|
|
17
|
+
"deepseek",
|
|
18
|
+
"groq",
|
|
19
|
+
|
|
20
|
+
// High-quality aggregators / hosted inference providers.
|
|
21
|
+
"fireworks",
|
|
22
|
+
"cerebras",
|
|
23
|
+
"openrouter",
|
|
24
|
+
"together",
|
|
25
|
+
|
|
26
|
+
// Generic gateways and editor/proxy providers. These are useful when picked
|
|
27
|
+
// explicitly, but should not win ambiguous automatic role selection.
|
|
28
|
+
"alibaba-coding-plan",
|
|
29
|
+
"google-antigravity",
|
|
30
|
+
"opencode-zen",
|
|
31
|
+
"gitlab-duo",
|
|
32
|
+
"opencode-go",
|
|
33
|
+
"kilo",
|
|
34
|
+
"vercel-ai-gateway",
|
|
35
|
+
"cloudflare-ai-gateway",
|
|
36
|
+
"nanogpt",
|
|
37
|
+
"github-copilot",
|
|
38
|
+
] as const;
|
|
39
|
+
|
|
40
|
+
function addProviderRank(rank: Map<string, number>, provider: string): void {
|
|
41
|
+
const normalized = provider.trim().toLowerCase();
|
|
42
|
+
if (!normalized || rank.has(normalized)) return;
|
|
43
|
+
rank.set(normalized, rank.size);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildModelProviderPriorityRank(configuredProviderOrder?: readonly string[]): Map<string, number> {
|
|
47
|
+
const rank = new Map<string, number>();
|
|
48
|
+
for (const provider of configuredProviderOrder ?? []) {
|
|
49
|
+
addProviderRank(rank, provider);
|
|
50
|
+
}
|
|
51
|
+
for (const provider of DEFAULT_MODEL_PROVIDER_ORDER) {
|
|
52
|
+
addProviderRank(rank, provider);
|
|
53
|
+
}
|
|
54
|
+
return rank;
|
|
55
|
+
}
|
|
@@ -118,6 +118,7 @@ import {
|
|
|
118
118
|
getModelLikeIdSegments,
|
|
119
119
|
stripBracketedModelIdAffixes,
|
|
120
120
|
} from "./model-id-affixes";
|
|
121
|
+
import { buildModelProviderPriorityRank } from "./model-provider-priority";
|
|
121
122
|
import {
|
|
122
123
|
type ModelOverride,
|
|
123
124
|
type ModelsConfig,
|
|
@@ -2208,27 +2209,8 @@ export class ModelRegistry {
|
|
|
2208
2209
|
});
|
|
2209
2210
|
}
|
|
2210
2211
|
|
|
2211
|
-
#providerRank(
|
|
2212
|
-
|
|
2213
|
-
const result = new Map<string, number>();
|
|
2214
|
-
let nextRank = 0;
|
|
2215
|
-
for (const provider of configuredProviders) {
|
|
2216
|
-
const normalized = provider.trim().toLowerCase();
|
|
2217
|
-
if (!normalized || result.has(normalized)) {
|
|
2218
|
-
continue;
|
|
2219
|
-
}
|
|
2220
|
-
result.set(normalized, nextRank);
|
|
2221
|
-
nextRank += 1;
|
|
2222
|
-
}
|
|
2223
|
-
for (const model of models) {
|
|
2224
|
-
const normalized = model.provider.toLowerCase();
|
|
2225
|
-
if (result.has(normalized)) {
|
|
2226
|
-
continue;
|
|
2227
|
-
}
|
|
2228
|
-
result.set(normalized, nextRank);
|
|
2229
|
-
nextRank += 1;
|
|
2230
|
-
}
|
|
2231
|
-
return result;
|
|
2212
|
+
#providerRank(): Map<string, number> {
|
|
2213
|
+
return buildModelProviderPriorityRank(getConfiguredProviderOrderFromSettings());
|
|
2232
2214
|
}
|
|
2233
2215
|
|
|
2234
2216
|
#resolveCanonicalVariant(
|
|
@@ -2238,7 +2220,7 @@ export class ModelRegistry {
|
|
|
2238
2220
|
if (variants.length === 0) {
|
|
2239
2221
|
return undefined;
|
|
2240
2222
|
}
|
|
2241
|
-
const providerRank = this.#providerRank(
|
|
2223
|
+
const providerRank = this.#providerRank();
|
|
2242
2224
|
const modelOrder = new Map<string, number>();
|
|
2243
2225
|
for (let index = 0; index < allCandidates.length; index += 1) {
|
|
2244
2226
|
modelOrder.set(formatCanonicalVariantSelector(allCandidates[index]!), index);
|
|
@@ -17,6 +17,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import MODEL_PRIO from "../priority.json" with { type: "json" };
|
|
19
19
|
import { parseThinkingLevel, resolveThinkingLevelForModel } from "../thinking";
|
|
20
|
+
import { buildModelProviderPriorityRank } from "./model-provider-priority";
|
|
20
21
|
import { isAuthenticated, kNoAuth, MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
|
|
21
22
|
import type { Settings } from "./settings";
|
|
22
23
|
|
|
@@ -179,7 +180,9 @@ export function resolveProviderModelReference(
|
|
|
179
180
|
export interface ModelMatchPreferences {
|
|
180
181
|
/** Most-recently-used model keys (provider/modelId) to prefer when ambiguous. */
|
|
181
182
|
usageOrder?: string[];
|
|
182
|
-
/**
|
|
183
|
+
/** Provider precedence used for ambiguous unqualified model patterns. */
|
|
184
|
+
providerOrder?: readonly string[];
|
|
185
|
+
/** Providers to deprioritize when no recent usage or provider priority is available. */
|
|
183
186
|
deprioritizeProviders?: string[];
|
|
184
187
|
}
|
|
185
188
|
|
|
@@ -194,6 +197,7 @@ type RestorableModelRegistry = Pick<ModelRegistry, "getAvailable" | "find" | "ge
|
|
|
194
197
|
interface ModelPreferenceContext {
|
|
195
198
|
modelUsageRank: Map<string, number>;
|
|
196
199
|
providerUsageRank: Map<string, number>;
|
|
200
|
+
providerPriorityRank: Map<string, number>;
|
|
197
201
|
deprioritizedProviders: Set<string>;
|
|
198
202
|
modelOrder: Map<string, number>;
|
|
199
203
|
}
|
|
@@ -215,14 +219,35 @@ function buildPreferenceContext(
|
|
|
215
219
|
providerUsageRank.set(parsed.provider, i);
|
|
216
220
|
}
|
|
217
221
|
}
|
|
218
|
-
|
|
219
|
-
const deprioritizedProviders = new Set(preferences?.deprioritizeProviders ?? [
|
|
222
|
+
const providerPriorityRank = buildModelProviderPriorityRank(preferences?.providerOrder);
|
|
223
|
+
const deprioritizedProviders = new Set(preferences?.deprioritizeProviders ?? []);
|
|
220
224
|
const modelOrder = new Map<string, number>();
|
|
221
225
|
for (let i = 0; i < availableModels.length; i += 1) {
|
|
222
226
|
modelOrder.set(formatModelString(availableModels[i]), i);
|
|
223
227
|
}
|
|
224
228
|
|
|
225
|
-
return { modelUsageRank, providerUsageRank, deprioritizedProviders, modelOrder };
|
|
229
|
+
return { modelUsageRank, providerUsageRank, providerPriorityRank, deprioritizedProviders, modelOrder };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function getModelMatchPreferences(
|
|
233
|
+
settings?: Partial<Pick<Settings, "get" | "getStorage">>,
|
|
234
|
+
): ModelMatchPreferences {
|
|
235
|
+
return {
|
|
236
|
+
usageOrder: settings?.getStorage?.()?.getModelUsageOrder(),
|
|
237
|
+
providerOrder: settings?.get?.("modelProviderOrder"),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function mergeModelMatchPreferences(
|
|
242
|
+
settings: Settings | undefined,
|
|
243
|
+
preferences: ModelMatchPreferences | undefined,
|
|
244
|
+
): ModelMatchPreferences {
|
|
245
|
+
const settingsPreferences = getModelMatchPreferences(settings);
|
|
246
|
+
return {
|
|
247
|
+
usageOrder: preferences?.usageOrder ?? settingsPreferences.usageOrder,
|
|
248
|
+
providerOrder: preferences?.providerOrder ?? settingsPreferences.providerOrder,
|
|
249
|
+
deprioritizeProviders: preferences?.deprioritizeProviders,
|
|
250
|
+
};
|
|
226
251
|
}
|
|
227
252
|
|
|
228
253
|
function pickPreferredModel(candidates: Model<Api>[], context: ModelPreferenceContext): Model<Api> {
|
|
@@ -236,6 +261,12 @@ function pickPreferredModel(candidates: Model<Api>[], context: ModelPreferenceCo
|
|
|
236
261
|
return (aUsage ?? Number.POSITIVE_INFINITY) - (bUsage ?? Number.POSITIVE_INFINITY);
|
|
237
262
|
}
|
|
238
263
|
|
|
264
|
+
const aProviderPriority = context.providerPriorityRank.get(a.provider.toLowerCase());
|
|
265
|
+
const bProviderPriority = context.providerPriorityRank.get(b.provider.toLowerCase());
|
|
266
|
+
if (aProviderPriority !== undefined || bProviderPriority !== undefined) {
|
|
267
|
+
return (aProviderPriority ?? Number.POSITIVE_INFINITY) - (bProviderPriority ?? Number.POSITIVE_INFINITY);
|
|
268
|
+
}
|
|
269
|
+
|
|
239
270
|
const aProviderUsage = context.providerUsageRank.get(a.provider);
|
|
240
271
|
const bProviderUsage = context.providerUsageRank.get(b.provider);
|
|
241
272
|
if (aProviderUsage !== undefined || bProviderUsage !== undefined) {
|
|
@@ -618,8 +649,9 @@ export function resolveModelRoleValue(
|
|
|
618
649
|
}
|
|
619
650
|
|
|
620
651
|
let warning: string | undefined;
|
|
652
|
+
const matchPreferences = mergeModelMatchPreferences(options?.settings, options?.matchPreferences);
|
|
621
653
|
for (const effectivePattern of effectivePatterns) {
|
|
622
|
-
const resolved = parseModelPattern(effectivePattern, availableModels,
|
|
654
|
+
const resolved = parseModelPattern(effectivePattern, availableModels, matchPreferences, {
|
|
623
655
|
modelRegistry: options?.modelRegistry,
|
|
624
656
|
});
|
|
625
657
|
if (resolved.model) {
|
|
@@ -720,7 +752,7 @@ export function resolveModelOverride(
|
|
|
720
752
|
): { model?: Model<Api>; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } {
|
|
721
753
|
if (modelPatterns.length === 0) return { explicitThinkingLevel: false };
|
|
722
754
|
const availableModels = modelRegistry.getAvailable();
|
|
723
|
-
const matchPreferences =
|
|
755
|
+
const matchPreferences = getModelMatchPreferences(settings);
|
|
724
756
|
for (const pattern of modelPatterns) {
|
|
725
757
|
const { model, thinkingLevel, explicitThinkingLevel } = resolveModelRoleValue(pattern, availableModels, {
|
|
726
758
|
settings,
|
|
@@ -800,7 +832,7 @@ export function resolveRoleSelection(
|
|
|
800
832
|
availableModels: Model<Api>[],
|
|
801
833
|
modelRegistry?: CanonicalModelRegistry,
|
|
802
834
|
): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
|
|
803
|
-
const matchPreferences =
|
|
835
|
+
const matchPreferences = getModelMatchPreferences(settings);
|
|
804
836
|
for (const role of roles) {
|
|
805
837
|
const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, {
|
|
806
838
|
settings,
|
package/src/config/settings.ts
CHANGED
|
@@ -72,7 +72,7 @@ export interface SettingsOptions {
|
|
|
72
72
|
/**
|
|
73
73
|
* Get a nested value from an object by path segments.
|
|
74
74
|
*/
|
|
75
|
-
function getByPath(obj: RawSettings, segments: string[]): unknown {
|
|
75
|
+
function getByPath(obj: RawSettings, segments: readonly string[]): unknown {
|
|
76
76
|
let current: unknown = obj;
|
|
77
77
|
for (const segment of segments) {
|
|
78
78
|
if (current === null || current === undefined || typeof current !== "object") {
|
|
@@ -83,6 +83,10 @@ function getByPath(obj: RawSettings, segments: string[]): unknown {
|
|
|
83
83
|
return current;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
const SETTING_PATH_SEGMENTS: Record<SettingPath, readonly string[]> = Object.fromEntries(
|
|
87
|
+
(Object.keys(SETTINGS_SCHEMA) as SettingPath[]).map(settingPath => [settingPath, settingPath.split(".")]),
|
|
88
|
+
) as unknown as Record<SettingPath, readonly string[]>;
|
|
89
|
+
|
|
86
90
|
/**
|
|
87
91
|
* Set a nested value in an object by path segments.
|
|
88
92
|
* Creates intermediate objects as needed.
|
|
@@ -196,6 +200,8 @@ export class Settings {
|
|
|
196
200
|
#overrides: RawSettings = {};
|
|
197
201
|
/** Merged view (global + project + overrides) */
|
|
198
202
|
#merged: RawSettings = {};
|
|
203
|
+
/** Cached resolved values from the merged view, including defaults/path scoping */
|
|
204
|
+
#resolvedCache = new Map<SettingPath, unknown>();
|
|
199
205
|
|
|
200
206
|
/** Paths modified during this session (for partial save) */
|
|
201
207
|
#modified = new Set<string>();
|
|
@@ -282,13 +288,15 @@ export class Settings {
|
|
|
282
288
|
* Returns the merged value from global + project + overrides, or the default.
|
|
283
289
|
*/
|
|
284
290
|
get<P extends SettingPath>(path: P): SettingValue<P> {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (value !== undefined) {
|
|
288
|
-
const pathScopedValue = resolvePathScopedStringArray(path, value, this.#cwd);
|
|
289
|
-
return (pathScopedValue ?? value) as SettingValue<P>;
|
|
291
|
+
if (this.#resolvedCache.has(path)) {
|
|
292
|
+
return this.#resolvedCache.get(path) as SettingValue<P>;
|
|
290
293
|
}
|
|
291
|
-
|
|
294
|
+
|
|
295
|
+
const value = getByPath(this.#merged, SETTING_PATH_SEGMENTS[path]);
|
|
296
|
+
const resolved =
|
|
297
|
+
value !== undefined ? (resolvePathScopedStringArray(path, value, this.#cwd) ?? value) : getDefault(path);
|
|
298
|
+
this.#resolvedCache.set(path, resolved);
|
|
299
|
+
return resolved as SettingValue<P>;
|
|
292
300
|
}
|
|
293
301
|
|
|
294
302
|
/**
|
|
@@ -302,6 +310,7 @@ export class Settings {
|
|
|
302
310
|
setByPath(this.#global, segments, value);
|
|
303
311
|
this.#modified.add(path);
|
|
304
312
|
this.#rebuildMerged();
|
|
313
|
+
const next = this.get(path);
|
|
305
314
|
this.#queueSave();
|
|
306
315
|
|
|
307
316
|
// Trigger hook if exists
|
|
@@ -309,21 +318,25 @@ export class Settings {
|
|
|
309
318
|
if (hook) {
|
|
310
319
|
hook(value, prev);
|
|
311
320
|
}
|
|
321
|
+
this.#fireEffectiveSettingChanged(path, next, prev);
|
|
312
322
|
}
|
|
313
323
|
|
|
314
324
|
/**
|
|
315
325
|
* Apply runtime overrides (not persisted).
|
|
316
326
|
*/
|
|
317
327
|
override<P extends SettingPath>(path: P, value: SettingValue<P>): void {
|
|
328
|
+
const prev = this.get(path);
|
|
318
329
|
const segments = path.split(".");
|
|
319
330
|
setByPath(this.#overrides, segments, value);
|
|
320
331
|
this.#rebuildMerged();
|
|
332
|
+
this.#fireEffectiveSettingChanged(path, this.get(path), prev);
|
|
321
333
|
}
|
|
322
334
|
|
|
323
335
|
/**
|
|
324
336
|
* Clear a runtime override.
|
|
325
337
|
*/
|
|
326
338
|
clearOverride(path: SettingPath): void {
|
|
339
|
+
const prev = this.get(path);
|
|
327
340
|
const segments = path.split(".");
|
|
328
341
|
let current = this.#overrides;
|
|
329
342
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
@@ -333,6 +346,14 @@ export class Settings {
|
|
|
333
346
|
}
|
|
334
347
|
delete current[segments[segments.length - 1]];
|
|
335
348
|
this.#rebuildMerged();
|
|
349
|
+
this.#fireEffectiveSettingChanged(path, this.get(path), prev);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#fireEffectiveSettingChanged(path: SettingPath, value: unknown, prev: unknown): void {
|
|
353
|
+
if (Object.is(value, prev)) return;
|
|
354
|
+
if (path === "statusLine.sessionAccent") {
|
|
355
|
+
statusLineSessionAccentSignal.fire();
|
|
356
|
+
}
|
|
336
357
|
}
|
|
337
358
|
|
|
338
359
|
/**
|
|
@@ -842,6 +863,7 @@ export class Settings {
|
|
|
842
863
|
#rebuildMerged(): void {
|
|
843
864
|
this.#merged = this.#deepMerge(this.#deepMerge({}, this.#global), this.#project);
|
|
844
865
|
this.#merged = this.#deepMerge(this.#merged, this.#overrides);
|
|
866
|
+
this.#resolvedCache.clear();
|
|
845
867
|
}
|
|
846
868
|
|
|
847
869
|
#fireAllHooks(): void {
|
|
@@ -885,6 +907,45 @@ export class Settings {
|
|
|
885
907
|
|
|
886
908
|
type SettingHook<P extends SettingPath> = (value: SettingValue<P>, prev: SettingValue<P>) => void;
|
|
887
909
|
|
|
910
|
+
/**
|
|
911
|
+
* Minimal change-notification primitive backing the exported `on*Changed`
|
|
912
|
+
* subscriptions. Holds a listener set, hands out unsubscribe closures, and
|
|
913
|
+
* isolates errors so a single throwing listener can't abort the rest or bubble
|
|
914
|
+
* out of `Settings.set()`.
|
|
915
|
+
*
|
|
916
|
+
* @typeParam A - argument tuple forwarded to each listener on `fire`.
|
|
917
|
+
*/
|
|
918
|
+
class SettingSignal<A extends unknown[] = []> {
|
|
919
|
+
#listeners = new Set<(...args: A) => void>();
|
|
920
|
+
|
|
921
|
+
constructor(private readonly label: string) {}
|
|
922
|
+
|
|
923
|
+
/** Subscribe `cb`; returns an unsubscribe function. */
|
|
924
|
+
on(cb: (...args: A) => void): () => void {
|
|
925
|
+
this.#listeners.add(cb);
|
|
926
|
+
return () => {
|
|
927
|
+
this.#listeners.delete(cb);
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Invoke every listener with `args`. Iterates a snapshot so a listener may
|
|
933
|
+
* (un)subscribe mid-fire without re-entrancy — the Hindsight backend
|
|
934
|
+
* re-registers the fresh state's listener on every rebuild — and wraps each
|
|
935
|
+
* call so a throwing listener is logged and skipped instead of aborting the
|
|
936
|
+
* rest.
|
|
937
|
+
*/
|
|
938
|
+
fire(...args: A): void {
|
|
939
|
+
for (const cb of [...this.#listeners]) {
|
|
940
|
+
try {
|
|
941
|
+
cb(...args);
|
|
942
|
+
} catch (err) {
|
|
943
|
+
logger.warn(`Settings: ${this.label} hook failed`, { error: String(err) });
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
888
949
|
const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook<any>>> = {
|
|
889
950
|
"theme.dark": value => {
|
|
890
951
|
if (typeof value === "string") {
|
|
@@ -917,45 +978,34 @@ const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook<any>>> = {
|
|
|
917
978
|
},
|
|
918
979
|
"provider.appendOnlyContext": value => {
|
|
919
980
|
if (typeof value === "string") {
|
|
920
|
-
|
|
981
|
+
appendOnlyModeSignal.fire(value);
|
|
921
982
|
}
|
|
922
983
|
},
|
|
923
|
-
"hindsight.bankId": () =>
|
|
924
|
-
"hindsight.bankIdPrefix": () =>
|
|
925
|
-
"hindsight.scoping": () =>
|
|
984
|
+
"hindsight.bankId": () => hindsightScopeSignal.fire(),
|
|
985
|
+
"hindsight.bankIdPrefix": () => hindsightScopeSignal.fire(),
|
|
986
|
+
"hindsight.scoping": () => hindsightScopeSignal.fire(),
|
|
926
987
|
};
|
|
927
|
-
/**
|
|
928
|
-
const
|
|
988
|
+
/** Fires when `provider.appendOnlyContext` changes at runtime. */
|
|
989
|
+
const appendOnlyModeSignal = new SettingSignal<[value: string]>("provider.appendOnlyContext");
|
|
929
990
|
|
|
930
991
|
/**
|
|
931
992
|
* Subscribe to append-only mode setting changes.
|
|
932
993
|
* Returns an unsubscribe function. Multiple sessions (main + subagents)
|
|
933
994
|
* can register independently without overwriting each other.
|
|
934
995
|
*/
|
|
935
|
-
export
|
|
936
|
-
appendOnlyModeCallbacks.add(cb);
|
|
937
|
-
return () => {
|
|
938
|
-
appendOnlyModeCallbacks.delete(cb);
|
|
939
|
-
};
|
|
940
|
-
}
|
|
996
|
+
export const onAppendOnlyModeChanged = (cb: (value: string) => void) => appendOnlyModeSignal.on(cb);
|
|
941
997
|
|
|
942
|
-
/**
|
|
943
|
-
const
|
|
998
|
+
/** Fires when `statusLine.sessionAccent` changes at runtime. */
|
|
999
|
+
const statusLineSessionAccentSignal = new SettingSignal("statusLine.sessionAccent");
|
|
944
1000
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
cb();
|
|
954
|
-
} catch (err) {
|
|
955
|
-
logger.warn("Settings: hindsight scope hook failed", { error: String(err) });
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Subscribe to session-accent setting changes.
|
|
1003
|
+
* Returns an unsubscribe function. Callers should re-read settings in the callback.
|
|
1004
|
+
*/
|
|
1005
|
+
export const onStatusLineSessionAccentChanged = (cb: () => void) => statusLineSessionAccentSignal.on(cb);
|
|
1006
|
+
|
|
1007
|
+
/** Fires when any `hindsight.bankId` / `bankIdPrefix` / `scoping` value changes. */
|
|
1008
|
+
const hindsightScopeSignal = new SettingSignal("hindsight scope");
|
|
959
1009
|
|
|
960
1010
|
/**
|
|
961
1011
|
* Subscribe to changes in the Hindsight bank-scoping settings. Lets the
|
|
@@ -967,12 +1017,7 @@ function fireHindsightScopeChanged(): void {
|
|
|
967
1017
|
* Returns an unsubscribe function. The callback receives no arguments — the
|
|
968
1018
|
* caller is expected to re-read the relevant settings via `Settings.get`.
|
|
969
1019
|
*/
|
|
970
|
-
export
|
|
971
|
-
hindsightScopeCallbacks.add(cb);
|
|
972
|
-
return () => {
|
|
973
|
-
hindsightScopeCallbacks.delete(cb);
|
|
974
|
-
};
|
|
975
|
-
}
|
|
1020
|
+
export const onHindsightScopeChanged = (cb: () => void) => hindsightScopeSignal.on(cb);
|
|
976
1021
|
|
|
977
1022
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
978
1023
|
// Global Singleton
|
package/src/debug/index.ts
CHANGED
|
@@ -195,6 +195,7 @@ export class DebugSelectorComponent extends Container {
|
|
|
195
195
|
const result = await createReportBundle({
|
|
196
196
|
sessionFile: this.ctx.sessionManager.getSessionFile(),
|
|
197
197
|
settings: this.#getResolvedSettings(),
|
|
198
|
+
rawSseText: this.#getRawSseText(),
|
|
198
199
|
cpuProfile,
|
|
199
200
|
workProfile,
|
|
200
201
|
});
|
|
@@ -253,6 +254,7 @@ export class DebugSelectorComponent extends Container {
|
|
|
253
254
|
const result = await createReportBundle({
|
|
254
255
|
sessionFile: this.ctx.sessionManager.getSessionFile(),
|
|
255
256
|
settings: this.#getResolvedSettings(),
|
|
257
|
+
rawSseText: this.#getRawSseText(),
|
|
256
258
|
});
|
|
257
259
|
|
|
258
260
|
loader.stop();
|
|
@@ -288,6 +290,7 @@ export class DebugSelectorComponent extends Container {
|
|
|
288
290
|
const result = await createReportBundle({
|
|
289
291
|
sessionFile: this.ctx.sessionManager.getSessionFile(),
|
|
290
292
|
settings: this.#getResolvedSettings(),
|
|
293
|
+
rawSseText: this.#getRawSseText(),
|
|
291
294
|
heapSnapshot,
|
|
292
295
|
});
|
|
293
296
|
|
|
@@ -490,6 +493,11 @@ export class DebugSelectorComponent extends Container {
|
|
|
490
493
|
}
|
|
491
494
|
}
|
|
492
495
|
|
|
496
|
+
#getRawSseText(): string | undefined {
|
|
497
|
+
const rawSseText = resolveRawSseDebugBuffer(this.ctx.session).toRawText();
|
|
498
|
+
return rawSseText.trim().length > 0 ? rawSseText : undefined;
|
|
499
|
+
}
|
|
500
|
+
|
|
493
501
|
#getResolvedSettings(): Record<string, unknown> {
|
|
494
502
|
// Extract key settings for the report
|
|
495
503
|
return {
|
|
@@ -152,9 +152,9 @@ export class RawSseDebugBuffer {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
// Ownership contract for `event.raw`:
|
|
155
|
-
// The caller (
|
|
156
|
-
//
|
|
157
|
-
//
|
|
155
|
+
// The caller (`notifyRawSseEvent` in `packages/ai/src/utils/sse-debug.ts`)
|
|
156
|
+
// hands us a freshly-allocated `string[]` per event and never retains,
|
|
157
|
+
// mutates, or re-dispatches it.
|
|
158
158
|
// That lets `trimRawLines` keep the array by reference instead of
|
|
159
159
|
// cloning on every chunk — a measurable savings on the streaming hot
|
|
160
160
|
// path. If a future observer-chain mutates the array, restore the
|
|
@@ -192,7 +192,10 @@ export class RawSseDebugBuffer {
|
|
|
192
192
|
toRawText(): string {
|
|
193
193
|
// Reads the live array directly: `rawRecordText` only computes a string
|
|
194
194
|
// from each record, so no caller-visible mutation is possible.
|
|
195
|
-
|
|
195
|
+
const body = this.#records.map(rawRecordText).join("\n");
|
|
196
|
+
if (this.#droppedRecords === 0) return body;
|
|
197
|
+
const dropped = `: omp-debug-dropped records=${this.#droppedRecords} chars=${this.#droppedChars}\n\n`;
|
|
198
|
+
return body.length > 0 ? `${dropped}${body}` : dropped;
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
#append(record: RawSseDebugRecord, chars: number): void {
|
|
@@ -45,6 +45,8 @@ export interface ReportBundleOptions {
|
|
|
45
45
|
heapSnapshot?: HeapSnapshot;
|
|
46
46
|
/** Work profile (for work scheduling reports) */
|
|
47
47
|
workProfile?: WorkProfile;
|
|
48
|
+
/** Raw provider SSE diagnostics captured by the session buffer */
|
|
49
|
+
rawSseText?: string;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export interface ReportBundleResult {
|
|
@@ -70,6 +72,7 @@ export interface DebugLogSource {
|
|
|
70
72
|
* - env.json: Sanitized environment variables
|
|
71
73
|
* - config.json: Resolved settings
|
|
72
74
|
* - profile.cpuprofile: CPU profile (performance report only)
|
|
75
|
+
* - raw-sse.txt: Recent raw provider SSE diagnostics (when captured)
|
|
73
76
|
* - profile.md: Markdown CPU profile (performance report only)
|
|
74
77
|
* - heap.heapsnapshot: Heap snapshot (memory report only)
|
|
75
78
|
* - work.folded: Work profile folded stacks (work report only)
|
|
@@ -109,6 +112,12 @@ export async function createReportBundle(options: ReportBundleOptions): Promise<
|
|
|
109
112
|
files.push("logs.txt");
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
// Recent raw provider SSE diagnostics
|
|
116
|
+
if (options.rawSseText && options.rawSseText.trim().length > 0) {
|
|
117
|
+
data["raw-sse.txt"] = options.rawSseText;
|
|
118
|
+
files.push("raw-sse.txt");
|
|
119
|
+
}
|
|
120
|
+
|
|
112
121
|
// Session file
|
|
113
122
|
if (options.sessionFile) {
|
|
114
123
|
try {
|