@oh-my-pi/pi-coding-agent 14.0.5 → 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 (40) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/package.json +7 -7
  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 +179 -11
  9. package/src/config/model-resolver.ts +171 -50
  10. package/src/config/settings-schema.ts +23 -0
  11. package/src/export/html/template.css +82 -0
  12. package/src/export/html/template.generated.ts +1 -1
  13. package/src/export/html/template.js +612 -97
  14. package/src/internal-urls/docs-index.generated.ts +1 -1
  15. package/src/internal-urls/jobs-protocol.ts +2 -1
  16. package/src/lsp/client.ts +1 -1
  17. package/src/main.ts +6 -1
  18. package/src/memories/index.ts +7 -6
  19. package/src/modes/components/model-selector.ts +221 -64
  20. package/src/modes/controllers/command-controller.ts +18 -0
  21. package/src/modes/controllers/selector-controller.ts +13 -5
  22. package/src/prompts/system/system-prompt.md +5 -1
  23. package/src/prompts/tools/bash.md +15 -0
  24. package/src/prompts/tools/cancel-job.md +1 -1
  25. package/src/prompts/tools/read-chunk.md +9 -0
  26. package/src/prompts/tools/read.md +9 -0
  27. package/src/prompts/tools/write.md +1 -0
  28. package/src/sdk.ts +7 -4
  29. package/src/session/agent-session.ts +23 -6
  30. package/src/task/executor.ts +5 -1
  31. package/src/tools/await-tool.ts +2 -1
  32. package/src/tools/bash.ts +221 -56
  33. package/src/tools/cancel-job.ts +2 -1
  34. package/src/tools/inspect-image.ts +1 -1
  35. package/src/tools/read.ts +218 -1
  36. package/src/tools/sqlite-reader.ts +623 -0
  37. package/src/tools/write.ts +187 -1
  38. package/src/utils/commit-message-generator.ts +1 -0
  39. package/src/utils/git.ts +24 -1
  40. package/src/utils/title-generator.ts +1 -1
