@oh-my-pi/pi-coding-agent 14.0.4 → 14.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/package.json +11 -8
  3. package/src/async/index.ts +1 -0
  4. package/src/async/support.ts +5 -0
  5. package/src/cli/list-models.ts +96 -57
  6. package/src/commit/model-selection.ts +16 -13
  7. package/src/config/model-equivalence.ts +674 -0
  8. package/src/config/model-registry.ts +182 -13
  9. package/src/config/model-resolver.ts +203 -74
  10. package/src/config/settings-schema.ts +23 -0
  11. package/src/config/settings.ts +9 -2
  12. package/src/dap/session.ts +31 -39
  13. package/src/debug/log-formatting.ts +2 -2
  14. package/src/edit/modes/chunk.ts +8 -3
  15. package/src/export/html/template.css +82 -0
  16. package/src/export/html/template.generated.ts +1 -1
  17. package/src/export/html/template.js +612 -97
  18. package/src/internal-urls/docs-index.generated.ts +1 -1
  19. package/src/internal-urls/jobs-protocol.ts +2 -1
  20. package/src/lsp/client.ts +5 -3
  21. package/src/lsp/index.ts +4 -9
  22. package/src/lsp/utils.ts +26 -0
  23. package/src/main.ts +6 -1
  24. package/src/memories/index.ts +7 -6
  25. package/src/modes/components/diff.ts +1 -1
  26. package/src/modes/components/model-selector.ts +221 -64
  27. package/src/modes/controllers/command-controller.ts +18 -0
  28. package/src/modes/controllers/event-controller.ts +438 -426
  29. package/src/modes/controllers/selector-controller.ts +13 -5
  30. package/src/modes/theme/mermaid-cache.ts +5 -7
  31. package/src/priority.json +8 -0
  32. package/src/prompts/agents/designer.md +1 -2
  33. package/src/prompts/system/system-prompt.md +5 -1
  34. package/src/prompts/tools/bash.md +15 -0
  35. package/src/prompts/tools/cancel-job.md +1 -1
  36. package/src/prompts/tools/chunk-edit.md +39 -40
  37. package/src/prompts/tools/read-chunk.md +13 -1
  38. package/src/prompts/tools/read.md +9 -0
  39. package/src/prompts/tools/write.md +1 -0
  40. package/src/sdk.ts +7 -4
  41. package/src/session/agent-session.ts +33 -6
  42. package/src/session/compaction/compaction.ts +1 -1
  43. package/src/task/executor.ts +5 -1
  44. package/src/tools/await-tool.ts +2 -1
  45. package/src/tools/bash.ts +221 -56
  46. package/src/tools/browser.ts +84 -21
  47. package/src/tools/cancel-job.ts +2 -1
  48. package/src/tools/fetch.ts +1 -1
  49. package/src/tools/find.ts +40 -94
  50. package/src/tools/gemini-image.ts +1 -0
  51. package/src/tools/inspect-image.ts +1 -1
  52. package/src/tools/read.ts +218 -1
  53. package/src/tools/render-utils.ts +1 -1
  54. package/src/tools/sqlite-reader.ts +623 -0
  55. package/src/tools/write.ts +187 -1
  56. package/src/utils/commit-message-generator.ts +1 -0
  57. package/src/utils/git.ts +24 -1
  58. package/src/utils/image-resize.ts +73 -37
  59. package/src/utils/title-generator.ts +1 -1
  60. package/src/web/scrapers/types.ts +50 -32
  61. package/src/web/search/providers/codex.ts +21 -2
