@f5xc-salesdemos/xcsh 16.0.0 → 17.0.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.
Files changed (109) hide show
  1. package/package.json +7 -7
  2. package/src/async/index.ts +1 -0
  3. package/src/async/job-manager.ts +43 -10
  4. package/src/async/support.ts +5 -0
  5. package/src/cli/list-models.ts +96 -57
  6. package/src/commit/agentic/tools/analyze-file.ts +1 -2
  7. package/src/commit/model-selection.ts +16 -13
  8. package/src/config/model-equivalence.ts +675 -0
  9. package/src/config/model-registry.ts +252 -176
  10. package/src/config/model-resolver.ts +314 -89
  11. package/src/config/settings-schema.ts +67 -70
  12. package/src/config/settings.ts +9 -2
  13. package/src/dap/session.ts +1 -1
  14. package/src/debug/log-formatting.ts +2 -2
  15. package/src/edit/index.ts +254 -89
  16. package/src/edit/modes/chunk.ts +342 -58
  17. package/src/edit/modes/hashline.ts +51 -26
  18. package/src/edit/modes/patch.ts +16 -10
  19. package/src/edit/modes/replace.ts +15 -7
  20. package/src/edit/renderer.ts +248 -94
  21. package/src/export/html/template.css +82 -0
  22. package/src/export/html/template.generated.ts +1 -1
  23. package/src/export/html/template.js +614 -97
  24. package/src/extensibility/custom-tools/types.ts +0 -4
  25. package/src/extensibility/extensions/loader.ts +24 -4
  26. package/src/extensibility/extensions/runner.ts +2 -7
  27. package/src/extensibility/extensions/types.ts +9 -5
  28. package/src/internal-urls/docs-index.generated.ts +2 -2
  29. package/src/internal-urls/jobs-protocol.ts +2 -1
  30. package/src/ipy/executor.ts +449 -55
  31. package/src/ipy/kernel.ts +39 -13
  32. package/src/lsp/client.ts +59 -3
  33. package/src/lsp/index.ts +12 -9
  34. package/src/lsp/types.ts +6 -0
  35. package/src/lsp/utils.ts +26 -0
  36. package/src/main.ts +20 -18
  37. package/src/memories/index.ts +7 -6
  38. package/src/modes/acp/acp-agent.ts +6 -3
  39. package/src/modes/components/diff.ts +1 -1
  40. package/src/modes/components/model-selector.ts +224 -67
  41. package/src/modes/components/status-line/segments.ts +12 -0
  42. package/src/modes/components/status-line-segment-editor.ts +1 -0
  43. package/src/modes/components/tool-execution.ts +145 -75
  44. package/src/modes/components/welcome-checks.ts +23 -3
  45. package/src/modes/controllers/command-controller.ts +42 -1
  46. package/src/modes/controllers/extension-ui-controller.ts +31 -8
  47. package/src/modes/controllers/input-controller.ts +9 -3
  48. package/src/modes/controllers/selector-controller.ts +39 -133
  49. package/src/modes/interactive-mode.ts +7 -0
  50. package/src/modes/print-mode.ts +4 -0
  51. package/src/modes/prompt-action-autocomplete.ts +3 -5
  52. package/src/modes/rpc/rpc-mode.ts +8 -2
  53. package/src/modes/shared.ts +2 -2
  54. package/src/modes/theme/mermaid-cache.ts +5 -7
  55. package/src/modes/types.ts +1 -0
  56. package/src/modes/utils/ui-helpers.ts +1 -0
  57. package/src/priority.json +8 -0
  58. package/src/prompts/agents/designer.md +1 -2
  59. package/src/prompts/tools/bash.md +16 -1
  60. package/src/prompts/tools/cancel-job.md +1 -1
  61. package/src/prompts/tools/chunk-edit.md +196 -169
  62. package/src/prompts/tools/hashline.md +11 -11
  63. package/src/prompts/tools/patch.md +10 -5
  64. package/src/prompts/tools/{await.md → poll.md} +1 -1
  65. package/src/prompts/tools/read-chunk.md +16 -4
  66. package/src/prompts/tools/read.md +9 -0
  67. package/src/prompts/tools/task.md +2 -2
  68. package/src/prompts/tools/vim.md +98 -0
  69. package/src/prompts/tools/write.md +1 -0
  70. package/src/sdk.ts +764 -829
  71. package/src/session/agent-session.ts +206 -47
  72. package/src/session/compaction/compaction.ts +1 -1
  73. package/src/session/session-manager.ts +50 -4
  74. package/src/session/session-storage.ts +3 -0
  75. package/src/slash-commands/builtin-registry.ts +17 -0
  76. package/src/task/executor.ts +7 -1
  77. package/src/task/index.ts +3 -5
  78. package/src/task/types.ts +2 -2
  79. package/src/tools/bash.ts +247 -138
  80. package/src/tools/browser.ts +84 -21
  81. package/src/tools/cancel-job.ts +2 -1
  82. package/src/tools/fetch.ts +1 -1
  83. package/src/tools/find.ts +46 -97
  84. package/src/tools/gemini-image.ts +1 -0
  85. package/src/tools/grep.ts +77 -8
  86. package/src/tools/index.ts +50 -21
  87. package/src/tools/inspect-image.ts +1 -1
  88. package/src/tools/{await-tool.ts → poll-tool.ts} +38 -31
  89. package/src/tools/python.ts +35 -4
  90. package/src/tools/read.ts +218 -1
  91. package/src/tools/render-utils.ts +1 -1
  92. package/src/tools/sqlite-reader.ts +623 -0
  93. package/src/tools/submit-result.ts +5 -2
  94. package/src/tools/todo-write.ts +8 -2
  95. package/src/tools/vim.ts +971 -0
  96. package/src/tools/write.ts +189 -3
  97. package/src/utils/commit-message-generator.ts +1 -0
  98. package/src/utils/edit-mode.ts +2 -1
  99. package/src/utils/git.ts +24 -1
  100. package/src/utils/image-resize.ts +73 -37
  101. package/src/utils/session-color.ts +55 -0
  102. package/src/utils/title-generator.ts +16 -7
  103. package/src/vim/buffer.ts +309 -0
  104. package/src/vim/commands.ts +382 -0
  105. package/src/vim/engine.ts +2426 -0
  106. package/src/vim/parser.ts +151 -0
  107. package/src/vim/render.ts +252 -0
  108. package/src/vim/types.ts +197 -0
  109. package/src/web/search/providers/codex.ts +21 -2
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "16.0.0",
4
+ "version": "17.0.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5xc-salesdemos/xcsh",
7
7
  "author": "Can Boluk",