@@ -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);
@@ -469,7 +491,7 @@ export interface ResolvedModelRoleValue {
469
491
  export function resolveModelRoleValue(
470
492
  roleValue: string | undefined,
471
493
  availableModels: Model<Api>[],
472
- options?: { settings?: Settings; matchPreferences?: ModelMatchPreferences },
494
+ options?: { settings?: Settings; matchPreferences?: ModelMatchPreferences; modelRegistry?: CanonicalModelRegistry },
473
495
  ): ResolvedModelRoleValue {
474
496
  if (!roleValue) {
475
497
  return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
@@ -490,7 +512,9 @@ export function resolveModelRoleValue(
490
512
 
491
513
  let warning: string | undefined;
492
514
  for (const effectivePattern of effectivePatterns) {
493
- const resolved = parseModelPattern(effectivePattern, availableModels, options?.matchPreferences);
515
+ const resolved = parseModelPattern(effectivePattern, availableModels, options?.matchPreferences, {
516
+ modelRegistry: options?.modelRegistry,
517
+ });
494
518
  if (resolved.model) {
495
519
  return {
496
520
  model: resolved.model,
@@ -543,13 +567,14 @@ export function resolveModelFromString(
543
567
  value: string,
544
568
  available: Model<Api>[],
545
569
  matchPreferences?: ModelMatchPreferences,
570
+ modelRegistry?: CanonicalModelRegistry,
546
571
  ): Model<Api> | undefined {
547
572
  const parsed = parseModelString(value);
548
573
  if (parsed) {
549
574
  const exact = available.find(model => model.provider === parsed.provider && model.id === parsed.id);
550
575
  if (exact) return exact;
551
576
  }
552
- return parseModelPattern(value, available, matchPreferences).model;
577
+ return parseModelPattern(value, available, matchPreferences, { modelRegistry }).model;
553
578
  }
554
579
 
555
580
  /**
@@ -560,13 +585,19 @@ export function resolveModelFromSettings(options: {
560
585
  availableModels: Model<Api>[];
561
586
  matchPreferences?: ModelMatchPreferences;
562
587
  roleOrder?: readonly ModelRole[];
588
+ modelRegistry?: CanonicalModelRegistry;
563
589
  }): Model<Api> | undefined {
564
- const { settings, availableModels, matchPreferences, roleOrder } = options;
590
+ const { settings, availableModels, matchPreferences, roleOrder, modelRegistry } = options;
565
591
  const roles = roleOrder ?? MODEL_ROLE_IDS;
566
592
  for (const role of roles) {
567
593
  const configured = settings.getModelRole(role);
568
594
  if (!configured) continue;
569
- const resolved = resolveModelFromString(expandRoleAlias(configured, settings), availableModels, matchPreferences);
595
+ const resolved = resolveModelFromString(
596
+ expandRoleAlias(configured, settings),
597
+ availableModels,
598
+ matchPreferences,
599
+ modelRegistry,
600
+ );
570
601
  if (resolved) return resolved;
571
602
  }
572
603
  return availableModels[0];
@@ -577,7 +608,7 @@ export function resolveModelFromSettings(options: {
577
608
  */
578
609
  export function resolveModelOverride(
579
610
  modelPatterns: string[],
580
- modelRegistry: ModelRegistry,
611
+ modelRegistry: ModelLookupRegistry,
581
612
  settings?: Settings,
582
613
  ): { model?: Model<Api>; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } {
583
614
  if (modelPatterns.length === 0) return { explicitThinkingLevel: false };
@@ -587,6 +618,7 @@ export function resolveModelOverride(
587
618
  const { model, thinkingLevel, explicitThinkingLevel } = resolveModelRoleValue(pattern, availableModels, {
588
619
  settings,
589
620
  matchPreferences,
621
+ modelRegistry,
590
622
  });
591
623
  if (model) {
592
624
  return { model, thinkingLevel, explicitThinkingLevel };
@@ -602,12 +634,14 @@ export function resolveRoleSelection(
602
634
  roles: readonly string[],
603
635
  settings: Settings,
604
636
  availableModels: Model<Api>[],
637
+ modelRegistry?: CanonicalModelRegistry,
605
638
  ): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
606
639
  const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
607
640
  for (const role of roles) {
608
641
  const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, {
609
642
  settings,
610
643
  matchPreferences,
644
+ modelRegistry,
611
645
  });
612
646
  if (resolved.model) {
613
647
  return { model: resolved.model, thinkingLevel: resolved.thinkingLevel };
@@ -616,6 +650,36 @@ export function resolveRoleSelection(
616
650
  return undefined;
617
651
  }
618
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
+
619
683
  /**
620
684
  * Resolve model patterns to actual Model objects with optional thinking levels
621
685
  * Format: "pattern:level" where :level is optional
@@ -629,7 +693,7 @@ export function resolveRoleSelection(
629
693
  */
630
694
  export async function resolveModelScope(
631
695
  patterns: string[],
632
- modelRegistry: ModelRegistry,
696
+ modelRegistry: Pick<ModelRegistry, "getAvailable" | "getCanonicalVariants">,
633
697
  preferences?: ModelMatchPreferences,
634
698
  ): Promise<ScopedModel[]> {
635
699
  const availableModels = modelRegistry.getAvailable();
@@ -682,10 +746,28 @@ export async function resolveModelScope(
682
746
  continue;
683
747
  }
684
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
+
685
766
  const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPatternWithContext(
686
767
  pattern,
687
768
  availableModels,
688
769
  context,
770
+ { modelRegistry },
689
771
  );
690
772
 
691
773
  if (warning) {
@@ -714,6 +796,7 @@ export async function resolveModelScope(
714
796
 
715
797
  export interface ResolveCliModelResult {
716
798
  model: Model<Api> | undefined;
799
+ selector?: string;
717
800
  thinkingLevel?: ThinkingLevel;
718
801
  warning: string | undefined;
719
802
  error: string | undefined;
@@ -725,19 +808,20 @@ export interface ResolveCliModelResult {
725
808
  export function resolveCliModel(options: {
726
809
  cliProvider?: string;
727
810
  cliModel?: string;
728
- modelRegistry: ModelRegistry;
811
+ modelRegistry: CliModelRegistry;
729
812
  preferences?: ModelMatchPreferences;
730
813
  }): ResolveCliModelResult {
731
814
  const { cliProvider, cliModel, modelRegistry, preferences } = options;
732
815
 
733
816
  if (!cliModel) {
734
- return { model: undefined, warning: undefined, error: undefined };
817
+ return { model: undefined, selector: undefined, warning: undefined, error: undefined };
735
818
  }
736
819
 
737
820
  const availableModels = modelRegistry.getAll();
738
821
  if (availableModels.length === 0) {
739
822
  return {
740
823
  model: undefined,
824
+ selector: undefined,
741
825
  warning: undefined,
742
826
  error: "No models available. Check your installation or add models to models.json.",
743
827
  };
@@ -752,13 +836,15 @@ export function resolveCliModel(options: {
752
836
  if (cliProvider && !provider) {
753
837
  return {
754
838
  model: undefined,
839
+ selector: undefined,
755
840
  warning: undefined,
756
841
  error: `Unknown provider "${cliProvider}". Use --list-models to see available providers/models.`,
757
842
  };
758
843
  }
759
844
 
845
+ const trimmedModel = cliModel.trim();
760
846
  if (!provider) {
761
- const lower = cliModel.toLowerCase();
847
+ const lower = trimmedModel.toLowerCase();
762
848
  // When input has provider/id format (e.g. "zai/glm-5"), prefer decomposed
763
849
  // provider+id match over flat id match. Without this, a model with id
764
850
  // "zai/glm-5" on provider "vercel-ai-gateway" wins over provider "zai"
@@ -772,17 +858,35 @@ export function resolveCliModel(options: {
772
858
  model => model.provider.toLowerCase() === prefix && model.id.toLowerCase() === suffix,
773
859
  );
774
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
+ }
775
873
  if (!exact) {
776
874
  exact = availableModels.find(
777
875
  model => model.id.toLowerCase() === lower || `${model.provider}/${model.id}`.toLowerCase() === lower,
778
876
  );
779
877
  }
780
878
  if (exact) {
781
- 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
+ };
782
886
  }
783
887
  }
784
888
 
785
- let pattern = cliModel;
889
+ let pattern = trimmedModel;
786
890
 
787
891
  if (!provider) {
788
892
  const slashIndex = cliModel.indexOf("/");
@@ -804,19 +908,42 @@ export function resolveCliModel(options: {
804
908
  const candidates = provider ? availableModels.filter(model => model.provider === provider) : availableModels;
805
909
  const { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, preferences, {
806
910
  allowInvalidThinkingSelectorFallback: false,
911
+ modelRegistry,
807
912
  });
808
913
 
809
914
  if (!model) {
810
915
  const display = provider ? `${provider}/${pattern}` : cliModel;
811
916
  return {
812
917
  model: undefined,
918
+ selector: undefined,
813
919
  thinkingLevel: undefined,
814
920
  warning,
815
921
  error: `Model "${display}" not found. Use --list-models to see available models.`,
816
922
  };
817
923
  }
818
924
 
819
- 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
+ };
820
947
  }
821
948
 
822
949
  export interface InitialModelResult {
@@ -841,7 +968,7 @@ export async function findInitialModel(options: {
841
968
  defaultProvider?: string;
842
969
  defaultModelId?: string;
843
970
  defaultThinkingSelector?: Effort;
844
- modelRegistry: ModelRegistry;
971
+ modelRegistry: InitialModelRegistry;
845
972
  }): Promise<InitialModelResult> {
846
973
  const {
847
974
  cliProvider,
@@ -923,7 +1050,7 @@ export async function restoreModelFromSession(
923
1050
  savedModelId: string,
924
1051
  currentModel: Model<Api> | undefined,
925
1052
  shouldPrintMessages: boolean,
926
- modelRegistry: ModelRegistry,
1053
+ modelRegistry: RestorableModelRegistry,
927
1054
  ): Promise<{ model: Model<Api> | undefined; fallbackMessage: string | undefined }> {
928
1055
  const restoredModel = modelRegistry.find(savedProvider, savedModelId);
929
1056
 
@@ -998,7 +1125,7 @@ export async function restoreModelFromSession(
998
1125
  * @returns The best available smol model, or undefined if none found
999
1126
  */
1000
1127
  export async function findSmolModel(
1001
- modelRegistry: ModelRegistry,
1128
+ modelRegistry: ModelLookupRegistry,
1002
1129
  savedModel?: string,
1003
1130
  ): Promise<Model<Api> | undefined> {
1004
1131
  const availableModels = modelRegistry.getAvailable();
@@ -1006,11 +1133,8 @@ export async function findSmolModel(
1006
1133
 
1007
1134
  // 1. Try saved model from settings
1008
1135
  if (savedModel) {
1009
- const parsed = parseModelString(savedModel);
1010
- if (parsed) {
1011
- const match = availableModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
1012
- if (match) return match;
1013
- }
1136
+ const match = resolveModelFromString(savedModel, availableModels, undefined, modelRegistry);
1137
+ if (match) return match;
1014
1138
  }
1015
1139
 
1016
1140
  // 2. Try priority chain
@@ -1020,7 +1144,7 @@ export async function findSmolModel(
1020
1144
  if (providerMatch) return providerMatch;
1021
1145
 
1022
1146
  // Try exact match first
1023
- const exactMatch = availableModels.find(m => m.id.toLowerCase() === pattern);
1147
+ const exactMatch = parseModelPattern(pattern, availableModels, undefined, { modelRegistry }).model;
1024
1148
  if (exactMatch) return exactMatch;
1025
1149
 
1026
1150
  // Try fuzzy match (substring)
@@ -1041,7 +1165,7 @@ export async function findSmolModel(
1041
1165
  * @returns The best available slow model, or undefined if none found
1042
1166
  */
1043
1167
  export async function findSlowModel(
1044
- modelRegistry: ModelRegistry,
1168
+ modelRegistry: ModelLookupRegistry,
1045
1169
  savedModel?: string,
1046
1170
  ): Promise<Model<Api> | undefined> {
1047
1171
  const availableModels = modelRegistry.getAvailable();
@@ -1049,17 +1173,14 @@ export async function findSlowModel(
1049
1173
 
1050
1174
  // 1. Try saved model from settings
1051
1175
  if (savedModel) {
1052
- const parsed = parseModelString(savedModel);
1053
- if (parsed) {
1054
- const match = availableModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
1055
- if (match) return match;
1056
- }
1176
+ const match = resolveModelFromString(savedModel, availableModels, undefined, modelRegistry);
1177
+ if (match) return match;
1057
1178
  }
1058
1179
 
1059
1180
  // 2. Try priority chain
1060
1181
  for (const pattern of MODEL_PRIO.slow) {
1061
1182
  // Try exact match first
1062
- const exactMatch = availableModels.find(m => m.id.toLowerCase() === pattern.toLowerCase());
1183
+ const exactMatch = parseModelPattern(pattern, availableModels, undefined, { modelRegistry }).model;
1063
1184
  if (exactMatch) return exactMatch;
1064
1185
 
1065
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",
@@ -705,6 +705,88 @@
705
705
  color: var(--error);
706
706
  }
707
707
 
708
+ /* Tool renderer extras */
709
+ .tool-meta {
710
+ margin-top: 4px;
711
+ }
712
+
713
+ .tool-badge {
714
+ display: inline-block;
715
+ padding: 0 6px;
716
+ margin-right: 4px;
717
+ border-radius: 3px;
718
+ background: rgba(255, 255, 255, 0.06);
719
+ color: var(--dim);
720
+ font-size: 11px;
721
+ font-weight: normal;
722
+ vertical-align: baseline;
723
+ }
724
+
725
+ .tool-pattern {
726
+ color: var(--warning);
727
+ }
728
+
729
+ .tool-args {
730
+ margin-top: 4px;
731
+ color: var(--toolOutput);
732
+ }
733
+
734
+ .tool-arg {
735
+ display: block;
736
+ line-height: var(--line-height);
737
+ white-space: pre-wrap;
738
+ word-break: break-word;
739
+ }
740
+
741
+ .tool-arg-key {
742
+ color: var(--dim);
743
+ }
744
+
745
+ .tool-arg-val {
746
+ color: var(--text);
747
+ }
748
+
749
+ .tool-cell {
750
+ margin-top: var(--line-height);
751
+ }
752
+
753
+ .tool-cell-title {
754
+ color: var(--dim);
755
+ font-size: 11px;
756
+ margin-bottom: 2px;
757
+ }
758
+
759
+ /* Todo write tree */
760
+ .todo-tree {
761
+ margin-top: var(--line-height);
762
+ }
763
+
764
+ .todo-phase {
765
+ margin-top: 6px;
766
+ color: var(--accent);
767
+ font-weight: bold;
768
+ }
769
+
770
+ .todo-task {
771
+ padding-left: 12px;
772
+ line-height: var(--line-height);
773
+ }
774
+
775
+ .todo-icon {
776
+ display: inline-block;
777
+ width: 14px;
778
+ text-align: center;
779
+ color: var(--dim);
780
+ }
781
+
782
+ .todo-completed { color: var(--toolDiffAdded); }
783
+ .todo-completed .todo-icon { color: var(--toolDiffAdded); }
784
+ .todo-in_progress { color: var(--warning); }
785
+ .todo-in_progress .todo-icon { color: var(--warning); }
786
+ .todo-abandoned { color: var(--toolDiffRemoved); }
787
+ .todo-abandoned .todo-icon { color: var(--toolDiffRemoved); }
788
+ .todo-pending { color: var(--toolOutput); }
789
+
708
790
  /* Images */
709
791
  .message-images {
710
792
  margin-bottom: 12px;