@oh-my-pi/pi-coding-agent 16.0.9 → 16.0.11

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 (110) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/cli.js +3402 -3443
  3. package/dist/types/advisor/index.d.ts +1 -0
  4. package/dist/types/advisor/transcript-recorder.d.ts +52 -0
  5. package/dist/types/collab/host.d.ts +2 -2
  6. package/dist/types/collab/protocol.d.ts +4 -5
  7. package/dist/types/commit/agentic/agent.d.ts +1 -1
  8. package/dist/types/config/model-resolver.d.ts +11 -2
  9. package/dist/types/config/settings-schema.d.ts +12 -6
  10. package/dist/types/edit/file-snapshot-store.d.ts +1 -1
  11. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  12. package/dist/types/modes/components/agent-hub.d.ts +6 -1
  13. package/dist/types/modes/components/agent-transcript-viewer.d.ts +39 -0
  14. package/dist/types/modes/components/chat-transcript-builder.d.ts +42 -0
  15. package/dist/types/modes/controllers/command-controller.d.ts +3 -2
  16. package/dist/types/modes/interactive-mode.d.ts +2 -1
  17. package/dist/types/modes/types.d.ts +2 -1
  18. package/dist/types/registry/agent-registry.d.ts +10 -3
  19. package/dist/types/session/agent-session.d.ts +13 -0
  20. package/dist/types/session/compact-modes.d.ts +60 -0
  21. package/dist/types/session/streaming-output.d.ts +0 -2
  22. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  23. package/dist/types/slash-commands/helpers/collab-qrcode.d.ts +13 -0
  24. package/dist/types/tools/__tests__/json-tree.test.d.ts +1 -0
  25. package/dist/types/tools/index.d.ts +9 -1
  26. package/dist/types/utils/image-loading.d.ts +12 -0
  27. package/dist/types/utils/qrcode.d.ts +48 -0
  28. package/package.json +12 -12
  29. package/src/advisor/index.ts +1 -0
  30. package/src/advisor/transcript-recorder.ts +136 -0
  31. package/src/cli/args.ts +7 -1
  32. package/src/cli/stats-cli.ts +2 -11
  33. package/src/collab/host.ts +29 -17
  34. package/src/collab/protocol.ts +48 -15
  35. package/src/commit/agentic/agent.ts +2 -1
  36. package/src/commit/agentic/tools/git-file-diff.ts +2 -2
  37. package/src/commit/changelog/index.ts +1 -1
  38. package/src/commit/map-reduce/map-phase.ts +1 -1
  39. package/src/commit/map-reduce/utils.ts +1 -1
  40. package/src/config/config-file.ts +1 -1
  41. package/src/config/keybindings.ts +2 -2
  42. package/src/config/model-registry.ts +16 -4
  43. package/src/config/model-resolver.ts +193 -35
  44. package/src/config/settings-schema.ts +14 -7
  45. package/src/config/settings.ts +3 -9
  46. package/src/edit/file-snapshot-store.ts +1 -1
  47. package/src/edit/renderer.ts +7 -7
  48. package/src/eval/js/tool-bridge.ts +3 -2
  49. package/src/eval/py/prelude.py +3 -2
  50. package/src/export/html/tool-views.generated.js +28 -28
  51. package/src/extensibility/extensions/types.ts +7 -0
  52. package/src/hindsight/mental-models.ts +1 -1
  53. package/src/internal-urls/docs-index.generated.txt +1 -1
  54. package/src/internal-urls/history-protocol.ts +8 -3
  55. package/src/irc/bus.ts +8 -0
  56. package/src/lsp/index.ts +2 -2
  57. package/src/main.ts +6 -3
  58. package/src/modes/acp/acp-agent.ts +63 -0
  59. package/src/modes/components/agent-hub.ts +97 -920
  60. package/src/modes/components/agent-transcript-viewer.ts +461 -0
  61. package/src/modes/components/chat-transcript-builder.ts +462 -0
  62. package/src/modes/components/diff.ts +12 -35
  63. package/src/modes/components/oauth-selector.ts +31 -2
  64. package/src/modes/controllers/command-controller.ts +12 -2
  65. package/src/modes/controllers/event-controller.ts +1 -1
  66. package/src/modes/controllers/input-controller.ts +8 -1
  67. package/src/modes/controllers/selector-controller.ts +4 -1
  68. package/src/modes/interactive-mode.ts +4 -2
  69. package/src/modes/types.ts +2 -1
  70. package/src/prompts/tools/inspect-image.md +1 -1
  71. package/src/prompts/tools/read.md +1 -1
  72. package/src/registry/agent-registry.ts +13 -4
  73. package/src/sdk.ts +27 -8
  74. package/src/session/agent-session.ts +185 -17
  75. package/src/session/compact-modes.ts +105 -0
  76. package/src/session/session-dump-format.ts +1 -1
  77. package/src/session/session-history-format.ts +1 -1
  78. package/src/session/streaming-output.ts +5 -5
  79. package/src/slash-commands/builtin-registry.ts +45 -15
  80. package/src/slash-commands/helpers/collab-qrcode.ts +28 -0
  81. package/src/task/executor.ts +1 -1
  82. package/src/task/output-manager.ts +5 -0
  83. package/src/thinking.ts +25 -5
  84. package/src/tools/__tests__/json-tree.test.ts +35 -0
  85. package/src/tools/approval.ts +1 -1
  86. package/src/tools/bash.ts +0 -1
  87. package/src/tools/browser.ts +0 -1
  88. package/src/tools/eval.ts +1 -1
  89. package/src/tools/gh.ts +1 -1
  90. package/src/tools/index.ts +10 -1
  91. package/src/tools/inspect-image.ts +72 -9
  92. package/src/tools/irc.ts +1 -1
  93. package/src/tools/json-tree.ts +22 -5
  94. package/src/tools/read.ts +5 -6
  95. package/src/utils/file-mentions.ts +5 -2
  96. package/src/utils/image-loading.ts +58 -0
  97. package/src/utils/qrcode.ts +535 -0
  98. package/src/web/scrapers/firefox-addons.ts +1 -1
  99. package/src/web/scrapers/github.ts +1 -1
  100. package/src/web/scrapers/go-pkg.ts +2 -2
  101. package/src/web/scrapers/metacpan.ts +2 -2
  102. package/src/web/scrapers/nvd.ts +2 -2
  103. package/src/web/scrapers/ollama.ts +1 -1
  104. package/src/web/scrapers/opencorporates.ts +1 -1
  105. package/src/web/scrapers/pub-dev.ts +1 -1
  106. package/src/web/scrapers/repology.ts +1 -1
  107. package/src/web/scrapers/sourcegraph.ts +1 -1
  108. package/src/web/scrapers/terraform.ts +6 -6
  109. package/src/web/scrapers/wikidata.ts +2 -2
  110. package/src/workspace-tree.ts +1 -1
