@oh-my-pi/pi-coding-agent 14.0.5 → 14.1.1

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