@@ -58,6 +58,10 @@ 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
+
61
65
  export interface ModelMatchPreferences {
62
66
  /** Most-recently-used model keys (provider/modelId) to prefer when ambiguous. */
63
67
  usageOrder?: string[];
@@ -65,6 +69,14 @@ export interface ModelMatchPreferences {
65
69
  deprioritizeProviders?: string[];
66
70
  }
67
71
 
72
+ export type CanonicalModelRegistry = Partial<
73
+ Pick<ModelRegistry, "resolveCanonicalModel" | "getCanonicalVariants" | "getCanonicalId">
74
+ >;
75
+ export type ModelLookupRegistry = Pick<ModelRegistry, "getAvailable"> & Partial<CanonicalModelRegistry>;
76
+ type CliModelRegistry = Pick<ModelRegistry, "getAll"> & Partial<CanonicalModelRegistry>;
77
+ type InitialModelRegistry = Pick<ModelRegistry, "getAvailable" | "find">;
78
+ type RestorableModelRegistry = Pick<ModelRegistry, "getAvailable" | "find" | "getApiKey">;
79
+
68
80
  interface ModelPreferenceContext {
69
81
  modelUsageRank: Map<string, number>;
70
82
  providerUsageRank: Map<string, number>;
@@ -142,9 +154,8 @@ function isAlias(id: string): boolean {
142
154
  }
143
155
 
144
156
  /**
145
- * Find an exact model reference match.
146
- * Supports either a bare model id or a canonical provider/modelId reference.
147
- * When matching by bare id, ambiguous matches across providers are rejected.
157
+ * Find an exact explicit provider/model match.
158
+ * Bare model ids are handled separately so canonical ids can coalesce variants.
148
159
  */
149
160
  export function findExactModelReferenceMatch(
150
161
  modelReference: string,
@@ -155,18 +166,6 @@ export function findExactModelReferenceMatch(
155
166
  return undefined;
156
167
  }
157
168
 
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
169
  const slashIndex = trimmedReference.indexOf("/");
171
170
  if (slashIndex !== -1) {
172
171
  const provider = trimmedReference.substring(0, slashIndex).trim();
@@ -185,9 +184,25 @@ export function findExactModelReferenceMatch(
185
184
  }
186
185
  }
187
186
  }
187
+ return undefined;
188
+ }
188
189
 
189
- const idMatches = availableModels.filter(model => model.id.toLowerCase() === normalizedReference);
190
- return idMatches.length === 1 ? idMatches[0] : undefined;
190
+ function findExactCanonicalModelMatch(
191
+ modelReference: string,
192
+ availableModels: Model<Api>[],
193
+ modelRegistry: CanonicalModelRegistry | undefined,
194
+ ): Model<Api> | undefined {
195
+ if (!modelRegistry) {
196
+ return undefined;
197
+ }
198
+ const trimmedReference = modelReference.trim();
199
+ if (!trimmedReference || trimmedReference.includes("/")) {
200
+ return undefined;
201
+ }
202
+ return modelRegistry.resolveCanonicalModel?.(trimmedReference, {
203
+ availableOnly: false,
204
+ candidates: availableModels,
205
+ });
191
206
  }
192
207
 
193
208
  /**
@@ -198,13 +213,20 @@ function tryMatchModel(
198
213
  modelPattern: string,
199
214
  availableModels: Model<Api>[],
200
215
  context: ModelPreferenceContext,
216
+ options?: { modelRegistry?: CanonicalModelRegistry },
201
217
  ): Model<Api> | undefined {
202
- // Try exact reference match first (handles provider/modelId and bare id with ambiguity rejection)
218
+ // Explicit provider/model selectors always bypass canonical coalescing.
203
219
  const exactRefMatch = findExactModelReferenceMatch(modelPattern, availableModels);
204
220
  if (exactRefMatch) {
205
221
  return exactRefMatch;
206
222
  }
207
223
 
224
+ // Exact canonical ids coalesce provider variants before bare-id matching.
225
+ const exactCanonicalMatch = findExactCanonicalModelMatch(modelPattern, availableModels, options?.modelRegistry);
226
+ if (exactCanonicalMatch) {
227
+ return exactCanonicalMatch;
228
+ }
229
+
208
230
  // Check for provider/modelId format — fuzzy match within provider
209
231
  const slashIndex = modelPattern.indexOf("/");
210
232
  if (slashIndex !== -1) {
@@ -300,10 +322,10 @@ function parseModelPatternWithContext(
300
322
  pattern: string,
301
323
  availableModels: Model<Api>[],
302
324
  context: ModelPreferenceContext,
303
- options?: { allowInvalidThinkingSelectorFallback?: boolean },
325
+ options?: { allowInvalidThinkingSelectorFallback?: boolean; modelRegistry?: CanonicalModelRegistry },
304
326
  ): ParsedModelResult {
305
327
  // Try exact match first
306
- const exactMatch = tryMatchModel(pattern, availableModels, context);
328
+ const exactMatch = tryMatchModel(pattern, availableModels, context, options);
307
329
  if (exactMatch) {
308
330
  return { model: exactMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
309
331
  }
@@ -357,7 +379,7 @@ export function parseModelPattern(
357
379
  pattern: string,
358
380
  availableModels: Model<Api>[],
359
381
  preferences?: ModelMatchPreferences,
360
- options?: { allowInvalidThinkingSelectorFallback?: boolean },
382
+ options?: { allowInvalidThinkingSelectorFallback?: boolean; modelRegistry?: CanonicalModelRegistry },
361
383
  ): ParsedModelResult {
362
384
  const context = buildPreferenceContext(availableModels, preferences);
363
385
  return parseModelPatternWithContext(pattern, availableModels, context, options);
@@ -387,7 +409,7 @@ function isSessionInheritedAgentPattern(value: string): boolean {
387
409
  return value === DEFAULT_MODEL_ROLE || value === `${PREFIX_MODEL_ROLE}${DEFAULT_MODEL_ROLE}` || value === "pi/task";
388
410
  }
389
411
 
390
- function resolveConfiguredRolePattern(value: string, settings?: Settings): string | undefined {
412
+ function resolveConfiguredRolePattern(value: string, settings?: Settings): string[] | undefined {
391
413
  const normalized = value.trim();
392
414
  if (!normalized) return undefined;
393
415
 
@@ -396,11 +418,16 @@ function resolveConfiguredRolePattern(value: string, settings?: Settings): strin
396
418
  lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(normalized.slice(lastColonIndex + 1)) : undefined;
397
419
  const aliasCandidate = thinkingLevel ? normalized.slice(0, lastColonIndex) : normalized;
398
420
  const role = getModelRoleAlias(aliasCandidate);
399
- if (!role) return normalized;
421
+ if (!role) return [normalized];
400
422
 
401
423
  const configured = settings?.getModelRole(role)?.trim();
402
- if (!configured) return undefined;
403
- return thinkingLevel ? `${configured}:${thinkingLevel}` : configured;
424
+ const roleDefaults = normalizeModelPatternList(MODEL_PRIO[role as keyof typeof MODEL_PRIO]);
425
+ const resolved = configured ? normalizeModelPatternList(configured) : roleDefaults;
426
+ if (!resolved || resolved.length === 0) {
427
+ return undefined;
428
+ }
429
+
430
+ return thinkingLevel ? resolved.map(pattern => `${pattern}:${thinkingLevel}`) : resolved;
404
431
  }
405
432
 
406
433
  /**
@@ -412,7 +439,7 @@ export function expandRoleAlias(value: string, settings?: Settings): string {
412
439
  return settings?.getModelRole("default") ?? value;
413
440
  }
414
441
 
415
- const resolved = resolveConfiguredRolePattern(value, settings);
442
+ const resolved = resolveConfiguredRolePattern(value, settings)?.[0];
416
443
  return resolved ?? value;
417
444
  }
418
445
 
@@ -420,10 +447,9 @@ export function resolveConfiguredModelPatterns(value: string | string[] | undefi
420
447
  const patterns = normalizeModelPatternList(value);
421
448
  return patterns.flatMap(pattern => {
422
449
  const resolved = resolveConfiguredRolePattern(pattern, settings);
423
- return resolved ? [resolved] : [];
450
+ return resolved ?? [];
424
451
  });
425
452
  }
426
-
427
453
  export interface AgentModelPatternResolutionOptions {
428
454
  settingsOverride?: string | string[];
429
455
  agentModel?: string | string[];
@@ -465,7 +491,7 @@ export interface ResolvedModelRoleValue {
465
491
  export function resolveModelRoleValue(
466
492
  roleValue: string | undefined,
467
493
  availableModels: Model<Api>[],
468
- options?: { settings?: Settings; matchPreferences?: ModelMatchPreferences },
494
+ options?: { settings?: Settings; matchPreferences?: ModelMatchPreferences; modelRegistry?: CanonicalModelRegistry },
469
495
  ): ResolvedModelRoleValue {
470
496
  if (!roleValue) {
471
497
  return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
@@ -477,28 +503,34 @@ export function resolveModelRoleValue(
477
503
  }
478
504
 
479
505
  const lastColonIndex = normalized.lastIndexOf(":");
480
- const thinkingSelector =
506
+ const _thinkingSelector =
481
507
  lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(normalized.slice(lastColonIndex + 1)) : undefined;
482
- const aliasCandidate = thinkingSelector ? normalized.slice(0, lastColonIndex) : normalized;
483
- const effectivePattern = resolveConfiguredRolePattern(aliasCandidate, options?.settings);
484
- if (!effectivePattern) {
508
+ const effectivePatterns = resolveConfiguredRolePattern(normalized, options?.settings);
509
+ if (!effectivePatterns || effectivePatterns.length === 0) {
485
510
  return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
486
511
  }
487
- const patternWithSuffix = thinkingSelector ? `${effectivePattern}:${thinkingSelector}` : effectivePattern;
488
- const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPattern(
489
- patternWithSuffix,
490
- availableModels,
491
- options?.matchPreferences,
492
- );
493
512
 
494
- return {
495
- model,
496
- thinkingLevel: explicitThinkingLevel
497
- ? (resolveThinkingLevelForModel(model, thinkingLevel) ?? thinkingLevel)
498
- : thinkingLevel,
499
- explicitThinkingLevel,
500
- warning,
501
- };
513
+ let warning: string | undefined;
514
+ for (const effectivePattern of effectivePatterns) {
515
+ const resolved = parseModelPattern(effectivePattern, availableModels, options?.matchPreferences, {
516
+ modelRegistry: options?.modelRegistry,
517
+ });
518
+ if (resolved.model) {
519
+ return {
520
+ model: resolved.model,
521
+ thinkingLevel: resolved.explicitThinkingLevel
522
+ ? (resolveThinkingLevelForModel(resolved.model, resolved.thinkingLevel) ?? resolved.thinkingLevel)
523
+ : resolved.thinkingLevel,
524
+ explicitThinkingLevel: resolved.explicitThinkingLevel,
525
+ warning: resolved.warning,
526
+ };
527
+ }
528
+ if (!warning && resolved.warning) {
529
+ warning = resolved.warning;
530
+ }
531
+ }
532
+
533
+ return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning };
502
534
  }
503
535
 
504
536
  export function extractExplicitThinkingSelector(
@@ -535,13 +567,14 @@ export function resolveModelFromString(
535
567
  value: string,
536
568
  available: Model<Api>[],
537
569
  matchPreferences?: ModelMatchPreferences,
570
+ modelRegistry?: CanonicalModelRegistry,
538
571
  ): Model<Api> | undefined {
539
572
  const parsed = parseModelString(value);
540
573
  if (parsed) {
541
574
  const exact = available.find(model => model.provider === parsed.provider && model.id === parsed.id);
542
575
  if (exact) return exact;
543
576
  }
544
- return parseModelPattern(value, available, matchPreferences).model;
577
+ return parseModelPattern(value, available, matchPreferences, { modelRegistry }).model;
545
578
  }
546
579
 
547
580
  /**
@@ -552,13 +585,19 @@ export function resolveModelFromSettings(options: {
552
585
  availableModels: Model<Api>[];
553
586
  matchPreferences?: ModelMatchPreferences;
554
587
  roleOrder?: readonly ModelRole[];
588
+ modelRegistry?: CanonicalModelRegistry;
555
589
  }): Model<Api> | undefined {
556
- const { settings, availableModels, matchPreferences, roleOrder } = options;
590
+ const { settings, availableModels, matchPreferences, roleOrder, modelRegistry } = options;
557
591
  const roles = roleOrder ?? MODEL_ROLE_IDS;
558
592
  for (const role of roles) {
559
593
  const configured = settings.getModelRole(role);
560
594
  if (!configured) continue;
561
- const resolved = resolveModelFromString(expandRoleAlias(configured, settings), availableModels, matchPreferences);
595
+ const resolved = resolveModelFromString(
596
+ expandRoleAlias(configured, settings),
597
+ availableModels,
598
+ matchPreferences,
599
+ modelRegistry,
600
+ );
562
601
  if (resolved) return resolved;
563
602
  }
564
603
  return availableModels[0];
@@ -569,7 +608,7 @@ export function resolveModelFromSettings(options: {
569
608
  */
570
609
  export function resolveModelOverride(
571
610
  modelPatterns: string[],
572
- modelRegistry: ModelRegistry,
611
+ modelRegistry: ModelLookupRegistry,
573
612
  settings?: Settings,
574
613
  ): { model?: Model<Api>; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } {
575
614
  if (modelPatterns.length === 0) return { explicitThinkingLevel: false };
@@ -579,6 +618,7 @@ export function resolveModelOverride(
579
618
  const { model, thinkingLevel, explicitThinkingLevel } = resolveModelRoleValue(pattern, availableModels, {
580
619
  settings,
581
620
  matchPreferences,
621
+ modelRegistry,
582
622
  });
583
623
  if (model) {
584
624
  return { model, thinkingLevel, explicitThinkingLevel };
@@ -594,12 +634,14 @@ export function resolveRoleSelection(
594
634
  roles: readonly string[],
595
635
  settings: Settings,
596
636
  availableModels: Model<Api>[],
637
+ modelRegistry?: CanonicalModelRegistry,
597
638
  ): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
598
639
  const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
599
640
  for (const role of roles) {
600
641
  const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, {
601
642
  settings,
602
643
  matchPreferences,
644
+ modelRegistry,
603
645
  });
604
646
  if (resolved.model) {
605
647
  return { model: resolved.model, thinkingLevel: resolved.thinkingLevel };
@@ -608,6 +650,36 @@ export function resolveRoleSelection(
608
650
  return undefined;
609
651
  }
610
652
 
653
+ function resolveExactCanonicalScopePattern(
654
+ pattern: string,
655
+ modelRegistry: Pick<ModelRegistry, "getCanonicalVariants">,
656
+ availableModels: Model<Api>[],
657
+ ): { models: Model<Api>[]; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } | undefined {
658
+ const lastColonIndex = pattern.lastIndexOf(":");
659
+ let canonicalId = pattern;
660
+ let thinkingLevel: ThinkingLevel | undefined;
661
+ let explicitThinkingLevel = false;
662
+
663
+ if (lastColonIndex !== -1) {
664
+ const suffix = pattern.substring(lastColonIndex + 1);
665
+ const parsedThinkingLevel = parseThinkingLevel(suffix);
666
+ if (parsedThinkingLevel) {
667
+ canonicalId = pattern.substring(0, lastColonIndex);
668
+ thinkingLevel = parsedThinkingLevel;
669
+ explicitThinkingLevel = true;
670
+ }
671
+ }
672
+
673
+ const variants = modelRegistry
674
+ .getCanonicalVariants(canonicalId, { availableOnly: true, candidates: availableModels })
675
+ .map(variant => variant.model);
676
+ if (variants.length === 0) {
677
+ return undefined;
678
+ }
679
+
680
+ return { models: variants, thinkingLevel, explicitThinkingLevel };
681
+ }
682
+
611
683
  /**
612
684
  * Resolve model patterns to actual Model objects with optional thinking levels
613
685
  * Format: "pattern:level" where :level is optional
@@ -621,7 +693,7 @@ export function resolveRoleSelection(
621
693
  */
622
694
  export async function resolveModelScope(
623
695
  patterns: string[],
624
- modelRegistry: ModelRegistry,
696
+ modelRegistry: Pick<ModelRegistry, "getAvailable" | "getCanonicalVariants">,
625
697
  preferences?: ModelMatchPreferences,
626
698
  ): Promise<ScopedModel[]> {
627
699
  const availableModels = modelRegistry.getAvailable();
@@ -674,10 +746,28 @@ export async function resolveModelScope(
674
746
  continue;
675
747
  }
676
748
 
749
+ const exactCanonical = resolveExactCanonicalScopePattern(pattern, modelRegistry, availableModels);
750
+ if (exactCanonical) {
751
+ for (const model of exactCanonical.models) {
752
+ if (!scopedModels.find(sm => modelsAreEqual(sm.model, model))) {
753
+ scopedModels.push({
754
+ model,
755
+ thinkingLevel: exactCanonical.explicitThinkingLevel
756
+ ? (resolveThinkingLevelForModel(model, exactCanonical.thinkingLevel) ??
757
+ exactCanonical.thinkingLevel)
758
+ : exactCanonical.thinkingLevel,
759
+ explicitThinkingLevel: exactCanonical.explicitThinkingLevel,
760
+ });
761
+ }
762
+ }
763
+ continue;
764
+ }
765
+
677
766
  const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPatternWithContext(
678
767
  pattern,
679
768
  availableModels,
680
769
  context,
770
+ { modelRegistry },
681
771
  );
682
772
 
683
773
  if (warning) {
@@ -706,6 +796,7 @@ export async function resolveModelScope(
706
796
 
707
797
  export interface ResolveCliModelResult {
708
798
  model: Model<Api> | undefined;
799
+ selector?: string;
709
800
  thinkingLevel?: ThinkingLevel;
710
801
  warning: string | undefined;
711
802
  error: string | undefined;
@@ -717,19 +808,20 @@ export interface ResolveCliModelResult {
717
808
  export function resolveCliModel(options: {
718
809
  cliProvider?: string;
719
810
  cliModel?: string;
720
- modelRegistry: ModelRegistry;
811
+ modelRegistry: CliModelRegistry;
721
812
  preferences?: ModelMatchPreferences;
722
813
  }): ResolveCliModelResult {
723
814
  const { cliProvider, cliModel, modelRegistry, preferences } = options;
724
815
 
725
816
  if (!cliModel) {
726
- return { model: undefined, warning: undefined, error: undefined };
817
+ return { model: undefined, selector: undefined, warning: undefined, error: undefined };
727
818
  }
728
819
 
729
820
  const availableModels = modelRegistry.getAll();
730
821
  if (availableModels.length === 0) {
731
822
  return {
732
823
  model: undefined,
824
+ selector: undefined,
733
825
  warning: undefined,
734
826
  error: "No models available. Check your installation or add models to models.json.",
735
827
  };
@@ -744,13 +836,15 @@ export function resolveCliModel(options: {
744
836
  if (cliProvider && !provider) {
745
837
  return {
746
838
  model: undefined,
839
+ selector: undefined,
747
840
  warning: undefined,
748
841
  error: `Unknown provider "${cliProvider}". Use --list-models to see available providers/models.`,
749
842
  };
750
843
  }
751
844
 
845
+ const trimmedModel = cliModel.trim();
752
846
  if (!provider) {
753
- const lower = cliModel.toLowerCase();
847
+ const lower = trimmedModel.toLowerCase();
754
848
  // When input has provider/id format (e.g. "zai/glm-5"), prefer decomposed
755
849
  // provider+id match over flat id match. Without this, a model with id
756
850
  // "zai/glm-5" on provider "vercel-ai-gateway" wins over provider "zai"
@@ -764,17 +858,35 @@ export function resolveCliModel(options: {
764
858
  model => model.provider.toLowerCase() === prefix && model.id.toLowerCase() === suffix,
765
859
  );
766
860
  }
861
+ if (!exact && !trimmedModel.includes(":")) {
862
+ const canonicalMatch = modelRegistry.resolveCanonicalModel?.(trimmedModel, { availableOnly: false });
863
+ if (canonicalMatch) {
864
+ return {
865
+ model: canonicalMatch,
866
+ selector: modelRegistry.getCanonicalId?.(canonicalMatch) ?? trimmedModel,
867
+ warning: undefined,
868
+ thinkingLevel: undefined,
869
+ error: undefined,
870
+ };
871
+ }
872
+ }
767
873
  if (!exact) {
768
874
  exact = availableModels.find(
769
875
  model => model.id.toLowerCase() === lower || `${model.provider}/${model.id}`.toLowerCase() === lower,
770
876
  );
771
877
  }
772
878
  if (exact) {
773
- return { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined };
879
+ return {
880
+ model: exact,
881
+ selector: formatModelString(exact),
882
+ warning: undefined,
883
+ thinkingLevel: undefined,
884
+ error: undefined,
885
+ };
774
886
  }
775
887
  }
776
888
 
777
- let pattern = cliModel;
889
+ let pattern = trimmedModel;
778
890
 
779
891
  if (!provider) {
780
892
  const slashIndex = cliModel.indexOf("/");
@@ -796,19 +908,42 @@ export function resolveCliModel(options: {
796
908
  const candidates = provider ? availableModels.filter(model => model.provider === provider) : availableModels;
797
909
  const { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, preferences, {
798
910
  allowInvalidThinkingSelectorFallback: false,
911
+ modelRegistry,
799
912
  });
800
913
 
801
914
  if (!model) {
802
915
  const display = provider ? `${provider}/${pattern}` : cliModel;
803
916
  return {
804
917
  model: undefined,
918
+ selector: undefined,
805
919
  thinkingLevel: undefined,
806
920
  warning,
807
921
  error: `Model "${display}" not found. Use --list-models to see available models.`,
808
922
  };
809
923
  }
810
924
 
811
- return { model, thinkingLevel, warning, error: undefined };
925
+ let selector = provider ? formatModelString(model) : undefined;
926
+ if (!provider) {
927
+ const lastColonIndex = pattern.lastIndexOf(":");
928
+ const canonicalCandidate =
929
+ lastColonIndex !== -1 && parseThinkingLevel(pattern.substring(lastColonIndex + 1))
930
+ ? pattern.substring(0, lastColonIndex)
931
+ : pattern;
932
+ if (!canonicalCandidate.includes("/")) {
933
+ const canonicalResolved = modelRegistry.resolveCanonicalModel?.(canonicalCandidate, { availableOnly: false });
934
+ if (canonicalResolved && canonicalResolved.provider === model.provider && canonicalResolved.id === model.id) {
935
+ selector = modelRegistry.getCanonicalId?.(canonicalResolved) ?? canonicalCandidate;
936
+ }
937
+ }
938
+ }
939
+
940
+ return {
941
+ model,
942
+ selector,
943
+ thinkingLevel,
944
+ warning,
945
+ error: undefined,
946
+ };
812
947
  }
813
948
 
814
949
  export interface InitialModelResult {
@@ -833,7 +968,7 @@ export async function findInitialModel(options: {
833
968
  defaultProvider?: string;
834
969
  defaultModelId?: string;
835
970
  defaultThinkingSelector?: Effort;
836
- modelRegistry: ModelRegistry;
971
+ modelRegistry: InitialModelRegistry;
837
972
  }): Promise<InitialModelResult> {
838
973
  const {
839
974
  cliProvider,
@@ -915,7 +1050,7 @@ export async function restoreModelFromSession(
915
1050
  savedModelId: string,
916
1051
  currentModel: Model<Api> | undefined,
917
1052
  shouldPrintMessages: boolean,
918
- modelRegistry: ModelRegistry,
1053
+ modelRegistry: RestorableModelRegistry,
919
1054
  ): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {
920
1055
  const restoredModel = modelRegistry.find(savedProvider, savedModelId);
921
1056
 
@@ -990,7 +1125,7 @@ export async function restoreModelFromSession(
990
1125
  * @returns The best available smol model, or undefined if none found
991
1126
  */
992
1127
  export async function findSmolModel(
993
- modelRegistry: ModelRegistry,
1128
+ modelRegistry: ModelLookupRegistry,
994
1129
  savedModel?: string,
995
1130
  ): Promise<Model<Api> | undefined> {
996
1131
  const availableModels = modelRegistry.getAvailable();
@@ -998,11 +1133,8 @@ export async function findSmolModel(
998
1133
 
999
1134
  // 1. Try saved model from settings
1000
1135
  if (savedModel) {
1001
- const parsed = parseModelString(savedModel);
1002
- if (parsed) {
1003
- const match = availableModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
1004
- if (match) return match;
1005
- }
1136
+ const match = resolveModelFromString(savedModel, availableModels, undefined, modelRegistry);
1137
+ if (match) return match;
1006
1138
  }
1007
1139
 
1008
1140
  // 2. Try priority chain
@@ -1012,7 +1144,7 @@ export async function findSmolModel(
1012
1144
  if (providerMatch) return providerMatch;
1013
1145
 
1014
1146
  // Try exact match first
1015
- const exactMatch = availableModels.find(m => m.id.toLowerCase() === pattern);
1147
+ const exactMatch = parseModelPattern(pattern, availableModels, undefined, { modelRegistry }).model;
1016
1148
  if (exactMatch) return exactMatch;
1017
1149
 
1018
1150
  // Try fuzzy match (substring)
@@ -1033,7 +1165,7 @@ export async function findSmolModel(
1033
1165
  * @returns The best available slow model, or undefined if none found
1034
1166
  */
1035
1167
  export async function findSlowModel(
1036
- modelRegistry: ModelRegistry,
1168
+ modelRegistry: ModelLookupRegistry,
1037
1169
  savedModel?: string,
1038
1170
  ): Promise<Model<Api> | undefined> {
1039
1171
  const availableModels = modelRegistry.getAvailable();
@@ -1041,17 +1173,14 @@ export async function findSlowModel(
1041
1173
 
1042
1174
  // 1. Try saved model from settings
1043
1175
  if (savedModel) {
1044
- const parsed = parseModelString(savedModel);
1045
- if (parsed) {
1046
- const match = availableModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
1047
- if (match) return match;
1048
- }
1176
+ const match = resolveModelFromString(savedModel, availableModels, undefined, modelRegistry);
1177
+ if (match) return match;
1049
1178
  }
1050
1179
 
1051
1180
  // 2. Try priority chain
1052
1181
  for (const pattern of MODEL_PRIO.slow) {
1053
1182
  // Try exact match first
1054
- const exactMatch = availableModels.find(m => m.id.toLowerCase() === pattern.toLowerCase());
1183
+ const exactMatch = parseModelPattern(pattern, availableModels, undefined, { modelRegistry }).model;
1055
1184
  if (exactMatch) return exactMatch;
1056
1185
 
1057
1186
  // Try fuzzy match (substring)
@@ -229,6 +229,8 @@ export const SETTINGS_SCHEMA = {
229
229
 
230
230
  modelTags: { type: "record", default: EMPTY_MODEL_TAGS_RECORD },
231
231
 
232
+ modelProviderOrder: { type: "array", default: EMPTY_STRING_ARRAY },
233
+
232
234
  cycleOrder: { type: "array", default: DEFAULT_CYCLE_ORDER },
233
235
 
234
236
  // ────────────────────────────────────────────────────────────────────────
@@ -1383,6 +1385,27 @@ export const SETTINGS_SCHEMA = {
1383
1385
  },
1384
1386
  },
1385
1387
 
1388
+ "bash.autoBackground.enabled": {
1389
+ type: "boolean",
1390
+ default: false,
1391
+ ui: {
1392
+ tab: "tools",
1393
+ label: "Bash Auto-Background",
1394
+ description: "Automatically background long-running bash commands and deliver the result later",
1395
+ },
1396
+ },
1397
+
1398
+ "bash.autoBackground.thresholdMs": {
1399
+ type: "number",
1400
+ default: 60_000,
1401
+ ui: {
1402
+ tab: "tools",
1403
+ label: "Bash Auto-Background Delay",
1404
+ description: "Milliseconds to wait before a bash command is moved to the background (0 = immediately)",
1405
+ submenu: true,
1406
+ },
1407
+ },
1408
+
1386
1409
  // MCP
1387
1410
  "mcp.enableProjectConfig": {
1388
1411
  type: "boolean",
@@ -13,8 +13,15 @@
13
13
 
14
14
  import * as fs from "node:fs";
15
15
  import * as path from "node:path";
16
- import { setDefaultTabWidth } from "@oh-my-pi/pi-natives";
17
- import { getAgentDbPath, getAgentDir, getProjectDir, isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
16
+ import {
17
+ getAgentDbPath,
18
+ getAgentDir,
19
+ getProjectDir,
20
+ isEnoent,
21
+ logger,
22
+ procmgr,
23
+ setDefaultTabWidth,
24
+ } from "@oh-my-pi/pi-utils";
18
25
  import { YAML } from "bun";
19
26
  import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
20
27
  import type { ModelRole } from "../config/model-registry";