@@ -72,6 +72,21 @@ export interface ScopedModel {
72
72
  explicitThinkingLevel: boolean;
73
73
  }
74
74
 
75
+ interface ThinkingSuffixOptions {
76
+ allowMaxAlias?: boolean;
77
+ }
78
+
79
+ interface ModelStringParseOptions extends ThinkingSuffixOptions {
80
+ isLiteralModelId?: (provider: string, id: string) => boolean;
81
+ }
82
+ const MAX_THINKING_SUFFIX_OPTIONS: ThinkingSuffixOptions = { allowMaxAlias: true };
83
+
84
+ function parseThinkingSuffix(value: string, options?: ThinkingSuffixOptions): ThinkingLevel | undefined {
85
+ const level = parseThinkingLevel(value);
86
+ if (level !== undefined) return level;
87
+ return options?.allowMaxAlias === true && value === "max" ? ThinkingLevel.XHigh : undefined;
88
+ }
89
+
75
90
  /**
76
91
  * Split a trailing `:<level>` thinking selector off a model pattern.
77
92
  *
@@ -81,27 +96,89 @@ export interface ScopedModel {
81
96
  * role-alias callers pass `PREFIX_MODEL_ROLE.length` so the base is at least
82
97
  * as long as the `pi/` prefix.
83
98
  */