@@ -46,12 +46,12 @@
46
46
  "dependencies": {
47
47
  "@agentclientprotocol/sdk": "0.16.1",
48
48
  "@mozilla/readability": "^0.6",
49
- "@f5xc-salesdemos/xcsh-stats": "16.0.0",
50
- "@f5xc-salesdemos/pi-agent-core": "16.0.0",
51
- "@f5xc-salesdemos/pi-ai": "16.0.0",
52
- "@f5xc-salesdemos/pi-natives": "16.0.0",
53
- "@f5xc-salesdemos/pi-tui": "16.0.0",
54
- "@f5xc-salesdemos/pi-utils": "16.0.0",
49
+ "@f5xc-salesdemos/xcsh-stats": "17.0.1",
50
+ "@f5xc-salesdemos/pi-agent-core": "17.0.1",
51
+ "@f5xc-salesdemos/pi-ai": "17.0.1",
52
+ "@f5xc-salesdemos/pi-natives": "17.0.1",
53
+ "@f5xc-salesdemos/pi-tui": "17.0.1",
54
+ "@f5xc-salesdemos/pi-utils": "17.0.1",
55
55
  "@sinclair/typebox": "^0.34",
56
56
  "@xterm/headless": "^6.0",
57
57
  "ajv": "^8.18",
@@ -1 +1,2 @@
1
1
  export * from "./job-manager";
2
+ export * from "./support";
@@ -1,4 +1,4 @@
1
- import { logger, Snowflake } from "@f5xc-salesdemos/pi-utils";
1
+ import { logger } from "@f5xc-salesdemos/pi-utils";
2
2
 
3
3
  const DELIVERY_RETRY_BASE_MS = 500;
4
4
  const DELIVERY_RETRY_MAX_MS = 30_000;
@@ -48,6 +48,7 @@ export class AsyncJobManager {
48
48
  readonly #jobs = new Map<string, AsyncJob>();
49
49
  readonly #deliveries: AsyncJobDelivery[] = [];
50
50
  readonly #suppressedDeliveries = new Set<string>();
51
+ readonly #watchedJobs = new Set<string>();
51
52
  readonly #evictionTimers = new Map<string, NodeJS.Timeout>();
52
53
  readonly #onJobComplete: AsyncJobManagerOptions["onJobComplete"];
53
54
  readonly #maxRunningJobs: number;
@@ -184,6 +185,25 @@ export class AsyncJobManager {
184
185
  return this.#deliveries.length > 0;
185
186
  }
186
187
 
188
+ watchJobs(jobIds: string[]): number {
189
+ const uniqueJobIds = Array.from(new Set(jobIds.map(id => id.trim()).filter(id => id.length > 0)));
190
+ for (const jobId of uniqueJobIds) {
191
+ this.#watchedJobs.add(jobId);
192
+ }
193
+ return uniqueJobIds.length;
194
+ }
195
+
196
+ unwatchJobs(jobIds: string[]): number {
197
+ const uniqueJobIds = Array.from(new Set(jobIds.map(id => id.trim()).filter(id => id.length > 0)));
198
+ let removed = 0;
199
+ for (const jobId of uniqueJobIds) {
200
+ if (this.#watchedJobs.delete(jobId)) {
201
+ removed += 1;
202
+ }
203
+ }
204
+ return removed;
205
+ }
206
+
187
207
  acknowledgeDeliveries(jobIds: string[]): number {
188
208
  const uniqueJobIds = Array.from(new Set(jobIds.map(id => id.trim()).filter(id => id.length > 0)));
189
209
  if (uniqueJobIds.length === 0) return 0;
@@ -196,7 +216,7 @@ export class AsyncJobManager {
196
216
  this.#deliveries.splice(
197
217
  0,
198
218
  this.#deliveries.length,
199
- ...this.#deliveries.filter(delivery => !this.#suppressedDeliveries.has(delivery.jobId)),
219
+ ...this.#deliveries.filter(delivery => !this.isDeliverySuppressed(delivery.jobId)),
200
220
  );
201
221
  return before - this.#deliveries.length;
202
222
  }
@@ -254,12 +274,21 @@ export class AsyncJobManager {
254
274
  this.#jobs.clear();
255
275
  this.#deliveries.length = 0;
256
276
  this.#suppressedDeliveries.clear();
277
+ this.#watchedJobs.clear();
257
278
  return drained;
258
279
  }
259
280
 
260
281
  #resolveJobId(preferredId?: string): string {
261
- if (!preferredId || preferredId.trim().length === 0) {
262
- return `bg_${Snowflake.next()}`;
282
+ preferredId = preferredId?.trim();
283
+ if (!preferredId) {
284
+ let candidate = 1;
285
+ while (true) {
286
+ const id = `bg_${candidate}`;
287
+ if (!this.#jobs.has(id)) {
288
+ return id;
289
+ }
290
+ candidate += 1;
291
+ }
263
292
  }
264
293
 
265
294
  const base = preferredId.trim();
@@ -278,6 +307,7 @@ export class AsyncJobManager {
278
307
  if (this.#retentionMs <= 0) {
279
308
  this.#jobs.delete(jobId);
280
309
  this.#suppressedDeliveries.delete(jobId);
310
+ this.#watchedJobs.delete(jobId);
281
311
  return;
282
312
  }
283
313
  const existing = this.#evictionTimers.get(jobId);
@@ -288,6 +318,7 @@ export class AsyncJobManager {
288
318
  this.#evictionTimers.delete(jobId);
289
319
  this.#jobs.delete(jobId);
290
320
  this.#suppressedDeliveries.delete(jobId);
321
+ this.#watchedJobs.delete(jobId);
291
322
  }, this.#retentionMs);
292
323
  timer.unref();
293
324
  this.#evictionTimers.set(jobId, timer);
@@ -300,12 +331,13 @@ export class AsyncJobManager {
300
331
  this.#evictionTimers.clear();
301
332
  }
302
333
 
303
- #isDeliverySuppressed(jobId: string): boolean {
304
- return this.#suppressedDeliveries.has(jobId);
334
+ isDeliverySuppressed(jobId: string): boolean {
335
+ return this.#suppressedDeliveries.has(jobId) || this.#watchedJobs.has(jobId);
305
336
  }
306
337
 
307
338
  #enqueueDelivery(jobId: string, text: string): void {
308
- if (this.#isDeliverySuppressed(jobId)) {
339
+ // Skip delivery if already acknowledged
340
+ if (this.isDeliverySuppressed(jobId)) {
309
341
  return;
310
342
  }
311
343
  this.#deliveries.push({
@@ -337,7 +369,7 @@ export class AsyncJobManager {
337
369
  async #runDeliveryLoop(): Promise<void> {
338
370
  while (this.#deliveries.length > 0) {
339
371
  const delivery = this.#deliveries[0];
340
- if (this.#isDeliverySuppressed(delivery.jobId)) {
372
+ if (this.isDeliverySuppressed(delivery.jobId)) {
341
373
  this.#deliveries.shift();
342
374
  continue;
343
375
  }
@@ -348,7 +380,8 @@ export class AsyncJobManager {
348
380
  if (this.#deliveries[0] !== delivery) {
349
381
  continue;
350
382
  }
351
- if (this.#isDeliverySuppressed(delivery.jobId)) {
383
+ // Check again after sleep
384
+ if (this.isDeliverySuppressed(delivery.jobId)) {
352
385
  this.#deliveries.shift();
353
386
  continue;
354
387
  }
@@ -361,7 +394,7 @@ export class AsyncJobManager {
361
394
  delivery.lastError = error instanceof Error ? error.message : String(error);
362
395
  delivery.nextAttemptAt = Date.now() + this.#getRetryDelay(delivery.attempt);
363
396
  this.#deliveries.shift();
364
- if (!this.#isDeliverySuppressed(delivery.jobId)) {
397
+ if (!this.isDeliverySuppressed(delivery.jobId)) {
365
398
  this.#deliveries.push(delivery);
366
399
  }
367
400
  logger.warn("Async job completion delivery failed", {
@@ -0,0 +1,5 @@
1
+ import type { Settings } from "../config/settings";
2
+
3
+ export function isBackgroundJobSupportEnabled(settings: Pick<Settings, "get">): boolean {
4
+ return settings.get("async.enabled") || settings.get("bash.autoBackground.enabled");
5
+ }
@@ -6,6 +6,45 @@ import { formatNumber } from "@f5xc-salesdemos/pi-utils";
6
6
  import type { ModelRegistry } from "../config/model-registry";
7
7
  import { fuzzyFilter } from "../utils/fuzzy";
8
8
 
9
+ interface ProviderRow {
10
+ provider: string;
11
+ model: string;
12
+ context: string;
13
+ maxOut: string;
14
+ thinking: string;
15
+ images: string;
16
+ }
17
+
18
+ interface CanonicalRow {
19
+ canonical: string;
20
+ selected: string;
21
+ variants: string;
22
+ context: string;
23
+ maxOut: string;
24
+ }
25
+
26
+ function writeLine(line = ""): void {
27
+ process.stdout.write(`${line}\n`);
28
+ }
29
+
30
+ function renderTable<T extends Record<string, string>>(rows: T[], headers: T): void {
31
+ const widths = Object.fromEntries(
32
+ Object.keys(headers).map(key => [key, Math.max(headers[key]!.length, ...rows.map(row => row[key]!.length))]),
33
+ ) as Record<keyof T, number>;
34
+
35
+ const headerLine = Object.keys(headers)
36
+ .map(key => headers[key as keyof T]!.padEnd(widths[key as keyof T]))
37
+ .join(" ");
38
+ writeLine(headerLine);
39
+
40
+ for (const row of rows) {
41
+ const line = Object.keys(headers)
42
+ .map(key => row[key as keyof T]!.padEnd(widths[key as keyof T]))
43
+ .join(" ");
44
+ writeLine(line);
45
+ }
46
+ }
47
+
9
48
  /**
10
49
  * List available models, optionally filtered by search pattern
11
50
  */
@@ -13,77 +52,77 @@ export async function listModels(modelRegistry: ModelRegistry, searchPattern?: s
13
52
  const models = modelRegistry.getAvailable();
14
53
 
15
54
  if (models.length === 0) {
16
- console.log("No models available. Set API keys in environment variables.");
55
+ writeLine("No models available. Set API keys in environment variables.");
17
56
  return;
18
57
  }
19
58
 
20
- // Apply fuzzy filter if search pattern provided
21
59
  let filteredModels: Model<Api>[] = models;
22
60
  if (searchPattern) {
23
- filteredModels = fuzzyFilter(models, searchPattern, m => `${m.provider} ${m.id}`);
61
+ filteredModels = fuzzyFilter(models, searchPattern, model => `${model.provider} ${model.id}`);
24
62
  }
25
63
 
26
- if (filteredModels.length === 0) {
27
- console.log(`No models matching "${searchPattern}"`);
64
+ const filteredCanonical = modelRegistry
65
+ .getCanonicalModels({ availableOnly: true, candidates: filteredModels })
66
+ .map(record => {
67
+ const selected = modelRegistry.resolveCanonicalModel(record.id, {
68
+ availableOnly: true,
69
+ candidates: filteredModels,
70
+ });
71
+ if (!selected) return undefined;
72
+ return {
73
+ canonical: record.id,
74
+ selected: `${selected.provider}/${selected.id}`,
75
+ variants: String(record.variants.length),
76
+ context: formatNumber(selected.contextWindow),
77
+ maxOut: formatNumber(selected.maxTokens),
78
+ } satisfies CanonicalRow;
79
+ })
80
+ .filter((row): row is CanonicalRow => row !== undefined)
81
+ .sort((left, right) => left.canonical.localeCompare(right.canonical));
82
+
83
+ if (filteredModels.length === 0 && filteredCanonical.length === 0) {
84
+ writeLine(`No models matching "${searchPattern}"`);
28
85
  return;
29
86
  }
30
87
 
31
- // Sort by provider, then by model id
32
- filteredModels.sort((a, b) => {
33
- const providerCmp = a.provider.localeCompare(b.provider);
88
+ filteredModels.sort((left, right) => {
89
+ const providerCmp = left.provider.localeCompare(right.provider);
34
90
  if (providerCmp !== 0) return providerCmp;
35
- return a.id.localeCompare(b.id);
91
+ return left.id.localeCompare(right.id);
36
92
  });
37
93
 
38
- // Calculate column widths
39
- const rows = filteredModels.map(m => ({
40
- provider: m.provider,
41
- model: m.id,
42
- context: formatNumber(m.contextWindow),
43
- maxOut: formatNumber(m.maxTokens),
44
- thinking: m.thinking ? getSupportedEfforts(m).join(",") : m.reasoning ? "yes" : "-",
45
- images: m.input.includes("image") ? "yes" : "no",
46
- }));
94
+ const providerRows = filteredModels.map(model => ({
95
+ provider: model.provider,
96
+ model: model.id,
97
+ context: formatNumber(model.contextWindow),
98
+ maxOut: formatNumber(model.maxTokens),
99
+ thinking: model.thinking ? getSupportedEfforts(model).join(",") : model.reasoning ? "yes" : "-",
100
+ images: model.input.includes("image") ? "yes" : "no",
101
+ })) satisfies ProviderRow[];
47
102
 
48
- const headers = {
49
- provider: "provider",
50
- model: "model",
51
- context: "context",
52
- maxOut: "max-out",
53
- thinking: "thinking",
54
- images: "images",
55
- };
56
-
57
- const widths = {
58
- provider: Math.max(headers.provider.length, ...rows.map(r => r.provider.length)),
59
- model: Math.max(headers.model.length, ...rows.map(r => r.model.length)),
60
- context: Math.max(headers.context.length, ...rows.map(r => r.context.length)),
61
- maxOut: Math.max(headers.maxOut.length, ...rows.map(r => r.maxOut.length)),
62
- thinking: Math.max(headers.thinking.length, ...rows.map(r => r.thinking.length)),
63
- images: Math.max(headers.images.length, ...rows.map(r => r.images.length)),
64
- };
65
-
66
- // Print header
67
- const headerLine = [
68
- headers.provider.padEnd(widths.provider),
69
- headers.model.padEnd(widths.model),
70
- headers.context.padEnd(widths.context),
71
- headers.maxOut.padEnd(widths.maxOut),
72
- headers.thinking.padEnd(widths.thinking),
73
- headers.images.padEnd(widths.images),
74
- ].join(" ");
75
- console.log(headerLine);
103
+ if (filteredCanonical.length > 0) {
104
+ writeLine("Canonical models");
105
+ renderTable(filteredCanonical, {
106
+ canonical: "canonical",
107
+ selected: "selected",
108
+ variants: "variants",
109
+ context: "context",
110
+ maxOut: "max-out",
111
+ });
112
+ if (providerRows.length > 0) {
113
+ writeLine();
114
+ }
115
+ }
76
116
 
77
- // Print rows
78
- for (const row of rows) {
79
- const line = [
80
- row.provider.padEnd(widths.provider),
81
- row.model.padEnd(widths.model),
82
- row.context.padEnd(widths.context),
83
- row.maxOut.padEnd(widths.maxOut),
84
- row.thinking.padEnd(widths.thinking),
85
- row.images.padEnd(widths.images),
86
- ].join(" ");
87
- console.log(line);
117
+ if (providerRows.length > 0) {
118
+ writeLine("Provider models");
119
+ renderTable(providerRows, {
120
+ provider: "provider",
121
+ model: "model",
122
+ context: "context",
123
+ maxOut: "max-out",
124
+ thinking: "thinking",
125
+ images: "images",
126
+ });
88
127
  }
89
128
  }
@@ -43,7 +43,6 @@ function buildToolSession(
43
43
  settings: options.settings,
44
44
  authStorage: options.authStorage,
45
45
  modelRegistry: options.modelRegistry,
46
- searchDb: ctx.searchDb,
47
46
  };
48
47
  }
49
48
 
@@ -79,7 +78,7 @@ export function createAnalyzeFileTool(options: {
79
78
  });
80
79
  const taskParams: TaskParams = {
81
80
  agent: "quick_task",
82
- schema: analyzeFileOutputSchema,
81
+ schema: JSON.stringify(analyzeFileOutputSchema),
83
82
  tasks,
84
83
  };
85
84
  return taskTool.execute(toolCallId, taskParams, signal, onUpdate);
@@ -1,7 +1,12 @@
1
1
  import type { ThinkingLevel } from "@f5xc-salesdemos/pi-agent-core";
2
2
  import type { Api, Model } from "@f5xc-salesdemos/pi-ai";
3
3
  import { MODEL_ROLE_IDS } from "../config/model-registry";
4
- import { parseModelPattern, resolveModelRoleValue, resolveRoleSelection } from "../config/model-resolver";
4
+ import {
5
+ type ModelLookupRegistry,
6
+ parseModelPattern,
7
+ resolveModelRoleValue,
8
+ resolveRoleSelection,
9
+ } from "../config/model-resolver";
5
10
  import type { Settings } from "../config/settings";
6
11
  import MODEL_PRIO from "../priority.json" with { type: "json" };
7
12
 
@@ -11,19 +16,20 @@ export interface ResolvedCommitModel {
11
16
  thinkingLevel?: ThinkingLevel;
12
17
  }
13
18
 
19
+ type CommitModelRegistry = ModelLookupRegistry & {
20
+ getApiKey: (model: Model<Api>) => Promise<string | undefined>;
21
+ };
22
+
14
23
  export async function resolvePrimaryModel(
15
24
  override: string | undefined,
16
25
  settings: Settings,
17
- modelRegistry: {
18
- getAvailable: () => Model<Api>[];
19
- getApiKey: (model: Model<Api>) => Promise<string | undefined>;
20
- },
26
+ modelRegistry: CommitModelRegistry,
21
27
  ): Promise<ResolvedCommitModel> {
22
28
  const available = modelRegistry.getAvailable();
23
29
  const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
24
30
  const resolved = override
25
- ? resolveModelRoleValue(override, available, { settings, matchPreferences })
26
- : resolveRoleSelection(["commit", "smol", ...MODEL_ROLE_IDS], settings, available);
31
+ ? resolveModelRoleValue(override, available, { settings, matchPreferences, modelRegistry })
32
+ : resolveRoleSelection(["commit", "smol", ...MODEL_ROLE_IDS], settings, available, modelRegistry);
27
33
  const model = resolved?.model;
28
34
  if (!model) {
29
35
  throw new Error("No model available for commit generation");
@@ -37,15 +43,12 @@ export async function resolvePrimaryModel(
37
43
 
38
44
  export async function resolveSmolModel(
39
45
  settings: Settings,
40
- modelRegistry: {
41
- getAvailable: () => Model<Api>[];
42
- getApiKey: (model: Model<Api>) => Promise<string | undefined>;
43
- },
46
+ modelRegistry: CommitModelRegistry,
44
47
  fallbackModel: Model<Api>,
45
48
  fallbackApiKey: string,
46
49
  ): Promise<ResolvedCommitModel> {
47
50
  const available = modelRegistry.getAvailable();
48
- const resolvedSmol = resolveRoleSelection(["smol"], settings, available);
51
+ const resolvedSmol = resolveRoleSelection(["smol"], settings, available, modelRegistry);
49
52
  if (resolvedSmol?.model) {
50
53
  const apiKey = await modelRegistry.getApiKey(resolvedSmol.model);
51
54
  if (apiKey) return { model: resolvedSmol.model, apiKey, thinkingLevel: resolvedSmol.thinkingLevel };
@@ -53,7 +56,7 @@ export async function resolveSmolModel(
53
56
 
54
57
  const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
55
58
  for (const pattern of MODEL_PRIO.smol) {
56
- const candidate = parseModelPattern(pattern, available, matchPreferences).model;
59
+ const candidate = parseModelPattern(pattern, available, matchPreferences, { modelRegistry }).model;
57
60
  if (!candidate) continue;
58
61
  const apiKey = await modelRegistry.getApiKey(candidate);
59
62
  if (apiKey) return { model: candidate, apiKey };