@oh-my-pi/pi-coding-agent 14.0.5 → 14.1.1
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 +120 -0
- package/package.json +8 -8
- package/src/async/index.ts +1 -0
- package/src/async/job-manager.ts +43 -10
- package/src/async/support.ts +5 -0
- package/src/cli/list-models.ts +96 -57
- package/src/commit/agentic/tools/analyze-file.ts +1 -2
- package/src/commit/model-selection.ts +16 -13
- package/src/config/mcp-schema.json +1 -1
- package/src/config/model-equivalence.ts +675 -0
- package/src/config/model-registry.ts +242 -45
- package/src/config/model-resolver.ts +282 -65
- package/src/config/settings-schema.ts +27 -3
- package/src/config/settings.ts +1 -1
- package/src/cursor.ts +64 -23
- package/src/edit/index.ts +254 -89
- package/src/edit/modes/chunk.ts +336 -57
- package/src/edit/modes/hashline.ts +51 -26
- package/src/edit/modes/patch.ts +16 -10
- package/src/edit/modes/replace.ts +15 -7
- package/src/edit/renderer.ts +248 -94
- package/src/export/html/template.css +82 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +614 -97
- package/src/extensibility/custom-tools/types.ts +0 -3
- package/src/extensibility/extensions/loader.ts +16 -0
- package/src/extensibility/extensions/runner.ts +2 -7
- package/src/extensibility/extensions/types.ts +8 -4
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/internal-urls/jobs-protocol.ts +2 -1
- package/src/ipy/executor.ts +447 -52
- package/src/ipy/kernel.ts +39 -13
- package/src/lsp/client.ts +55 -1
- package/src/lsp/index.ts +8 -0
- package/src/lsp/types.ts +6 -0
- package/src/main.ts +6 -2
- package/src/memories/index.ts +7 -6
- package/src/modes/acp/acp-agent.ts +4 -1
- package/src/modes/components/bash-execution.ts +16 -4
- package/src/modes/components/model-selector.ts +221 -64
- package/src/modes/components/status-line/presets.ts +17 -6
- package/src/modes/components/status-line/segments.ts +15 -0
- package/src/modes/components/status-line-segment-editor.ts +1 -0
- package/src/modes/components/status-line.ts +7 -1
- package/src/modes/components/tool-execution.ts +145 -75
- package/src/modes/controllers/command-controller.ts +42 -1
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +28 -5
- package/src/modes/controllers/input-controller.ts +9 -3
- package/src/modes/controllers/selector-controller.ts +17 -6
- package/src/modes/interactive-mode.ts +19 -3
- package/src/modes/print-mode.ts +13 -4
- package/src/modes/prompt-action-autocomplete.ts +3 -5
- package/src/modes/rpc/rpc-mode.ts +8 -2
- package/src/modes/shared.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/bash.md +16 -1
- package/src/prompts/tools/cancel-job.md +1 -1
- package/src/prompts/tools/chunk-edit.md +191 -163
- package/src/prompts/tools/hashline.md +11 -11
- package/src/prompts/tools/patch.md +10 -5
- package/src/prompts/tools/{await.md → poll.md} +1 -1
- package/src/prompts/tools/read-chunk.md +12 -3
- package/src/prompts/tools/read.md +9 -0
- package/src/prompts/tools/task.md +2 -2
- package/src/prompts/tools/vim.md +98 -0
- package/src/prompts/tools/write.md +1 -0
- package/src/sdk.ts +758 -725
- package/src/session/agent-session.ts +187 -40
- package/src/session/session-manager.ts +50 -4
- package/src/slash-commands/builtin-registry.ts +17 -0
- package/src/task/executor.ts +9 -5
- package/src/task/index.ts +3 -5
- package/src/task/types.ts +2 -2
- package/src/tools/bash.ts +240 -57
- package/src/tools/cancel-job.ts +2 -1
- package/src/tools/find.ts +5 -2
- package/src/tools/grep.ts +77 -8
- package/src/tools/index.ts +48 -19
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/{await-tool.ts → poll-tool.ts} +38 -31
- package/src/tools/python.ts +293 -278
- package/src/tools/read.ts +218 -1
- package/src/tools/sqlite-reader.ts +623 -0
- package/src/tools/submit-result.ts +5 -2
- package/src/tools/todo-write.ts +8 -2
- package/src/tools/vim.ts +966 -0
- package/src/tools/write.ts +187 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/git.ts +24 -1
- package/src/utils/session-color.ts +55 -0
- package/src/utils/title-generator.ts +16 -7
- package/src/vim/buffer.ts +309 -0
- package/src/vim/commands.ts +382 -0
- package/src/vim/engine.ts +2426 -0
- package/src/vim/parser.ts +151 -0
- package/src/vim/render.ts +252 -0
- package/src/vim/types.ts +197 -0
|
@@ -58,6 +58,105 @@ export function formatModelString(model: Model<Api>): string {
|
|
|
58
58
|
return `${model.provider}/${model.id}`;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export function formatModelSelectorValue(selector: string, thinkingLevel: ThinkingLevel | undefined): string {
|
|
62
|
+
return thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit ? `${selector}:${thinkingLevel}` : selector;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getOpenRouterRouteSuffix(modelId: string): { baseId: string; suffix: string } | undefined {
|
|
66
|
+
const colonIdx = modelId.lastIndexOf(":");
|
|
67
|
+
if (colonIdx === -1) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const suffix = modelId.slice(colonIdx + 1).trim();
|
|
72
|
+
if (!suffix || parseThinkingLevel(suffix)) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { baseId: modelId.slice(0, colonIdx), suffix };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stripOpenRouterDateSuffix(modelId: string): string | undefined {
|
|
80
|
+
const stripped = modelId.replace(/-\d{8}(?=$|:)/i, "");
|
|
81
|
+
return stripped !== modelId ? stripped : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getOpenRouterFallbackModelIds(modelId: string): string[] {
|
|
85
|
+
const orderedCandidates: string[] = [];
|
|
86
|
+
const queue = [modelId];
|
|
87
|
+
const seen = new Set<string>();
|
|
88
|
+
|
|
89
|
+
while (queue.length > 0) {
|
|
90
|
+
const candidate = queue.shift();
|
|
91
|
+
if (!candidate || seen.has(candidate)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
seen.add(candidate);
|
|
95
|
+
orderedCandidates.push(candidate);
|
|
96
|
+
|
|
97
|
+
const routedSuffix = getOpenRouterRouteSuffix(candidate);
|
|
98
|
+
if (routedSuffix) {
|
|
99
|
+
queue.push(routedSuffix.baseId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const strippedDate = stripOpenRouterDateSuffix(candidate);
|
|
103
|
+
if (strippedDate) {
|
|
104
|
+
queue.push(strippedDate);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return orderedCandidates;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function cloneModelWithRequestedId(model: Model<Api>, requestedId: string): Model<Api> {
|
|
112
|
+
return {
|
|
113
|
+
...model,
|
|
114
|
+
id: requestedId,
|
|
115
|
+
...(model.name === model.id ? { name: requestedId } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function resolveProviderModelReference(
|
|
120
|
+
provider: string,
|
|
121
|
+
modelId: string,
|
|
122
|
+
availableModels: readonly Model<Api>[],
|
|
123
|
+
): Model<Api> | undefined {
|
|
124
|
+
const normalizedProvider = provider.trim().toLowerCase();
|
|
125
|
+
const normalizedModelId = modelId.trim().toLowerCase();
|
|
126
|
+
if (!normalizedProvider || !normalizedModelId) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const exactMatches = availableModels.filter(
|
|
131
|
+
model => model.provider.toLowerCase() === normalizedProvider && model.id.toLowerCase() === normalizedModelId,
|
|
132
|
+
);
|
|
133
|
+
if (exactMatches.length === 1) {
|
|
134
|
+
return exactMatches[0];
|
|
135
|
+
}
|
|
136
|
+
if (exactMatches.length > 1) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (normalizedProvider !== "openrouter") {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const fallbackId of getOpenRouterFallbackModelIds(modelId).slice(1)) {
|
|
145
|
+
const baseMatches = availableModels.filter(
|
|
146
|
+
model =>
|
|
147
|
+
model.provider.toLowerCase() === normalizedProvider && model.id.toLowerCase() === fallbackId.toLowerCase(),
|
|
148
|
+
);
|
|
149
|
+
if (baseMatches.length === 1) {
|
|
150
|
+
return cloneModelWithRequestedId(baseMatches[0], modelId);
|
|
151
|
+
}
|
|
152
|
+
if (baseMatches.length > 1) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
61
160
|
export interface ModelMatchPreferences {
|
|
62
161
|
/** Most-recently-used model keys (provider/modelId) to prefer when ambiguous. */
|
|
63
162
|
usageOrder?: string[];
|
|
@@ -65,6 +164,14 @@ export interface ModelMatchPreferences {
|
|
|
65
164
|
deprioritizeProviders?: string[];
|
|
66
165
|
}
|
|
67
166
|
|
|
167
|
+
export type CanonicalModelRegistry = Partial<
|
|
168
|
+
Pick<ModelRegistry, "resolveCanonicalModel" | "getCanonicalVariants" | "getCanonicalId">
|
|
169
|
+
>;
|
|
170
|
+
export type ModelLookupRegistry = Pick<ModelRegistry, "getAvailable"> & Partial<CanonicalModelRegistry>;
|
|
171
|
+
type CliModelRegistry = Pick<ModelRegistry, "getAll"> & Partial<CanonicalModelRegistry>;
|
|
172
|
+
type InitialModelRegistry = Pick<ModelRegistry, "getAvailable" | "find">;
|
|
173
|
+
type RestorableModelRegistry = Pick<ModelRegistry, "getAvailable" | "find" | "getApiKey">;
|
|
174
|
+
|
|
68
175
|
interface ModelPreferenceContext {
|
|
69
176
|
modelUsageRank: Map<string, number>;
|
|
70
177
|
providerUsageRank: Map<string, number>;
|
|
@@ -142,9 +249,8 @@ function isAlias(id: string): boolean {
|
|
|
142
249
|
}
|
|
143
250
|
|
|
144
251
|
/**
|
|
145
|
-
* Find an exact model
|
|
146
|
-
*
|
|
147
|
-
* When matching by bare id, ambiguous matches across providers are rejected.
|
|
252
|
+
* Find an exact explicit provider/model match.
|
|
253
|
+
* Bare model ids are handled separately so canonical ids can coalesce variants.
|
|
148
254
|
*/
|
|
149
255
|
export function findExactModelReferenceMatch(
|
|
150
256
|
modelReference: string,
|
|
@@ -155,39 +261,33 @@ export function findExactModelReferenceMatch(
|
|
|
155
261
|
return undefined;
|
|
156
262
|
}
|
|
157
263
|
|
|
158
|
-
const normalizedReference = trimmedReference.toLowerCase();
|
|
159
|
-
|
|
160
|
-
const canonicalMatches = availableModels.filter(
|
|
161
|
-
model => `${model.provider}/${model.id}`.toLowerCase() === normalizedReference,
|
|
162
|
-
);
|
|
163
|
-
if (canonicalMatches.length === 1) {
|
|
164
|
-
return canonicalMatches[0];
|
|
165
|
-
}
|
|
166
|
-
if (canonicalMatches.length > 1) {
|
|
167
|
-
return undefined;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
264
|
const slashIndex = trimmedReference.indexOf("/");
|
|
171
265
|
if (slashIndex !== -1) {
|
|
172
266
|
const provider = trimmedReference.substring(0, slashIndex).trim();
|
|
173
267
|
const modelId = trimmedReference.substring(slashIndex + 1).trim();
|
|
174
268
|
if (provider && modelId) {
|
|
175
|
-
|
|
176
|
-
model =>
|
|
177
|
-
model.provider.toLowerCase() === provider.toLowerCase() &&
|
|
178
|
-
model.id.toLowerCase() === modelId.toLowerCase(),
|
|
179
|
-
);
|
|
180
|
-
if (providerMatches.length === 1) {
|
|
181
|
-
return providerMatches[0];
|
|
182
|
-
}
|
|
183
|
-
if (providerMatches.length > 1) {
|
|
184
|
-
return undefined;
|
|
185
|
-
}
|
|
269
|
+
return resolveProviderModelReference(provider, modelId, availableModels);
|
|
186
270
|
}
|
|
187
271
|
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
188
274
|
|
|
189
|
-
|
|
190
|
-
|
|
275
|
+
function findExactCanonicalModelMatch(
|
|
276
|
+
modelReference: string,
|
|
277
|
+
availableModels: Model<Api>[],
|
|
278
|
+
modelRegistry: CanonicalModelRegistry | undefined,
|
|
279
|
+
): Model<Api> | undefined {
|
|
280
|
+
if (!modelRegistry) {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
const trimmedReference = modelReference.trim();
|
|
284
|
+
if (!trimmedReference || trimmedReference.includes("/")) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
return modelRegistry.resolveCanonicalModel?.(trimmedReference, {
|
|
288
|
+
availableOnly: false,
|
|
289
|
+
candidates: availableModels,
|
|
290
|
+
});
|
|
191
291
|
}
|
|
192
292
|
|
|
193
293
|
/**
|
|
@@ -198,13 +298,20 @@ function tryMatchModel(
|
|
|
198
298
|
modelPattern: string,
|
|
199
299
|
availableModels: Model<Api>[],
|
|
200
300
|
context: ModelPreferenceContext,
|
|
301
|
+
options?: { modelRegistry?: CanonicalModelRegistry },
|
|
201
302
|
): Model<Api> | undefined {
|
|
202
|
-
//
|
|
303
|
+
// Explicit provider/model selectors always bypass canonical coalescing.
|
|
203
304
|
const exactRefMatch = findExactModelReferenceMatch(modelPattern, availableModels);
|
|
204
305
|
if (exactRefMatch) {
|
|
205
306
|
return exactRefMatch;
|
|
206
307
|
}
|
|
207
308
|
|
|
309
|
+
// Exact canonical ids coalesce provider variants before bare-id matching.
|
|
310
|
+
const exactCanonicalMatch = findExactCanonicalModelMatch(modelPattern, availableModels, options?.modelRegistry);
|
|
311
|
+
if (exactCanonicalMatch) {
|
|
312
|
+
return exactCanonicalMatch;
|
|
313
|
+
}
|
|
314
|
+
|
|
208
315
|
// Check for provider/modelId format — fuzzy match within provider
|
|
209
316
|
const slashIndex = modelPattern.indexOf("/");
|
|
210
317
|
if (slashIndex !== -1) {
|
|
@@ -300,10 +407,10 @@ function parseModelPatternWithContext(
|
|
|
300
407
|
pattern: string,
|
|
301
408
|
availableModels: Model<Api>[],
|
|
302
409
|
context: ModelPreferenceContext,
|
|
303
|
-
options?: { allowInvalidThinkingSelectorFallback?: boolean },
|
|
410
|
+
options?: { allowInvalidThinkingSelectorFallback?: boolean; modelRegistry?: CanonicalModelRegistry },
|
|
304
411
|
): ParsedModelResult {
|
|
305
412
|
// Try exact match first
|
|
306
|
-
const exactMatch = tryMatchModel(pattern, availableModels, context);
|
|
413
|
+
const exactMatch = tryMatchModel(pattern, availableModels, context, options);
|
|
307
414
|
if (exactMatch) {
|
|
308
415
|
return { model: exactMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
|
|
309
416
|
}
|
|
@@ -357,7 +464,7 @@ export function parseModelPattern(
|
|
|
357
464
|
pattern: string,
|
|
358
465
|
availableModels: Model<Api>[],
|
|
359
466
|
preferences?: ModelMatchPreferences,
|
|
360
|
-
options?: { allowInvalidThinkingSelectorFallback?: boolean },
|
|
467
|
+
options?: { allowInvalidThinkingSelectorFallback?: boolean; modelRegistry?: CanonicalModelRegistry },
|
|
361
468
|
): ParsedModelResult {
|
|
362
469
|
const context = buildPreferenceContext(availableModels, preferences);
|
|
363
470
|
return parseModelPatternWithContext(pattern, availableModels, context, options);
|
|
@@ -469,7 +576,7 @@ export interface ResolvedModelRoleValue {
|
|
|
469
576
|
export function resolveModelRoleValue(
|
|
470
577
|
roleValue: string | undefined,
|
|
471
578
|
availableModels: Model<Api>[],
|
|
472
|
-
options?: { settings?: Settings; matchPreferences?: ModelMatchPreferences },
|
|
579
|
+
options?: { settings?: Settings; matchPreferences?: ModelMatchPreferences; modelRegistry?: CanonicalModelRegistry },
|
|
473
580
|
): ResolvedModelRoleValue {
|
|
474
581
|
if (!roleValue) {
|
|
475
582
|
return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
|
|
@@ -490,7 +597,9 @@ export function resolveModelRoleValue(
|
|
|
490
597
|
|
|
491
598
|
let warning: string | undefined;
|
|
492
599
|
for (const effectivePattern of effectivePatterns) {
|
|
493
|
-
const resolved = parseModelPattern(effectivePattern, availableModels, options?.matchPreferences
|
|
600
|
+
const resolved = parseModelPattern(effectivePattern, availableModels, options?.matchPreferences, {
|
|
601
|
+
modelRegistry: options?.modelRegistry,
|
|
602
|
+
});
|
|
494
603
|
if (resolved.model) {
|
|
495
604
|
return {
|
|
496
605
|
model: resolved.model,
|
|
@@ -543,13 +652,14 @@ export function resolveModelFromString(
|
|
|
543
652
|
value: string,
|
|
544
653
|
available: Model<Api>[],
|
|
545
654
|
matchPreferences?: ModelMatchPreferences,
|
|
655
|
+
modelRegistry?: CanonicalModelRegistry,
|
|
546
656
|
): Model<Api> | undefined {
|
|
547
657
|
const parsed = parseModelString(value);
|
|
548
658
|
if (parsed) {
|
|
549
659
|
const exact = available.find(model => model.provider === parsed.provider && model.id === parsed.id);
|
|
550
660
|
if (exact) return exact;
|
|
551
661
|
}
|
|
552
|
-
return parseModelPattern(value, available, matchPreferences).model;
|
|
662
|
+
return parseModelPattern(value, available, matchPreferences, { modelRegistry }).model;
|
|
553
663
|
}
|
|
554
664
|
|
|
555
665
|
/**
|
|
@@ -560,13 +670,19 @@ export function resolveModelFromSettings(options: {
|
|
|
560
670
|
availableModels: Model<Api>[];
|
|
561
671
|
matchPreferences?: ModelMatchPreferences;
|
|
562
672
|
roleOrder?: readonly ModelRole[];
|
|
673
|
+
modelRegistry?: CanonicalModelRegistry;
|
|
563
674
|
}): Model<Api> | undefined {
|
|
564
|
-
const { settings, availableModels, matchPreferences, roleOrder } = options;
|
|
675
|
+
const { settings, availableModels, matchPreferences, roleOrder, modelRegistry } = options;
|
|
565
676
|
const roles = roleOrder ?? MODEL_ROLE_IDS;
|
|
566
677
|
for (const role of roles) {
|
|
567
678
|
const configured = settings.getModelRole(role);
|
|
568
679
|
if (!configured) continue;
|
|
569
|
-
const resolved = resolveModelFromString(
|
|
680
|
+
const resolved = resolveModelFromString(
|
|
681
|
+
expandRoleAlias(configured, settings),
|
|
682
|
+
availableModels,
|
|
683
|
+
matchPreferences,
|
|
684
|
+
modelRegistry,
|
|
685
|
+
);
|
|
570
686
|
if (resolved) return resolved;
|
|
571
687
|
}
|
|
572
688
|
return availableModels[0];
|
|
@@ -577,7 +693,7 @@ export function resolveModelFromSettings(options: {
|
|
|
577
693
|
*/
|
|
578
694
|
export function resolveModelOverride(
|
|
579
695
|
modelPatterns: string[],
|
|
580
|
-
modelRegistry:
|
|
696
|
+
modelRegistry: ModelLookupRegistry,
|
|
581
697
|
settings?: Settings,
|
|
582
698
|
): { model?: Model<Api>; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } {
|
|
583
699
|
if (modelPatterns.length === 0) return { explicitThinkingLevel: false };
|
|
@@ -587,6 +703,7 @@ export function resolveModelOverride(
|
|
|
587
703
|
const { model, thinkingLevel, explicitThinkingLevel } = resolveModelRoleValue(pattern, availableModels, {
|
|
588
704
|
settings,
|
|
589
705
|
matchPreferences,
|
|
706
|
+
modelRegistry,
|
|
590
707
|
});
|
|
591
708
|
if (model) {
|
|
592
709
|
return { model, thinkingLevel, explicitThinkingLevel };
|
|
@@ -602,12 +719,14 @@ export function resolveRoleSelection(
|
|
|
602
719
|
roles: readonly string[],
|
|
603
720
|
settings: Settings,
|
|
604
721
|
availableModels: Model<Api>[],
|
|
722
|
+
modelRegistry?: CanonicalModelRegistry,
|
|
605
723
|
): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
|
|
606
724
|
const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
|
|
607
725
|
for (const role of roles) {
|
|
608
726
|
const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, {
|
|
609
727
|
settings,
|
|
610
728
|
matchPreferences,
|
|
729
|
+
modelRegistry,
|
|
611
730
|
});
|
|
612
731
|
if (resolved.model) {
|
|
613
732
|
return { model: resolved.model, thinkingLevel: resolved.thinkingLevel };
|
|
@@ -616,6 +735,36 @@ export function resolveRoleSelection(
|
|
|
616
735
|
return undefined;
|
|
617
736
|
}
|
|
618
737
|
|
|
738
|
+
function resolveExactCanonicalScopePattern(
|
|
739
|
+
pattern: string,
|
|
740
|
+
modelRegistry: Pick<ModelRegistry, "getCanonicalVariants">,
|
|
741
|
+
availableModels: Model<Api>[],
|
|
742
|
+
): { models: Model<Api>[]; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } | undefined {
|
|
743
|
+
const lastColonIndex = pattern.lastIndexOf(":");
|
|
744
|
+
let canonicalId = pattern;
|
|
745
|
+
let thinkingLevel: ThinkingLevel | undefined;
|
|
746
|
+
let explicitThinkingLevel = false;
|
|
747
|
+
|
|
748
|
+
if (lastColonIndex !== -1) {
|
|
749
|
+
const suffix = pattern.substring(lastColonIndex + 1);
|
|
750
|
+
const parsedThinkingLevel = parseThinkingLevel(suffix);
|
|
751
|
+
if (parsedThinkingLevel) {
|
|
752
|
+
canonicalId = pattern.substring(0, lastColonIndex);
|
|
753
|
+
thinkingLevel = parsedThinkingLevel;
|
|
754
|
+
explicitThinkingLevel = true;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const variants = modelRegistry
|
|
759
|
+
.getCanonicalVariants(canonicalId, { availableOnly: true, candidates: availableModels })
|
|
760
|
+
.map(variant => variant.model);
|
|
761
|
+
if (variants.length === 0) {
|
|
762
|
+
return undefined;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return { models: variants, thinkingLevel, explicitThinkingLevel };
|
|
766
|
+
}
|
|
767
|
+
|
|
619
768
|
/**
|
|
620
769
|
* Resolve model patterns to actual Model objects with optional thinking levels
|
|
621
770
|
* Format: "pattern:level" where :level is optional
|
|
@@ -629,7 +778,7 @@ export function resolveRoleSelection(
|
|
|
629
778
|
*/
|
|
630
779
|
export async function resolveModelScope(
|
|
631
780
|
patterns: string[],
|
|
632
|
-
modelRegistry: ModelRegistry,
|
|
781
|
+
modelRegistry: Pick<ModelRegistry, "getAvailable" | "getCanonicalVariants">,
|
|
633
782
|
preferences?: ModelMatchPreferences,
|
|
634
783
|
): Promise<ScopedModel[]> {
|
|
635
784
|
const availableModels = modelRegistry.getAvailable();
|
|
@@ -682,10 +831,28 @@ export async function resolveModelScope(
|
|
|
682
831
|
continue;
|
|
683
832
|
}
|
|
684
833
|
|
|
834
|
+
const exactCanonical = resolveExactCanonicalScopePattern(pattern, modelRegistry, availableModels);
|
|
835
|
+
if (exactCanonical) {
|
|
836
|
+
for (const model of exactCanonical.models) {
|
|
837
|
+
if (!scopedModels.find(sm => modelsAreEqual(sm.model, model))) {
|
|
838
|
+
scopedModels.push({
|
|
839
|
+
model,
|
|
840
|
+
thinkingLevel: exactCanonical.explicitThinkingLevel
|
|
841
|
+
? (resolveThinkingLevelForModel(model, exactCanonical.thinkingLevel) ??
|
|
842
|
+
exactCanonical.thinkingLevel)
|
|
843
|
+
: exactCanonical.thinkingLevel,
|
|
844
|
+
explicitThinkingLevel: exactCanonical.explicitThinkingLevel,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
|
|
685
851
|
const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPatternWithContext(
|
|
686
852
|
pattern,
|
|
687
853
|
availableModels,
|
|
688
854
|
context,
|
|
855
|
+
{ modelRegistry },
|
|
689
856
|
);
|
|
690
857
|
|
|
691
858
|
if (warning) {
|
|
@@ -714,6 +881,7 @@ export async function resolveModelScope(
|
|
|
714
881
|
|
|
715
882
|
export interface ResolveCliModelResult {
|
|
716
883
|
model: Model<Api> | undefined;
|
|
884
|
+
selector?: string;
|
|
717
885
|
thinkingLevel?: ThinkingLevel;
|
|
718
886
|
warning: string | undefined;
|
|
719
887
|
error: string | undefined;
|
|
@@ -725,19 +893,20 @@ export interface ResolveCliModelResult {
|
|
|
725
893
|
export function resolveCliModel(options: {
|
|
726
894
|
cliProvider?: string;
|
|
727
895
|
cliModel?: string;
|
|
728
|
-
modelRegistry:
|
|
896
|
+
modelRegistry: CliModelRegistry;
|
|
729
897
|
preferences?: ModelMatchPreferences;
|
|
730
898
|
}): ResolveCliModelResult {
|
|
731
899
|
const { cliProvider, cliModel, modelRegistry, preferences } = options;
|
|
732
900
|
|
|
733
901
|
if (!cliModel) {
|
|
734
|
-
return { model: undefined, warning: undefined, error: undefined };
|
|
902
|
+
return { model: undefined, selector: undefined, warning: undefined, error: undefined };
|
|
735
903
|
}
|
|
736
904
|
|
|
737
905
|
const availableModels = modelRegistry.getAll();
|
|
738
906
|
if (availableModels.length === 0) {
|
|
739
907
|
return {
|
|
740
908
|
model: undefined,
|
|
909
|
+
selector: undefined,
|
|
741
910
|
warning: undefined,
|
|
742
911
|
error: "No models available. Check your installation or add models to models.json.",
|
|
743
912
|
};
|
|
@@ -752,13 +921,15 @@ export function resolveCliModel(options: {
|
|
|
752
921
|
if (cliProvider && !provider) {
|
|
753
922
|
return {
|
|
754
923
|
model: undefined,
|
|
924
|
+
selector: undefined,
|
|
755
925
|
warning: undefined,
|
|
756
926
|
error: `Unknown provider "${cliProvider}". Use --list-models to see available providers/models.`,
|
|
757
927
|
};
|
|
758
928
|
}
|
|
759
929
|
|
|
930
|
+
const trimmedModel = cliModel.trim();
|
|
760
931
|
if (!provider) {
|
|
761
|
-
const lower =
|
|
932
|
+
const lower = trimmedModel.toLowerCase();
|
|
762
933
|
// When input has provider/id format (e.g. "zai/glm-5"), prefer decomposed
|
|
763
934
|
// provider+id match over flat id match. Without this, a model with id
|
|
764
935
|
// "zai/glm-5" on provider "vercel-ai-gateway" wins over provider "zai"
|
|
@@ -767,10 +938,20 @@ export function resolveCliModel(options: {
|
|
|
767
938
|
let exact: (typeof availableModels)[number] | undefined;
|
|
768
939
|
if (slashIdx !== -1) {
|
|
769
940
|
const prefix = lower.substring(0, slashIdx);
|
|
770
|
-
const suffix =
|
|
771
|
-
exact = availableModels
|
|
772
|
-
|
|
773
|
-
|
|
941
|
+
const suffix = trimmedModel.substring(slashIdx + 1);
|
|
942
|
+
exact = resolveProviderModelReference(prefix, suffix, availableModels);
|
|
943
|
+
}
|
|
944
|
+
if (!exact && !trimmedModel.includes(":")) {
|
|
945
|
+
const canonicalMatch = modelRegistry.resolveCanonicalModel?.(trimmedModel, { availableOnly: false });
|
|
946
|
+
if (canonicalMatch) {
|
|
947
|
+
return {
|
|
948
|
+
model: canonicalMatch,
|
|
949
|
+
selector: modelRegistry.getCanonicalId?.(canonicalMatch) ?? trimmedModel,
|
|
950
|
+
warning: undefined,
|
|
951
|
+
thinkingLevel: undefined,
|
|
952
|
+
error: undefined,
|
|
953
|
+
};
|
|
954
|
+
}
|
|
774
955
|
}
|
|
775
956
|
if (!exact) {
|
|
776
957
|
exact = availableModels.find(
|
|
@@ -778,11 +959,17 @@ export function resolveCliModel(options: {
|
|
|
778
959
|
);
|
|
779
960
|
}
|
|
780
961
|
if (exact) {
|
|
781
|
-
return {
|
|
962
|
+
return {
|
|
963
|
+
model: exact,
|
|
964
|
+
selector: formatModelString(exact),
|
|
965
|
+
warning: undefined,
|
|
966
|
+
thinkingLevel: undefined,
|
|
967
|
+
error: undefined,
|
|
968
|
+
};
|
|
782
969
|
}
|
|
783
970
|
}
|
|
784
971
|
|
|
785
|
-
let pattern =
|
|
972
|
+
let pattern = trimmedModel;
|
|
786
973
|
|
|
787
974
|
if (!provider) {
|
|
788
975
|
const slashIndex = cliModel.indexOf("/");
|
|
@@ -801,22 +988,58 @@ export function resolveCliModel(options: {
|
|
|
801
988
|
}
|
|
802
989
|
}
|
|
803
990
|
|
|
991
|
+
if (provider) {
|
|
992
|
+
const exactProviderMatch = resolveProviderModelReference(provider, pattern, availableModels);
|
|
993
|
+
if (exactProviderMatch) {
|
|
994
|
+
return {
|
|
995
|
+
model: exactProviderMatch,
|
|
996
|
+
selector: formatModelString(exactProviderMatch),
|
|
997
|
+
warning: undefined,
|
|
998
|
+
thinkingLevel: undefined,
|
|
999
|
+
error: undefined,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
804
1004
|
const candidates = provider ? availableModels.filter(model => model.provider === provider) : availableModels;
|
|
805
1005
|
const { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, preferences, {
|
|
806
1006
|
allowInvalidThinkingSelectorFallback: false,
|
|
1007
|
+
modelRegistry,
|
|
807
1008
|
});
|
|
808
1009
|
|
|
809
1010
|
if (!model) {
|
|
810
1011
|
const display = provider ? `${provider}/${pattern}` : cliModel;
|
|
811
1012
|
return {
|
|
812
1013
|
model: undefined,
|
|
1014
|
+
selector: undefined,
|
|
813
1015
|
thinkingLevel: undefined,
|
|
814
1016
|
warning,
|
|
815
1017
|
error: `Model "${display}" not found. Use --list-models to see available models.`,
|
|
816
1018
|
};
|
|
817
1019
|
}
|
|
818
1020
|
|
|
819
|
-
|
|
1021
|
+
let selector = provider ? formatModelString(model) : undefined;
|
|
1022
|
+
if (!provider) {
|
|
1023
|
+
const lastColonIndex = pattern.lastIndexOf(":");
|
|
1024
|
+
const canonicalCandidate =
|
|
1025
|
+
lastColonIndex !== -1 && parseThinkingLevel(pattern.substring(lastColonIndex + 1))
|
|
1026
|
+
? pattern.substring(0, lastColonIndex)
|
|
1027
|
+
: pattern;
|
|
1028
|
+
if (!canonicalCandidate.includes("/")) {
|
|
1029
|
+
const canonicalResolved = modelRegistry.resolveCanonicalModel?.(canonicalCandidate, { availableOnly: false });
|
|
1030
|
+
if (canonicalResolved && canonicalResolved.provider === model.provider && canonicalResolved.id === model.id) {
|
|
1031
|
+
selector = modelRegistry.getCanonicalId?.(canonicalResolved) ?? canonicalCandidate;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return {
|
|
1037
|
+
model,
|
|
1038
|
+
selector,
|
|
1039
|
+
thinkingLevel,
|
|
1040
|
+
warning,
|
|
1041
|
+
error: undefined,
|
|
1042
|
+
};
|
|
820
1043
|
}
|
|
821
1044
|
|
|
822
1045
|
export interface InitialModelResult {
|
|
@@ -841,7 +1064,7 @@ export async function findInitialModel(options: {
|
|
|
841
1064
|
defaultProvider?: string;
|
|
842
1065
|
defaultModelId?: string;
|
|
843
1066
|
defaultThinkingSelector?: Effort;
|
|
844
|
-
modelRegistry:
|
|
1067
|
+
modelRegistry: InitialModelRegistry;
|
|
845
1068
|
}): Promise<InitialModelResult> {
|
|
846
1069
|
const {
|
|
847
1070
|
cliProvider,
|
|
@@ -923,7 +1146,7 @@ export async function restoreModelFromSession(
|
|
|
923
1146
|
savedModelId: string,
|
|
924
1147
|
currentModel: Model<Api> | undefined,
|
|
925
1148
|
shouldPrintMessages: boolean,
|
|
926
|
-
modelRegistry:
|
|
1149
|
+
modelRegistry: RestorableModelRegistry,
|
|
927
1150
|
): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {
|
|
928
1151
|
const restoredModel = modelRegistry.find(savedProvider, savedModelId);
|
|
929
1152
|
|
|
@@ -998,7 +1221,7 @@ export async function restoreModelFromSession(
|
|
|
998
1221
|
* @returns The best available smol model, or undefined if none found
|
|
999
1222
|
*/
|
|
1000
1223
|
export async function findSmolModel(
|
|
1001
|
-
modelRegistry:
|
|
1224
|
+
modelRegistry: ModelLookupRegistry,
|
|
1002
1225
|
savedModel?: string,
|
|
1003
1226
|
): Promise<Model<Api> | undefined> {
|
|
1004
1227
|
const availableModels = modelRegistry.getAvailable();
|
|
@@ -1006,11 +1229,8 @@ export async function findSmolModel(
|
|
|
1006
1229
|
|
|
1007
1230
|
// 1. Try saved model from settings
|
|
1008
1231
|
if (savedModel) {
|
|
1009
|
-
const
|
|
1010
|
-
if (
|
|
1011
|
-
const match = availableModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
|
|
1012
|
-
if (match) return match;
|
|
1013
|
-
}
|
|
1232
|
+
const match = resolveModelFromString(savedModel, availableModels, undefined, modelRegistry);
|
|
1233
|
+
if (match) return match;
|
|
1014
1234
|
}
|
|
1015
1235
|
|
|
1016
1236
|
// 2. Try priority chain
|
|
@@ -1020,7 +1240,7 @@ export async function findSmolModel(
|
|
|
1020
1240
|
if (providerMatch) return providerMatch;
|
|
1021
1241
|
|
|
1022
1242
|
// Try exact match first
|
|
1023
|
-
const exactMatch =
|
|
1243
|
+
const exactMatch = parseModelPattern(pattern, availableModels, undefined, { modelRegistry }).model;
|
|
1024
1244
|
if (exactMatch) return exactMatch;
|
|
1025
1245
|
|
|
1026
1246
|
// Try fuzzy match (substring)
|
|
@@ -1041,7 +1261,7 @@ export async function findSmolModel(
|
|
|
1041
1261
|
* @returns The best available slow model, or undefined if none found
|
|
1042
1262
|
*/
|
|
1043
1263
|
export async function findSlowModel(
|
|
1044
|
-
modelRegistry:
|
|
1264
|
+
modelRegistry: ModelLookupRegistry,
|
|
1045
1265
|
savedModel?: string,
|
|
1046
1266
|
): Promise<Model<Api> | undefined> {
|
|
1047
1267
|
const availableModels = modelRegistry.getAvailable();
|
|
@@ -1049,17 +1269,14 @@ export async function findSlowModel(
|
|
|
1049
1269
|
|
|
1050
1270
|
// 1. Try saved model from settings
|
|
1051
1271
|
if (savedModel) {
|
|
1052
|
-
const
|
|
1053
|
-
if (
|
|
1054
|
-
const match = availableModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
|
|
1055
|
-
if (match) return match;
|
|
1056
|
-
}
|
|
1272
|
+
const match = resolveModelFromString(savedModel, availableModels, undefined, modelRegistry);
|
|
1273
|
+
if (match) return match;
|
|
1057
1274
|
}
|
|
1058
1275
|
|
|
1059
1276
|
// 2. Try priority chain
|
|
1060
1277
|
for (const pattern of MODEL_PRIO.slow) {
|
|
1061
1278
|
// Try exact match first
|
|
1062
|
-
const exactMatch =
|
|
1279
|
+
const exactMatch = parseModelPattern(pattern, availableModels, undefined, { modelRegistry }).model;
|
|
1063
1280
|
if (exactMatch) return exactMatch;
|
|
1064
1281
|
|
|
1065
1282
|
// Try fuzzy match (substring)
|
|
@@ -74,7 +74,8 @@ export type StatusLineSegmentId =
|
|
|
74
74
|
| "session"
|
|
75
75
|
| "hostname"
|
|
76
76
|
| "cache_read"
|
|
77
|
-
| "cache_write"
|
|
77
|
+
| "cache_write"
|
|
78
|
+
| "session_name";
|
|
78
79
|
|
|
79
80
|
interface UiMetadata {
|
|
80
81
|
tab: SettingTab;
|
|
@@ -229,6 +230,8 @@ export const SETTINGS_SCHEMA = {
|
|
|
229
230
|
|
|
230
231
|
modelTags: { type: "record", default: EMPTY_MODEL_TAGS_RECORD },
|
|
231
232
|
|
|
233
|
+
modelProviderOrder: { type: "array", default: EMPTY_STRING_ARRAY },
|
|
234
|
+
|
|
232
235
|
cycleOrder: { type: "array", default: DEFAULT_CYCLE_ORDER },
|
|
233
236
|
|
|
234
237
|
// ────────────────────────────────────────────────────────────────────────
|
|
@@ -949,12 +952,12 @@ export const SETTINGS_SCHEMA = {
|
|
|
949
952
|
// Edit tool
|
|
950
953
|
"edit.mode": {
|
|
951
954
|
type: "enum",
|
|
952
|
-
values: ["replace", "patch", "hashline", "chunk"] as const,
|
|
955
|
+
values: ["replace", "patch", "hashline", "chunk", "vim"] as const,
|
|
953
956
|
default: "hashline",
|
|
954
957
|
ui: {
|
|
955
958
|
tab: "editing",
|
|
956
959
|
label: "Edit Mode",
|
|
957
|
-
description: "Select the edit tool variant (replace, patch, hashline, or
|
|
960
|
+
description: "Select the edit tool variant (replace, patch, hashline, chunk, or vim)",
|
|
958
961
|
},
|
|
959
962
|
},
|
|
960
963
|
|
|
@@ -1383,6 +1386,27 @@ export const SETTINGS_SCHEMA = {
|
|
|
1383
1386
|
},
|
|
1384
1387
|
},
|
|
1385
1388
|
|
|
1389
|
+
"bash.autoBackground.enabled": {
|
|
1390
|
+
type: "boolean",
|
|
1391
|
+
default: false,
|
|
1392
|
+
ui: {
|
|
1393
|
+
tab: "tools",
|
|
1394
|
+
label: "Bash Auto-Background",
|
|
1395
|
+
description: "Automatically background long-running bash commands and deliver the result later",
|
|
1396
|
+
},
|
|
1397
|
+
},
|
|
1398
|
+
|
|
1399
|
+
"bash.autoBackground.thresholdMs": {
|
|
1400
|
+
type: "number",
|
|
1401
|
+
default: 60_000,
|
|
1402
|
+
ui: {
|
|
1403
|
+
tab: "tools",
|
|
1404
|
+
label: "Bash Auto-Background Delay",
|
|
1405
|
+
description: "Milliseconds to wait before a bash command is moved to the background (0 = immediately)",
|
|
1406
|
+
submenu: true,
|
|
1407
|
+
},
|
|
1408
|
+
},
|
|
1409
|
+
|
|
1386
1410
|
// MCP
|
|
1387
1411
|
"mcp.enableProjectConfig": {
|
|
1388
1412
|
type: "boolean",
|
package/src/config/settings.ts
CHANGED
|
@@ -326,7 +326,7 @@ export class Settings {
|
|
|
326
326
|
|
|
327
327
|
/**
|
|
328
328
|
* Get the edit variant for a specific model.
|
|
329
|
-
* Returns "patch", "replace", "hashline", "chunk", or null (use global default).
|
|
329
|
+
* Returns "patch", "replace", "hashline", "chunk", "vim", or null (use global default).
|
|
330
330
|
*/
|
|
331
331
|
getEditVariantForModel(model: string | undefined): EditMode | null {
|
|
332
332
|
if (!model) return null;
|