84
- function splitThinkingSuffix(pattern: string, minColonIndex = -1): { base: string; level?: ThinkingLevel } {
99
+ function splitThinkingSuffix(
100
+ pattern: string,
101
+ minColonIndex = -1,
102
+ options?: ThinkingSuffixOptions,
103
+ ): { base: string; level?: ThinkingLevel } {
85
104
  const colonIdx = pattern.lastIndexOf(":");
86
105
  if (colonIdx <= minColonIndex) return { base: pattern };
87
- const level = parseThinkingLevel(pattern.slice(colonIdx + 1));
106
+ const level = parseThinkingSuffix(pattern.slice(colonIdx + 1), options);
88
107
  return level ? { base: pattern.slice(0, colonIdx), level } : { base: pattern };
89
108
  }
90
109
 
110
+ function hasExactModelPattern(pattern: string, availableModels: readonly Model<Api>[]): boolean {
111
+ const normalized = pattern.toLowerCase();
112
+ return availableModels.some(
113
+ model => model.id.toLowerCase() === normalized || `${model.provider}/${model.id}`.toLowerCase() === normalized,
114
+ );
115
+ }
116
+
117
+ function matchingGlobModels(pattern: string, availableModels: readonly Model<Api>[]): Model<Api>[] {
118
+ const glob = new Bun.Glob(pattern.toLowerCase());
119
+ return availableModels.filter(model => {
120
+ const fullId = `${model.provider}/${model.id}`;
121
+ return glob.match(fullId.toLowerCase()) || glob.match(model.id.toLowerCase());
122
+ });
123
+ }
124
+
125
+ function resolveGlobScopePattern(
126
+ pattern: string,
127
+ availableModels: readonly Model<Api>[],
128
+ ): { models: Model<Api>[]; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } {
129
+ const strictSuffix = splitThinkingSuffix(pattern);
130
+ if (strictSuffix.level !== undefined) {
131
+ return {
132
+ models: matchingGlobModels(strictSuffix.base, availableModels),
133
+ thinkingLevel: strictSuffix.level,
134
+ explicitThinkingLevel: true,
135
+ };
136
+ }
137
+
138
+ const maxSuffix = splitThinkingSuffix(pattern, -1, MAX_THINKING_SUFFIX_OPTIONS);
139
+ if (maxSuffix.level !== undefined) {
140
+ const literalMatches = matchingGlobModels(pattern, availableModels);
141
+ if (literalMatches.length > 0) {
142
+ return { models: literalMatches, thinkingLevel: undefined, explicitThinkingLevel: false };
143
+ }
144
+ return {
145
+ models: matchingGlobModels(maxSuffix.base, availableModels),
146
+ thinkingLevel: maxSuffix.level,
147
+ explicitThinkingLevel: true,
148
+ };
149
+ }
150
+
151
+ return {
152
+ models: matchingGlobModels(pattern, availableModels),
153
+ thinkingLevel: undefined,
154
+ explicitThinkingLevel: false,
155
+ };
156
+ }
157
+
91
158
  /**
92
159
  * Parse a model string in "provider/modelId" format.
93
160
  * Returns undefined if the format is invalid.
94
161
  */
95
162
  export function parseModelString(
96
163
  modelStr: string,
164
+ options?: ModelStringParseOptions,
97
165
  ): { provider: string; id: string; thinkingLevel?: ThinkingLevel } | undefined {
98
166
  const slashIdx = modelStr.indexOf("/");
99
167
  if (slashIdx <= 0) return undefined;
100
168
  const id = modelStr.slice(slashIdx + 1);
101
169
  const provider = modelStr.slice(0, slashIdx);
102
- // Strip valid thinking level suffix (e.g., "claude-sonnet-4-6:high" -> id "claude-sonnet-4-6", thinkingLevel "high")
103
- const { base, level } = splitThinkingSuffix(id);
104
- return level ? { provider, id: base, thinkingLevel: level } : { provider, id };
170
+ // Strip strict thinking level suffixes first (e.g. "claude-sonnet-4-6:high" -> id "claude-sonnet-4-6", thinkingLevel "high").
171
+ const strict = splitThinkingSuffix(id);
172
+ if (strict.level) return { provider, id: strict.base, thinkingLevel: strict.level };
173
+ // `max` is a provider-facing alias for xhigh, but real model IDs can end in
174
+ // `:max`. Context-aware callers pass a literal lookup so those models win.
175
+ const maxAlias = splitThinkingSuffix(id, -1, options);
176
+ if (maxAlias.level) {
177
+ return options?.isLiteralModelId?.(provider, id) === true
178
+ ? { provider, id }
179
+ : { provider, id: maxAlias.base, thinkingLevel: maxAlias.level };
180
+ }
181
+ return { provider, id };
105
182
  }
106
183
 
107
184
  /**
@@ -149,7 +226,10 @@ function getOpenRouterRouteSuffix(modelId: string): { baseId: string; suffix: st
149
226
  }
150
227
 
151
228
  const suffix = modelId.slice(colonIdx + 1).trim();
152
- if (!suffix || parseThinkingLevel(suffix)) {
229
+ // `max` is a thinking-level alias (xhigh), never an OpenRouter route suffix, so
230
+ // `openrouter/<id>:max` falls through to the max-aware selector split instead of
231
+ // being cloned into a literal `<id>:max` model id with the reasoning level lost.
232
+ if (!suffix || parseThinkingSuffix(suffix, MAX_THINKING_SUFFIX_OPTIONS)) {
153
233
  return undefined;
154
234
  }
155
235
 
@@ -196,6 +276,50 @@ function cloneModelWithRequestedId(model: Model<Api>, requestedId: string): Mode
196
276
  };
197
277
  }
198
278
 
279
+ const AMAZON_BEDROCK_PROVIDER = "amazon-bedrock";
280
+ const BEDROCK_INFERENCE_PROFILE_ARN =
281
+ /^arn:aws(?:-[a-z]+)*:bedrock:[a-z0-9-]+:[0-9]*:(?:application-inference-profile|inference-profile)\/[a-z0-9][a-z0-9._:-]*$/i;
282
+
283
+ function hasBedrockInferenceProfileThinkingSuffix(modelId: string): boolean {
284
+ const { base, level } = splitThinkingSuffix(modelId);
285
+ return level !== undefined && BEDROCK_INFERENCE_PROFILE_ARN.test(base.trim());
286
+ }
287
+
288
+ function resolveBedrockInferenceProfileModelId(
289
+ modelId: string,
290
+ availableModels: readonly Model<Api>[],
291
+ ): Model<Api> | undefined {
292
+ const requestedId = modelId.trim();
293
+ if (hasBedrockInferenceProfileThinkingSuffix(requestedId) || !BEDROCK_INFERENCE_PROFILE_ARN.test(requestedId)) {
294
+ return undefined;
295
+ }
296
+
297
+ const template = availableModels.find(model => model.provider.toLowerCase() === AMAZON_BEDROCK_PROVIDER);
298
+ if (!template) return undefined;
299
+
300
+ return buildModel({
301
+ id: requestedId,
302
+ name: "Bedrock inference profile",
303
+ api: "bedrock-converse-stream",
304
+ provider: AMAZON_BEDROCK_PROVIDER,
305
+ baseUrl: template.baseUrl,
306
+ reasoning: false,
307
+ input: ["text"],
308
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
309
+ contextWindow: null,
310
+ maxTokens: null,
311
+ });
312
+ }
313
+
314
+ function resolveBedrockInferenceProfileReference(
315
+ provider: string,
316
+ modelId: string,
317
+ availableModels: readonly Model<Api>[],
318
+ ): Model<Api> | undefined {
319
+ if (provider.toLowerCase() !== AMAZON_BEDROCK_PROVIDER) return undefined;
320
+ return resolveBedrockInferenceProfileModelId(modelId, availableModels);
321
+ }
322
+
199
323
  const UPSTREAM_ROUTING_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
200
324
 
201
325
  /**
@@ -289,6 +413,11 @@ export function resolveProviderModelReference(
289
413
  }
290
414
  }
291
415
 
416
+ const bedrockInferenceProfile = resolveBedrockInferenceProfileReference(provider, modelId, availableModels);
417
+ if (bedrockInferenceProfile) {
418
+ return bedrockInferenceProfile;
419
+ }
420
+
292
421
  if (normalizedProvider !== "openrouter") {
293
422
  return undefined;
294
423
  }
@@ -470,6 +599,7 @@ function findExactCanonicalModelMatch(
470
599
  * The single model-matching engine. Tries, in order:
471
600
  * 1. exact `provider/id` reference (variant-alias and OpenRouter routed/date
472
601
  * fallbacks included),
602
+
473
603
  * 2. exact canonical id (coalesces provider variants),
474
604
  * 3. exact bare id (preference-ranked),
475
605
  * 4. retired effort-tier variant alias (collapsed catalog entries),
@@ -502,6 +632,11 @@ function matchModel(
502
632
  return pickPreferredModel(exactMatches, context);
503
633
  }
504
634
 
635
+ const bedrockInferenceProfile = resolveBedrockInferenceProfileModelId(modelPattern, availableModels);
636
+ if (bedrockInferenceProfile) {
637
+ return bedrockInferenceProfile;
638
+ }
639
+
505
640
  // Retired effort-tier variant ids (bare, no provider prefix) resolve to
506
641
  // their collapsed logical model; models from the providers whose table
507
642
  // declared the alias win ties. Auto-derived `X-thinking` pairs resolve
@@ -625,8 +760,10 @@ function parseModelPatternWithContext(
625
760
  return { model: exactMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
626
761
  }
627
762
 
628
- // No match - try stripping a valid thinking suffix and recursing
629
- const { base, level } = splitThinkingSuffix(pattern);
763
+ // No match - try stripping a valid thinking suffix and recursing.
764
+ // `max` is accepted only after the full pattern failed, so literal model IDs
765
+ // ending in `:max` keep winning over the alias.
766
+ const { base, level } = splitThinkingSuffix(pattern, -1, MAX_THINKING_SUFFIX_OPTIONS);
630
767
  if (level) {
631
768
  const result = parseModelPatternWithContext(base, availableModels, context, options);
632
769
  if (result.model) {
@@ -730,7 +867,11 @@ function resolveDefaultInheritedPatterns(
730
867
 
731
868
  const resolved: string[] = [];
732
869
  for (const pattern of normalizeModelPatternList(configuredDefault)) {
733
- const { base: aliasCandidate, level: thinkingLevel } = splitThinkingSuffix(pattern, PREFIX_MODEL_ROLE.length);
870
+ const { base: aliasCandidate, level: thinkingLevel } = splitThinkingSuffix(
871
+ pattern,
872
+ PREFIX_MODEL_ROLE.length,
873
+ MAX_THINKING_SUFFIX_OPTIONS,
874
+ );
734
875
  const aliasRole = getModelRoleAlias(aliasCandidate);
735
876
  if (aliasRole === role) {
736
877
  // Self-alias (e.g. modelRoles.default = "pi/smol") would loop back to the
@@ -765,7 +906,11 @@ function resolveConfiguredRolePattern(
765
906
  const normalized = value.trim();
766
907
  if (!normalized) return undefined;
767
908
 
768
- const { base: aliasCandidate, level: thinkingLevel } = splitThinkingSuffix(normalized, PREFIX_MODEL_ROLE.length);
909
+ const { base: aliasCandidate, level: thinkingLevel } = splitThinkingSuffix(
910
+ normalized,
911
+ PREFIX_MODEL_ROLE.length,
912
+ MAX_THINKING_SUFFIX_OPTIONS,
913
+ );
769
914
  const role = getModelRoleAlias(aliasCandidate);
770
915
  if (!role) return [normalized];
771
916
  if (visited.has(role)) return undefined;
@@ -888,9 +1033,19 @@ export function resolveModelRoleValue(
888
1033
  return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning };
889
1034
  }
890
1035
 
1036
+ interface ExplicitThinkingSelectorOptions {
1037
+ isLiteralModelId?: (provider: string, id: string) => boolean;
1038
+ }
1039
+
1040
+ function isLiteralModelSelector(value: string, options?: ExplicitThinkingSelectorOptions): boolean {
1041
+ const parsed = parseModelString(value);
1042
+ return parsed !== undefined && options?.isLiteralModelId?.(parsed.provider, parsed.id) === true;
1043
+ }
1044
+
891
1045
  export function extractExplicitThinkingSelector(
892
1046
  value: string | undefined,
893
1047
  settings?: Settings,
1048
+ options?: ExplicitThinkingSelectorOptions,
894
1049
  ): ThinkingLevel | undefined {
895
1050
  if (!value) return undefined;
896
1051
  const normalized = value.trim();
@@ -900,9 +1055,13 @@ export function extractExplicitThinkingSelector(
900
1055
  let current = normalized;
901
1056
  while (!visited.has(current)) {
902
1057
  visited.add(current);
903
- const thinkingSelector = splitThinkingSuffix(current, PREFIX_MODEL_ROLE.length).level;
904
- if (thinkingSelector) {
905
- return thinkingSelector;
1058
+ const strictSelector = splitThinkingSuffix(current, PREFIX_MODEL_ROLE.length).level;
1059
+ if (strictSelector) {
1060
+ return strictSelector;
1061
+ }
1062
+ const maxSelector = splitThinkingSuffix(current, PREFIX_MODEL_ROLE.length, MAX_THINKING_SUFFIX_OPTIONS).level;
1063
+ if (maxSelector && (current.startsWith(PREFIX_MODEL_ROLE) || !isLiteralModelSelector(current, options))) {
1064
+ return maxSelector;
906
1065
  }
907
1066
  const expanded = expandRoleAlias(current, settings).trim();
908
1067
  if (!expanded || expanded === current) break;
@@ -922,10 +1081,15 @@ export function resolveModelFromString(
922
1081
  matchPreferences?: ModelMatchPreferences,
923
1082
  modelRegistry?: CanonicalModelRegistry,
924
1083
  ): Model<Api> | undefined {
925
- const parsed = parseModelString(value);
1084
+ const exact = available.find(model => `${model.provider}/${model.id}` === value);
1085
+ if (exact) return exact;
1086
+ const parsed = parseModelString(value, {
1087
+ ...MAX_THINKING_SUFFIX_OPTIONS,
1088
+ isLiteralModelId: (provider, id) => available.some(model => model.provider === provider && model.id === id),
1089
+ });
926
1090
  if (parsed) {
927
- const exact = available.find(model => model.provider === parsed.provider && model.id === parsed.id);
928
- if (exact) return exact;
1091
+ const parsedExact = available.find(model => model.provider === parsed.provider && model.id === parsed.id);
1092
+ if (parsedExact) return parsedExact;
929
1093
  }
930
1094
  return parseModelPattern(value, available, matchPreferences, { modelRegistry }).model;
931
1095
  }
@@ -1065,7 +1229,10 @@ function resolveExactCanonicalScopePattern(
1065
1229
  modelRegistry: Pick<ModelRegistry, "getCanonicalVariants">,
1066
1230
  availableModels: Model<Api>[],
1067
1231
  ): { models: Model<Api>[]; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } | undefined {
1068
- const { base: canonicalId, level: thinkingLevel } = splitThinkingSuffix(pattern);
1232
+ if (pattern.endsWith(":max") && hasExactModelPattern(pattern, availableModels)) {
1233
+ return undefined;
1234
+ }
1235
+ const { base: canonicalId, level: thinkingLevel } = splitThinkingSuffix(pattern, -1, MAX_THINKING_SUFFIX_OPTIONS);
1069
1236
  const explicitThinkingLevel = thinkingLevel !== undefined;
1070
1237
 
1071
1238
  const variants = modelRegistry
@@ -1111,17 +1278,13 @@ export async function resolveModelScope(
1111
1278
  for (const pattern of patterns) {
1112
1279
  // Check if pattern contains glob characters
1113
1280
  if (pattern.includes("*") || pattern.includes("?") || pattern.includes("[")) {
1114
- // Extract optional thinking level suffix (e.g., "provider/*:high")
1115
- const { base: globPattern, level: thinkingLevel } = splitThinkingSuffix(pattern);
1116
- const explicitThinkingLevel = thinkingLevel !== undefined;
1117
-
1118
- // Match against "provider/modelId" format OR just model ID
1119
- // This allows "*sonnet*" to match without requiring "anthropic/*sonnet*"
1120
- const matchingModels = availableModels.filter(m => {
1121
- const fullId = `${m.provider}/${m.id}`;
1122
- const glob = new Bun.Glob(globPattern.toLowerCase());
1123
- return glob.match(fullId.toLowerCase()) || glob.match(m.id.toLowerCase());
1124
- });
1281
+ // Extract optional thinking level suffix (e.g., "provider/*:high") only
1282
+ // after literal `:max` globs had a chance to match real model IDs.
1283
+ const {
1284
+ models: matchingModels,
1285
+ thinkingLevel,
1286
+ explicitThinkingLevel,
1287
+ } = resolveGlobScopePattern(pattern, availableModels);
1125
1288
 
1126
1289
  if (matchingModels.length === 0) {
1127
1290
  logger.warn(`No models match pattern "${pattern}"`);
@@ -1226,13 +1389,8 @@ export function filterAvailableModelsByEnabledPatterns(
1226
1389
 
1227
1390
  for (const pattern of patterns) {
1228
1391
  if (pattern.includes("*") || pattern.includes("?") || pattern.includes("[")) {
1229
- const { base: globPattern } = splitThinkingSuffix(pattern);
1230
- const glob = new Bun.Glob(globPattern.toLowerCase());
1231
- for (const model of available) {
1232
- const fullId = `${model.provider}/${model.id}`.toLowerCase();
1233
- if (glob.match(fullId) || glob.match(model.id.toLowerCase())) {
1234
- addAllowed(model);
1235
- }
1392
+ for (const model of resolveGlobScopePattern(pattern, available).models) {
1393
+ addAllowed(model);
1236
1394
  }
1237
1395
  continue;
1238
1396
  }
@@ -809,11 +809,6 @@ export const SETTINGS_SCHEMA = {
809
809
  description: "Remove the 1-character horizontal padding from the left and right of the terminal output",
810
810
  },
811
811
  },
812
- // Display rendering
813
- "display.tabWidth": {
814
- type: "number",
815
- default: 3,
816
- },
817
812
 
818
813
  "display.shimmer": {
819
814
  type: "enum",
@@ -872,7 +867,7 @@ export const SETTINGS_SCHEMA = {
872
867
  // Reasoning and prompts
873
868
  defaultThinkingLevel: {
874
869
  type: "enum",
875
- values: [...THINKING_EFFORTS, AUTO_THINKING],
870
+ values: [...THINKING_EFFORTS, AUTO_THINKING, "max"],
876
871
  default: "high",
877
872
  ui: {
878
873
  tab: "model",
@@ -1519,6 +1514,18 @@ export const SETTINGS_SCHEMA = {
1519
1514
  },
1520
1515
  },
1521
1516
 
1517
+ "collab.webUrl": {
1518
+ type: "string",
1519
+ default: "",
1520
+ ui: {
1521
+ tab: "interaction",
1522
+ group: "Collab",
1523
+ label: "Web UI URL",
1524
+ description:
1525
+ "Browser UI used by /collab links; empty derives from collab.relayUrl; explicit http:// is localhost-only",
1526
+ },
1527
+ },
1528
+
1522
1529
  "collab.displayName": {
1523
1530
  type: "string",
1524
1531
  default: "",
@@ -1878,7 +1885,7 @@ export const SETTINGS_SCHEMA = {
1878
1885
  { value: "anthropic", label: "Anthropic", description: "Use Anthropic-style in-band tool calls." },
1879
1886
  { value: "deepseek", label: "DeepSeek", description: "Use DeepSeek-style in-band tool calls." },
1880
1887
  { value: "harmony", label: "Harmony", description: "Use Harmony-style in-band tool calls." },
1881
- { value: "pi", label: "Pi", description: "Use the Pi owned dialect." },
1888
+ { value: "pi", label: "Pi", description: "Use the Pi owned dialect (compact sigil-delimited tool calls)." },
1882
1889
  { value: "qwen3", label: "Qwen3", description: "Use the Qwen3 owned dialect." },
1883
1890
  { value: "gemini", label: "Gemini", description: "Use the Gemini owned dialect." },
1884
1891
  { value: "gemma", label: "Gemma", description: "Use the Gemma owned dialect." },
@@ -22,9 +22,8 @@ import {
22
22
  isEnoent,
23
23
  logger,
24
24
  procmgr,
25
- setDefaultTabWidth,
26
25
  } from "@oh-my-pi/pi-utils";
27
- import { YAML } from "bun";
26
+ import { JSONC, YAML } from "bun";
28
27
  import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
29
28
  import type { ModelRole } from "../config/model-roles";
30
29
  import { loadCapability } from "../discovery";
@@ -668,9 +667,9 @@ export class Settings {
668
667
  // 1. Migrate from settings.json
669
668
  const settingsJsonPath = path.join(this.#agentDir, "settings.json");
670
669
  try {
671
- const parsed = JSON.parse(await Bun.file(settingsJsonPath).text());
670
+ const parsed: unknown = JSONC.parse(await Bun.file(settingsJsonPath).text());
672
671
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
673
- settings = this.#deepMerge(settings, this.#migrateRawSettings(parsed));
672
+ settings = this.#deepMerge(settings, this.#migrateRawSettings(parsed as RawSettings));
674
673
  migrated = true;
675
674
  try {
676
675
  fs.renameSync(settingsJsonPath, `${settingsJsonPath}.bak`);
@@ -1103,11 +1102,6 @@ const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook<any>>> = {
1103
1102
  });
1104
1103
  }
1105
1104
  },
1106
- "display.tabWidth": value => {
1107
- if (typeof value === "number") {
1108
- setDefaultTabWidth(value);
1109
- }
1110
- },
1111
1105
  "provider.appendOnlyContext": value => {
1112
1106
  if (typeof value === "string") {
1113
1107
  appendOnlyModeSignal.fire(value);
@@ -102,7 +102,7 @@ const HASHLINE_LINE_PREFIX = /^[ *]?(\d+)(?:-(\d+))?:/;
102
102
  /**
103
103
  * The 1-indexed file lines a hashline-formatted body actually displayed.
104
104
  * Single `NN:` rows contribute that line; a collapsed summary `NN-MM:` row
105
- * (a `{ .. }` brace pair) contributes only its boundary lines `NN` and `MM` —
105
+ * (a `{ }` brace pair) contributes only its boundary lines `NN` and `MM` —
106
106
  * the elided interior was never shown, so editing inside it must be rejected.
107
107
  */
108
108
  export function parseSeenLinesFromHashlineBody(body: string): number[] {
@@ -230,7 +230,7 @@ function truncateEditTitlePath(displayPath: string, maxWidth: number | undefined
230
230
  }
231
231
 
232
232
  function formatEditTitlePath(pathValue: string, maxWidth?: number): string {
233
- return truncateEditTitlePath(replaceTabs(shortenPath(pathValue), pathValue), maxWidth);
233
+ return truncateEditTitlePath(replaceTabs(shortenPath(pathValue)), maxWidth);
234
234
  }
235
235
 
236
236
  function formatEditPathDisplay(
@@ -326,11 +326,11 @@ function renderEditHeader(
326
326
  return buildHeader(fitted.description);
327
327
  }
328
328
 
329
- function renderPlainTextPreview(text: string, uiTheme: Theme, filePath?: string): string {
329
+ function renderPlainTextPreview(text: string, uiTheme: Theme, _filePath?: string): string {
330
330
  const previewLines = sanitizeText(text).split("\n");
331
331
  let preview = "\n\n";
332
332
  for (const line of previewLines.slice(0, CALL_TEXT_PREVIEW_LINES)) {
333
- preview += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line, filePath), CALL_TEXT_PREVIEW_WIDTH))}\n`;
333
+ preview += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), CALL_TEXT_PREVIEW_WIDTH))}\n`;
334
334
  }
335
335
  if (previewLines.length > CALL_TEXT_PREVIEW_LINES) {
336
336
  preview += uiTheme.fg("dim", `… ${previewLines.length - CALL_TEXT_PREVIEW_LINES} more lines`);
@@ -395,7 +395,7 @@ function formatMultiFileStreamingDiff(
395
395
  if (!preview.diff && !preview.error) continue;
396
396
  const header = uiTheme.fg("dim", `\n\n── ${shortenPath(preview.path)} ──`);
397
397
  if (preview.error) {
398
- parts.push(`${header}\n${uiTheme.fg("error", replaceTabs(preview.error, preview.path))}`);
398
+ parts.push(`${header}\n${uiTheme.fg("error", replaceTabs(preview.error))}`);
399
399
  continue;
400
400
  }
401
401
  if (preview.diff) {
@@ -637,7 +637,7 @@ export const editToolRenderer = {
637
637
  callPreviewCaches,
638
638
  );
639
639
  if (applyPatchSummary?.error) {
640
- body += `\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error, rawPath), Math.max(1, width - 2)))}`;
640
+ body += `\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error), Math.max(1, width - 2)))}`;
641
641
  }
642
642
  const bodyLines = body ? body.split("\n") : [];
643
643
  while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
@@ -733,11 +733,11 @@ function renderSingleFileResult(
733
733
 
734
734
  let body = "";
735
735
  if (isError) {
736
- if (errorText) body = uiTheme.fg("error", replaceTabs(errorText, rawPath));
736
+ if (errorText) body = uiTheme.fg("error", replaceTabs(errorText));
737
737
  } else if (details?.diff) {
738
738
  body = renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn, diffSectionCache);
739
739
  } else if (editDiffPreview) {
740
- if ("error" in editDiffPreview) body = uiTheme.fg("error", replaceTabs(editDiffPreview.error, rawPath));
740
+ if ("error" in editDiffPreview) body = uiTheme.fg("error", replaceTabs(editDiffPreview.error));
741
741
  else if (editDiffPreview.diff)
742
742
  body = renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn, diffSectionCache);
743
743
  }
@@ -1,4 +1,5 @@
1
1
  import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
+ import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
2
3
  import type { ToolSession } from "../../tools";
3
4
  import { ToolError } from "../../tools/tool-errors";
4
5
  import { EVAL_AGENT_BRIDGE_NAME, runEvalAgent } from "../agent-bridge";
@@ -48,8 +49,8 @@ function normalizeArgs(args: unknown): unknown {
48
49
  return args;
49
50
  }
50
51
  const record = { ...(args as Record<string, unknown>) };
51
- if (record._i === undefined) {
52
- record._i = "js prelude";
52
+ if (record[INTENT_FIELD] === undefined) {
53
+ record[INTENT_FIELD] = "js prelude";
53
54
  }
54
55
  return record;
55
56
  }
@@ -5,6 +5,7 @@ if "__omp_prelude_loaded__" not in globals():
5
5
  from pathlib import Path
6
6
  import os, json, math, re
7
7
  from urllib.parse import unquote
8
+ INTENT_FIELD = "_i"
8
9
 
9
10
  # __omp_display is injected by runner.py before the prelude executes; it
10
11
  # mirrors IPython's display() semantics with the same MIME bundle output.
@@ -479,8 +480,8 @@ if "__omp_prelude_loaded__" not in globals():
479
480
  f"tool.{self._name}(...) expects a dict of arguments (got {type(args).__name__})"
480
481
  )
481
482
  merged.update(kwargs)
482
- if "_i" not in merged:
483
- merged["_i"] = "py prelude"
483
+ if INTENT_FIELD not in merged:
484
+ merged[INTENT_FIELD] = "py prelude"
484
485
  return _bridge_call(self._name, merged)
485
486
 
486
487
  class _ToolProxy: