tau-coding-agent 0.1.3 → 0.1.5

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.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/extensions/answer.ts +49 -43
  3. package/extensions/branch-term.ts +11 -16
  4. package/extensions/btw.ts +8 -8
  5. package/extensions/ghostty.ts +65 -12
  6. package/extensions/git-diff-stats.ts +1 -1
  7. package/extensions/git-pr-status.ts +1 -1
  8. package/extensions/insights.ts +6 -5
  9. package/extensions/loop.ts +39 -11
  10. package/extensions/memory.ts +6 -5
  11. package/extensions/notify.ts +25 -6
  12. package/extensions/openai-fast.ts +6 -4
  13. package/extensions/openai-verbosity.ts +6 -4
  14. package/extensions/review/fix.ts +239 -0
  15. package/extensions/review/git.ts +517 -0
  16. package/extensions/review/index.ts +232 -0
  17. package/extensions/review/message-queue.ts +249 -0
  18. package/extensions/review/models.ts +589 -0
  19. package/extensions/review/prompts.ts +337 -0
  20. package/extensions/review/renderers/inline.ts +126 -0
  21. package/extensions/review/request.ts +504 -0
  22. package/extensions/review/review.ts +1305 -0
  23. package/extensions/review/runner.ts +522 -0
  24. package/extensions/review/runtime.ts +275 -0
  25. package/extensions/review/schema.ts +160 -0
  26. package/extensions/review/submit-review-tool.ts +66 -0
  27. package/extensions/review/submit-triage-tool.ts +59 -0
  28. package/extensions/review/triage.ts +746 -0
  29. package/extensions/sandbox/index.ts +428 -103
  30. package/extensions/{interlude.ts → stash.ts} +13 -11
  31. package/extensions/tool-display-mode.ts +173 -17
  32. package/extensions/usage/anthropic.ts +1 -1
  33. package/extensions/usage/github-copilot.ts +1 -1
  34. package/extensions/usage/index.ts +9 -5
  35. package/extensions/usage/minimax.ts +1 -1
  36. package/extensions/usage/openai-codex.ts +1 -1
  37. package/extensions/usage/openrouter.ts +1 -1
  38. package/extensions/usage/providers.ts +0 -2
  39. package/extensions/usage/types.ts +1 -2
  40. package/extensions/usage/zai.ts +1 -1
  41. package/extensions/websearch/README.md +1 -1
  42. package/extensions/websearch/browser/chromium.ts +42 -31
  43. package/extensions/websearch/browser/discovery.ts +11 -9
  44. package/extensions/websearch/browser/firefox.ts +10 -7
  45. package/extensions/websearch/browser/fs.ts +10 -0
  46. package/extensions/websearch/browser/sqlite.ts +22 -15
  47. package/extensions/websearch/config.ts +2 -2
  48. package/extensions/websearch/index.ts +9 -9
  49. package/extensions/websearch/providers/anthropic.pi.ts +1 -1
  50. package/extensions/websearch/providers/gemini.pi.ts +1 -1
  51. package/extensions/websearch/providers/openai-codex.pi.ts +1 -1
  52. package/extensions/websearch/providers/pi-model.shared.ts +2 -2
  53. package/extensions/worktree.ts +36 -36
  54. package/package.json +7 -7
  55. package/skills/git-clean-history/SKILL.md +1 -0
  56. package/skills/git-commit/SKILL.md +2 -2
  57. package/skills/oracle/scripts/oracle +28 -12
  58. package/themes/tau-dark.json +2 -1
  59. package/extensions/review.ts +0 -4347
  60. package/extensions/usage/google-gemini-cli.ts +0 -232
@@ -0,0 +1,589 @@
1
+ import { getSupportedThinkingLevels, type Api, type Model } from "@earendil-works/pi-ai";
2
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+
4
+ const OPENAI_FAST_MODEL_ID = "gpt-5.3-codex-spark";
5
+ const ANTHROPIC_FAST_MODEL_ID = "claude-haiku-4-5";
6
+
7
+ type ModelFamily = "openai" | "anthropic";
8
+
9
+ export type ReviewThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
10
+
11
+ export type ReviewThinkingSource = "explicit" | "inherited";
12
+
13
+ export type ResolvedReviewProviderCandidate = {
14
+ modelArg?: string;
15
+ baseModelArg?: string;
16
+ supportsThinking?: boolean;
17
+ supportsXhigh?: boolean;
18
+ supportsMax?: boolean;
19
+ };
20
+
21
+ export type ResolvedReviewModel = {
22
+ modelPattern: string;
23
+ modelPatternBase: string;
24
+ thinkingSource: ReviewThinkingSource;
25
+ requestedThinkingLevel?: ReviewThinkingLevel;
26
+ providerCandidates: ResolvedReviewProviderCandidate[];
27
+ };
28
+
29
+ const providerCandidateAvailabilityCache = new WeakMap<
30
+ ResolvedReviewModel,
31
+ Map<string, "supported" | "unsupported">
32
+ >();
33
+ const providerCandidateProbeCache = new WeakMap<
34
+ ResolvedReviewModel,
35
+ Map<string, Promise<"supported" | "unsupported">>
36
+ >();
37
+
38
+ function getProviderCandidateCacheKey(candidate: ResolvedReviewProviderCandidate): string {
39
+ return candidate.modelArg ?? candidate.baseModelArg ?? "__default__";
40
+ }
41
+
42
+ function getProviderCandidateAvailabilityMap(
43
+ model: ResolvedReviewModel,
44
+ ): Map<string, "supported" | "unsupported"> {
45
+ let cache = providerCandidateAvailabilityCache.get(model);
46
+ if (!cache) {
47
+ cache = new Map();
48
+ providerCandidateAvailabilityCache.set(model, cache);
49
+ }
50
+ return cache;
51
+ }
52
+
53
+ function getProviderCandidateProbeMap(
54
+ model: ResolvedReviewModel,
55
+ ): Map<string, Promise<"supported" | "unsupported">> {
56
+ let cache = providerCandidateProbeCache.get(model);
57
+ if (!cache) {
58
+ cache = new Map();
59
+ providerCandidateProbeCache.set(model, cache);
60
+ }
61
+ return cache;
62
+ }
63
+
64
+ export function getProviderCandidateAvailability(
65
+ model: ResolvedReviewModel,
66
+ candidate: ResolvedReviewProviderCandidate,
67
+ ): "supported" | "unsupported" | undefined {
68
+ return getProviderCandidateAvailabilityMap(model).get(getProviderCandidateCacheKey(candidate));
69
+ }
70
+
71
+ export function setProviderCandidateAvailability(
72
+ model: ResolvedReviewModel,
73
+ candidate: ResolvedReviewProviderCandidate,
74
+ availability: "supported" | "unsupported",
75
+ ): void {
76
+ getProviderCandidateAvailabilityMap(model).set(
77
+ getProviderCandidateCacheKey(candidate),
78
+ availability,
79
+ );
80
+ }
81
+
82
+ export function getProviderCandidateProbe(
83
+ model: ResolvedReviewModel,
84
+ candidate: ResolvedReviewProviderCandidate,
85
+ ): Promise<"supported" | "unsupported"> | undefined {
86
+ return getProviderCandidateProbeMap(model).get(getProviderCandidateCacheKey(candidate));
87
+ }
88
+
89
+ export function setProviderCandidateProbe(
90
+ model: ResolvedReviewModel,
91
+ candidate: ResolvedReviewProviderCandidate,
92
+ probe: Promise<"supported" | "unsupported">,
93
+ ): void {
94
+ getProviderCandidateProbeMap(model).set(getProviderCandidateCacheKey(candidate), probe);
95
+ }
96
+
97
+ export function clearProviderCandidateProbe(
98
+ model: ResolvedReviewModel,
99
+ candidate: ResolvedReviewProviderCandidate,
100
+ ): void {
101
+ getProviderCandidateProbeMap(model).delete(getProviderCandidateCacheKey(candidate));
102
+ }
103
+
104
+ const REVIEW_THINKING_LEVEL_ORDER: ReviewThinkingLevel[] = [
105
+ "off",
106
+ "minimal",
107
+ "low",
108
+ "medium",
109
+ "high",
110
+ "xhigh",
111
+ "max",
112
+ ];
113
+ const REVIEW_THINKING_LEVELS = new Set<ReviewThinkingLevel>(REVIEW_THINKING_LEVEL_ORDER);
114
+
115
+ function detectModelFamily(provider: string): ModelFamily | null {
116
+ const normalizedProvider = provider.toLowerCase();
117
+ if (normalizedProvider.includes("openai")) return "openai";
118
+ if (normalizedProvider.includes("anthropic")) return "anthropic";
119
+ return null;
120
+ }
121
+
122
+ function isReviewThinkingLevel(value: string): value is ReviewThinkingLevel {
123
+ return REVIEW_THINKING_LEVELS.has(value as ReviewThinkingLevel);
124
+ }
125
+
126
+ function splitModelPatternThinkingSuffix(modelPattern: string): {
127
+ basePattern: string;
128
+ thinkingSuffix?: ReviewThinkingLevel;
129
+ } {
130
+ const lastColon = modelPattern.lastIndexOf(":");
131
+ if (lastColon <= 0) return { basePattern: modelPattern };
132
+
133
+ const thinkingSuffix = modelPattern
134
+ .slice(lastColon + 1)
135
+ .trim()
136
+ .toLowerCase();
137
+ if (!isReviewThinkingLevel(thinkingSuffix)) {
138
+ return { basePattern: modelPattern };
139
+ }
140
+
141
+ const basePattern = modelPattern.slice(0, lastColon).trim();
142
+ return basePattern ? { basePattern, thinkingSuffix } : { basePattern: modelPattern };
143
+ }
144
+
145
+ function clampInheritedThinkingLevel(
146
+ thinkingLevel: ReviewThinkingLevel,
147
+ supportsThinking: boolean | undefined,
148
+ supportsXhighThinking: boolean | undefined,
149
+ supportsMaxThinking: boolean | undefined,
150
+ ): ReviewThinkingLevel {
151
+ if (supportsThinking === false) return "off";
152
+ if (thinkingLevel === "max" && supportsMaxThinking === false) {
153
+ return supportsXhighThinking === false ? "high" : "xhigh";
154
+ }
155
+ if (thinkingLevel === "xhigh" && supportsXhighThinking === false) return "high";
156
+ return thinkingLevel;
157
+ }
158
+
159
+ export function getFallbackThinkingLevels(
160
+ thinkingLevel: ReviewThinkingLevel,
161
+ ): ReviewThinkingLevel[] {
162
+ const requestedIndex = REVIEW_THINKING_LEVEL_ORDER.indexOf(thinkingLevel);
163
+ if (requestedIndex === -1) return [];
164
+ return REVIEW_THINKING_LEVEL_ORDER.slice(0, requestedIndex + 1).reverse();
165
+ }
166
+
167
+ function buildGlobRegExp(pattern: string): RegExp {
168
+ let regex = "^";
169
+
170
+ for (let index = 0; index < pattern.length; index += 1) {
171
+ const char = pattern[index];
172
+ if (char === "*") {
173
+ regex += ".*";
174
+ continue;
175
+ }
176
+ if (char === "?") {
177
+ regex += ".";
178
+ continue;
179
+ }
180
+ if (char === "[") {
181
+ const closingIndex = pattern.indexOf("]", index + 1);
182
+ if (closingIndex > index + 1) {
183
+ regex += pattern.slice(index, closingIndex + 1);
184
+ index = closingIndex;
185
+ continue;
186
+ }
187
+ }
188
+ regex += char.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
189
+ }
190
+
191
+ return new RegExp(`${regex}$`, "i");
192
+ }
193
+
194
+ export function appendModelThinkingSuffix(
195
+ modelArg: string,
196
+ thinkingSuffix: ReviewThinkingLevel | undefined,
197
+ ): string {
198
+ return thinkingSuffix ? `${modelArg}:${thinkingSuffix}` : modelArg;
199
+ }
200
+
201
+ function createResolvedReviewProviderCandidate(options: {
202
+ modelArg?: string;
203
+ baseModelArg?: string;
204
+ supportsThinking?: boolean;
205
+ supportsXhigh?: boolean;
206
+ supportsMax?: boolean;
207
+ }): ResolvedReviewProviderCandidate {
208
+ return {
209
+ modelArg: options.modelArg,
210
+ baseModelArg: options.baseModelArg,
211
+ supportsThinking: options.supportsThinking,
212
+ supportsXhigh: options.supportsXhigh,
213
+ supportsMax: options.supportsMax,
214
+ };
215
+ }
216
+
217
+ export function getResolvedReviewPrimaryProviderCandidate(
218
+ model: ResolvedReviewModel,
219
+ ): ResolvedReviewProviderCandidate {
220
+ return model.providerCandidates[0] ?? createResolvedReviewProviderCandidate({});
221
+ }
222
+
223
+ export function getResolvedReviewCurrentThinkingLevel(
224
+ model: ResolvedReviewModel,
225
+ providerCandidate: ResolvedReviewProviderCandidate,
226
+ ): ReviewThinkingLevel | undefined {
227
+ if (model.requestedThinkingLevel === undefined) return undefined;
228
+ return model.thinkingSource === "inherited"
229
+ ? clampInheritedThinkingLevel(
230
+ model.requestedThinkingLevel,
231
+ providerCandidate.supportsThinking,
232
+ providerCandidate.supportsXhigh,
233
+ providerCandidate.supportsMax,
234
+ )
235
+ : model.requestedThinkingLevel;
236
+ }
237
+
238
+ function shouldShowResolvedReviewThinkingSuffix(
239
+ thinkingSource: ReviewThinkingSource,
240
+ requestedThinkingLevel: ReviewThinkingLevel | undefined,
241
+ currentThinkingLevel: ReviewThinkingLevel | undefined,
242
+ ): boolean {
243
+ if (!currentThinkingLevel) return false;
244
+ if (thinkingSource === "explicit") return true;
245
+ return requestedThinkingLevel !== undefined && requestedThinkingLevel !== currentThinkingLevel;
246
+ }
247
+
248
+ export function buildResolvedReviewModelLabel(
249
+ model: ResolvedReviewModel,
250
+ currentThinkingLevel: ReviewThinkingLevel | undefined,
251
+ ): string {
252
+ if (model.thinkingSource === "explicit") return model.modelPattern;
253
+ return shouldShowResolvedReviewThinkingSuffix(
254
+ model.thinkingSource,
255
+ model.requestedThinkingLevel,
256
+ currentThinkingLevel,
257
+ )
258
+ ? appendModelThinkingSuffix(model.modelPatternBase, currentThinkingLevel)
259
+ : model.modelPatternBase;
260
+ }
261
+
262
+ export function getResolvedReviewStatusModelArg(model: ResolvedReviewModel): string | undefined {
263
+ const providerCandidate = getResolvedReviewPrimaryProviderCandidate(model);
264
+ if (providerCandidate.modelArg) return providerCandidate.modelArg;
265
+ if (!providerCandidate.baseModelArg) return undefined;
266
+ return appendModelThinkingSuffix(
267
+ providerCandidate.baseModelArg,
268
+ getResolvedReviewCurrentThinkingLevel(model, providerCandidate),
269
+ );
270
+ }
271
+
272
+ export function buildResolvedReviewStatusModelLabel(model: ResolvedReviewModel): string {
273
+ const providerCandidate = getResolvedReviewPrimaryProviderCandidate(model);
274
+ const currentThinkingLevel = getResolvedReviewCurrentThinkingLevel(model, providerCandidate);
275
+ if (!providerCandidate.baseModelArg) {
276
+ return buildResolvedReviewModelLabel(model, currentThinkingLevel);
277
+ }
278
+
279
+ return shouldShowResolvedReviewThinkingSuffix(
280
+ model.thinkingSource,
281
+ model.requestedThinkingLevel,
282
+ currentThinkingLevel,
283
+ )
284
+ ? appendModelThinkingSuffix(providerCandidate.baseModelArg, currentThinkingLevel)
285
+ : providerCandidate.baseModelArg;
286
+ }
287
+
288
+ export function buildReviewProgressModelLabel(model: ResolvedReviewModel): string {
289
+ return getResolvedReviewStatusModelArg(model) ?? buildResolvedReviewStatusModelLabel(model);
290
+ }
291
+
292
+ function createResolvedReviewModel(options: {
293
+ modelPattern: string;
294
+ modelPatternBase: string;
295
+ thinkingSource: ReviewThinkingSource;
296
+ requestedThinkingLevel?: ReviewThinkingLevel;
297
+ providerCandidates: ResolvedReviewProviderCandidate[];
298
+ }): ResolvedReviewModel {
299
+ return {
300
+ modelPattern: options.modelPattern,
301
+ modelPatternBase: options.modelPatternBase,
302
+ thinkingSource: options.thinkingSource,
303
+ requestedThinkingLevel: options.requestedThinkingLevel,
304
+ providerCandidates:
305
+ options.providerCandidates.length > 0
306
+ ? options.providerCandidates
307
+ : [createResolvedReviewProviderCandidate({})],
308
+ };
309
+ }
310
+
311
+ export function selectReviewDedupModel(ctx: ExtensionContext): { modelArg: string } | null {
312
+ if (!ctx.model) return null;
313
+
314
+ const family = detectModelFamily(ctx.model.provider);
315
+ if (family) {
316
+ const modelId = family === "openai" ? OPENAI_FAST_MODEL_ID : ANTHROPIC_FAST_MODEL_ID;
317
+ const providerCandidates =
318
+ family === "openai"
319
+ ? [ctx.model.provider, "openai-codex", "openai"]
320
+ : [ctx.model.provider, "anthropic"];
321
+ const availableModels = ctx.modelRegistry.getAvailable();
322
+
323
+ for (const provider of new Set(providerCandidates)) {
324
+ const candidate = availableModels.find(
325
+ (model) => model.provider === provider && model.id === modelId,
326
+ );
327
+ if (!candidate) continue;
328
+
329
+ return {
330
+ modelArg: `${candidate.provider}/${candidate.id}`,
331
+ };
332
+ }
333
+ }
334
+
335
+ return {
336
+ modelArg: `${ctx.model.provider}/${ctx.model.id}`,
337
+ };
338
+ }
339
+
340
+ function rankModelCandidateGroup<T extends { id: string; provider: string }>(candidates: T[]): T[] {
341
+ if (candidates.length === 0) return [];
342
+
343
+ const aliases = candidates.filter(
344
+ (candidate) => candidate.id.endsWith("-latest") || !/-\d{8}$/.test(candidate.id),
345
+ );
346
+ const ranked = (aliases.length > 0 ? aliases : candidates).slice();
347
+ ranked.sort((a, b) => b.id.localeCompare(a.id));
348
+ return ranked;
349
+ }
350
+
351
+ function rankPreferredModelCandidates<T extends { id: string; provider: string }>(
352
+ candidates: T[],
353
+ currentProvider: string | undefined,
354
+ modelRegistry: ExtensionContext["modelRegistry"],
355
+ ): T[] {
356
+ if (candidates.length === 0) return [];
357
+
358
+ const preferredProviderCandidates = currentProvider
359
+ ? candidates.filter(
360
+ (candidate) => candidate.provider.toLowerCase() === currentProvider.toLowerCase(),
361
+ )
362
+ : [];
363
+ const otherProviderCandidates = currentProvider
364
+ ? candidates.filter(
365
+ (candidate) => candidate.provider.toLowerCase() !== currentProvider.toLowerCase(),
366
+ )
367
+ : candidates;
368
+
369
+ return [
370
+ ...rankModelCandidateGroup(preferredProviderCandidates),
371
+ ...rankModelCandidatesByProviderAuth(otherProviderCandidates, modelRegistry),
372
+ ];
373
+ }
374
+
375
+ function shouldTreatAsExplicitProviderPattern(
376
+ modelPattern: string,
377
+ availableModels: Array<Model<Api>>,
378
+ modelRegistry: ExtensionContext["modelRegistry"],
379
+ ): boolean {
380
+ const slash = modelPattern.indexOf("/");
381
+ if (slash <= 0) return false;
382
+
383
+ const providerPrefix = modelPattern.slice(0, slash);
384
+ const provider = availableModels.find(
385
+ (model) => model.provider.toLowerCase() === providerPrefix.toLowerCase(),
386
+ )?.provider;
387
+ if (!provider) return false;
388
+
389
+ const providerModel = availableModels.find(
390
+ (model) =>
391
+ model.provider.toLowerCase() === provider.toLowerCase() &&
392
+ model.id.toLowerCase() === modelPattern.slice(slash + 1).toLowerCase(),
393
+ );
394
+ const rawModelMatches = availableModels.filter(
395
+ (model) => model.id.toLowerCase() === modelPattern.toLowerCase(),
396
+ );
397
+ if (rawModelMatches.length === 0) return true;
398
+ if (!providerModel) return false;
399
+ if (modelRegistry.hasConfiguredAuth(providerModel)) return true;
400
+
401
+ return !rawModelMatches.some((model) => modelRegistry.hasConfiguredAuth(model));
402
+ }
403
+
404
+ function rankModelCandidatesByProviderAuth<T extends { id: string; provider: string }>(
405
+ candidates: T[],
406
+ modelRegistry: ExtensionContext["modelRegistry"],
407
+ ): T[] {
408
+ const grouped = new Map<number, T[]>();
409
+ for (const candidate of candidates) {
410
+ let rank: number;
411
+ switch (modelRegistry.getProviderAuthStatus(candidate.provider).source) {
412
+ case "stored":
413
+ case "runtime":
414
+ case "fallback":
415
+ case "models_json_key":
416
+ case "models_json_command":
417
+ rank = 0;
418
+ break;
419
+ case "environment":
420
+ rank = 1;
421
+ break;
422
+ default:
423
+ rank = 2;
424
+ break;
425
+ }
426
+
427
+ const group = grouped.get(rank) ?? [];
428
+ group.push(candidate);
429
+ grouped.set(rank, group);
430
+ }
431
+
432
+ return [...grouped.entries()]
433
+ .sort(([left], [right]) => left - right)
434
+ .flatMap(([, group]) => rankModelCandidateGroup(group));
435
+ }
436
+
437
+ function createResolvedReviewProviderCandidateFromModel(
438
+ model: Model<Api>,
439
+ ): ResolvedReviewProviderCandidate {
440
+ const supportedThinkingLevels = getSupportedThinkingLevels(model);
441
+ return createResolvedReviewProviderCandidate({
442
+ baseModelArg: `${model.provider}/${model.id}`,
443
+ supportsThinking: model.reasoning,
444
+ supportsXhigh: supportedThinkingLevels.includes("xhigh"),
445
+ supportsMax: supportedThinkingLevels.includes("max"),
446
+ });
447
+ }
448
+
449
+ function getProviderFallbackModelKey(model: { id: string }): string {
450
+ const slash = model.id.lastIndexOf("/");
451
+ return slash === -1 ? model.id : model.id.slice(slash + 1);
452
+ }
453
+
454
+ function resolveUnqualifiedModelPattern(
455
+ modelPattern: string,
456
+ availableModels: Array<Model<Api>>,
457
+ currentProvider: string | undefined,
458
+ modelRegistry: ExtensionContext["modelRegistry"],
459
+ inheritedThinkingLevel: ReviewThinkingLevel,
460
+ ): ResolvedReviewModel | undefined {
461
+ const { basePattern, thinkingSuffix } = splitModelPatternThinkingSuffix(modelPattern);
462
+ const thinkingSource: ReviewThinkingSource = thinkingSuffix ? "explicit" : "inherited";
463
+ const requestedThinkingLevel = thinkingSuffix ?? inheritedThinkingLevel;
464
+ const normalizedPattern = basePattern.toLowerCase();
465
+ const hasWildcard =
466
+ basePattern.includes("*") || basePattern.includes("?") || basePattern.includes("[");
467
+ const globMatcher = hasWildcard ? buildGlobRegExp(basePattern) : undefined;
468
+ const exactMatches = hasWildcard
469
+ ? []
470
+ : availableModels.filter((model) => model.id.toLowerCase() === normalizedPattern);
471
+ const candidates =
472
+ exactMatches.length > 0
473
+ ? exactMatches
474
+ : availableModels.filter((model) => {
475
+ if (globMatcher) {
476
+ const fullReference = `${model.provider}/${model.id}`;
477
+ return (
478
+ globMatcher.test(model.id) ||
479
+ (model.name ? globMatcher.test(model.name) : false) ||
480
+ globMatcher.test(fullReference)
481
+ );
482
+ }
483
+
484
+ const byId = model.id.toLowerCase().includes(normalizedPattern);
485
+ const byName = model.name?.toLowerCase().includes(normalizedPattern) ?? false;
486
+ return byId || byName;
487
+ });
488
+ if (candidates.length === 0) return undefined;
489
+
490
+ const rankedCandidates = rankPreferredModelCandidates(candidates, currentProvider, modelRegistry);
491
+ const preferredCandidate = rankedCandidates[0];
492
+ const providerFallbackCandidates = rankedCandidates.filter(
493
+ (candidate) =>
494
+ getProviderFallbackModelKey(candidate) === getProviderFallbackModelKey(preferredCandidate),
495
+ );
496
+
497
+ return createResolvedReviewModel({
498
+ modelPattern,
499
+ modelPatternBase: basePattern,
500
+ thinkingSource,
501
+ requestedThinkingLevel,
502
+ providerCandidates: providerFallbackCandidates.map(
503
+ createResolvedReviewProviderCandidateFromModel,
504
+ ),
505
+ });
506
+ }
507
+
508
+ export async function resolveModels(
509
+ ctx: ExtensionContext,
510
+ requestedModels: string[],
511
+ currentThinkingLevel: ReviewThinkingLevel,
512
+ ): Promise<ResolvedReviewModel[]> {
513
+ ctx.modelRegistry.refresh();
514
+ const currentProvider = typeof ctx.model?.provider === "string" ? ctx.model.provider : undefined;
515
+ const currentModelId = ctx.model?.id;
516
+ const allModels = ctx.modelRegistry.getAll();
517
+
518
+ const resolveRequestedModel = (modelPattern: string): ResolvedReviewModel => {
519
+ const { basePattern, thinkingSuffix } = splitModelPatternThinkingSuffix(modelPattern);
520
+ const thinkingSource: ReviewThinkingSource = thinkingSuffix ? "explicit" : "inherited";
521
+ const requestedThinkingLevel = thinkingSuffix ?? currentThinkingLevel;
522
+ const isExplicitProvider = shouldTreatAsExplicitProviderPattern(
523
+ basePattern,
524
+ allModels,
525
+ ctx.modelRegistry,
526
+ );
527
+ if (isExplicitProvider) {
528
+ const exactCandidate = allModels.find(
529
+ (model) => `${model.provider}/${model.id}`.toLowerCase() === basePattern.toLowerCase(),
530
+ );
531
+ return createResolvedReviewModel({
532
+ modelPattern,
533
+ modelPatternBase: basePattern,
534
+ thinkingSource,
535
+ requestedThinkingLevel,
536
+ providerCandidates: [
537
+ exactCandidate
538
+ ? createResolvedReviewProviderCandidateFromModel(exactCandidate)
539
+ : createResolvedReviewProviderCandidate({ modelArg: modelPattern }),
540
+ ],
541
+ });
542
+ }
543
+
544
+ const resolved = resolveUnqualifiedModelPattern(
545
+ modelPattern,
546
+ allModels,
547
+ currentProvider,
548
+ ctx.modelRegistry,
549
+ currentThinkingLevel,
550
+ );
551
+ if (resolved) return resolved;
552
+
553
+ return createResolvedReviewModel({
554
+ modelPattern,
555
+ modelPatternBase: basePattern,
556
+ thinkingSource,
557
+ requestedThinkingLevel,
558
+ providerCandidates: [createResolvedReviewProviderCandidate({ modelArg: modelPattern })],
559
+ });
560
+ };
561
+
562
+ if (requestedModels.length > 0) {
563
+ return requestedModels.map(resolveRequestedModel);
564
+ }
565
+
566
+ const modelPatternBase = currentModelId ?? "default";
567
+ const baseModelArg =
568
+ currentModelId && currentProvider ? `${currentProvider}/${currentModelId}` : currentModelId;
569
+ return [
570
+ createResolvedReviewModel({
571
+ modelPattern: modelPatternBase,
572
+ modelPatternBase,
573
+ thinkingSource: "inherited",
574
+ requestedThinkingLevel: currentThinkingLevel,
575
+ providerCandidates: [
576
+ createResolvedReviewProviderCandidate({
577
+ baseModelArg,
578
+ supportsThinking: ctx.model?.reasoning,
579
+ supportsXhigh: ctx.model
580
+ ? getSupportedThinkingLevels(ctx.model).includes("xhigh")
581
+ : undefined,
582
+ supportsMax: ctx.model
583
+ ? getSupportedThinkingLevels(ctx.model).includes("max")
584
+ : undefined,
585
+ }),
586
+ ],
587
+ }),
588
+ ];
589
+ }