@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.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 (217) hide show
  1. package/CHANGELOG.md +103 -2
  2. package/dist/cli.js +5790 -5731
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  6. package/dist/types/cli-commands.d.ts +12 -0
  7. package/dist/types/commands/launch.d.ts +4 -0
  8. package/dist/types/config/api-key-resolver.d.ts +3 -0
  9. package/dist/types/config/keybindings.d.ts +6 -1
  10. package/dist/types/config/model-registry.d.ts +1 -0
  11. package/dist/types/config/model-resolver.d.ts +18 -0
  12. package/dist/types/config/settings-schema.d.ts +85 -34
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
  15. package/dist/types/eval/py/executor.d.ts +5 -0
  16. package/dist/types/eval/py/kernel.d.ts +6 -1
  17. package/dist/types/eval/py/runtime.d.ts +9 -0
  18. package/dist/types/exec/bash-executor.d.ts +2 -0
  19. package/dist/types/export/html/template.generated.d.ts +1 -1
  20. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -2
  22. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  23. package/dist/types/extensibility/shared-events.d.ts +2 -2
  24. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  25. package/dist/types/internal-urls/index.d.ts +1 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/irc/bus.d.ts +66 -0
  28. package/dist/types/memory-backend/index.d.ts +1 -0
  29. package/dist/types/memory-backend/runtime.d.ts +4 -0
  30. package/dist/types/memory-backend/types.d.ts +66 -1
  31. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  32. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  34. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  35. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  36. package/dist/types/modes/components/welcome.d.ts +3 -9
  37. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  38. package/dist/types/modes/index.d.ts +3 -3
  39. package/dist/types/modes/interactive-mode.d.ts +10 -4
  40. package/dist/types/modes/oauth-manual-input.d.ts +7 -0
  41. package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
  42. package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
  43. package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
  44. package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
  45. package/dist/types/modes/setup-wizard/index.d.ts +5 -1
  46. package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
  47. package/dist/types/modes/theme/theme.d.ts +2 -1
  48. package/dist/types/modes/types.d.ts +5 -2
  49. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  50. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  51. package/dist/types/registry/agent-registry.d.ts +16 -5
  52. package/dist/types/secrets/index.d.ts +1 -1
  53. package/dist/types/secrets/obfuscator.d.ts +8 -2
  54. package/dist/types/session/agent-session.d.ts +49 -32
  55. package/dist/types/session/messages.d.ts +2 -4
  56. package/dist/types/session/session-history-format.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +21 -3
  58. package/dist/types/session/streaming-output.d.ts +46 -0
  59. package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
  60. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  61. package/dist/types/slash-commands/types.d.ts +1 -1
  62. package/dist/types/system-prompt.d.ts +2 -0
  63. package/dist/types/task/executor.d.ts +12 -2
  64. package/dist/types/task/index.d.ts +13 -6
  65. package/dist/types/task/output-manager.d.ts +0 -7
  66. package/dist/types/task/repair-args.d.ts +8 -7
  67. package/dist/types/task/types.d.ts +63 -51
  68. package/dist/types/thinking.d.ts +4 -0
  69. package/dist/types/tiny/title-client.d.ts +11 -0
  70. package/dist/types/tiny/title-protocol.d.ts +1 -0
  71. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  72. package/dist/types/tools/find.d.ts +0 -11
  73. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  74. package/dist/types/tools/index.d.ts +7 -3
  75. package/dist/types/tools/irc.d.ts +76 -38
  76. package/dist/types/tools/job.d.ts +7 -1
  77. package/dist/types/utils/git.d.ts +15 -2
  78. package/dist/types/utils/title-generator.d.ts +3 -2
  79. package/examples/extensions/with-deps/package.json +1 -0
  80. package/package.json +11 -10
  81. package/scripts/bundle-dist.ts +28 -19
  82. package/src/async/index.ts +0 -1
  83. package/src/auto-thinking/classifier.ts +1 -0
  84. package/src/cli/args.ts +3 -0
  85. package/src/cli/gallery-cli.ts +1 -1
  86. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  87. package/src/cli/gallery-fixtures/types.ts +5 -0
  88. package/src/cli-commands.ts +29 -0
  89. package/src/cli.ts +28 -15
  90. package/src/commands/launch.ts +4 -0
  91. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  92. package/src/commit/model-selection.ts +3 -2
  93. package/src/config/api-key-resolver.ts +8 -6
  94. package/src/config/keybindings.ts +6 -1
  95. package/src/config/model-registry.ts +97 -30
  96. package/src/config/model-resolver.ts +60 -0
  97. package/src/config/settings-schema.ts +99 -55
  98. package/src/config/settings.ts +68 -3
  99. package/src/edit/hashline/execute.ts +39 -2
  100. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  101. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  102. package/src/eval/agent-bridge.ts +3 -16
  103. package/src/eval/completion-bridge.ts +1 -0
  104. package/src/eval/js/shared/prelude.txt +1 -1
  105. package/src/eval/py/executor.ts +29 -7
  106. package/src/eval/py/index.ts +6 -1
  107. package/src/eval/py/kernel.ts +31 -11
  108. package/src/eval/py/prelude.py +5 -6
  109. package/src/eval/py/runtime.ts +37 -0
  110. package/src/exec/bash-executor.ts +82 -3
  111. package/src/export/html/template.generated.ts +1 -1
  112. package/src/export/html/template.js +38 -13
  113. package/src/extensibility/custom-tools/types.ts +2 -2
  114. package/src/extensibility/extensions/get-commands-handler.ts +2 -1
  115. package/src/extensibility/extensions/runner.ts +6 -1
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/shared-events.ts +2 -2
  118. package/src/hindsight/bank.ts +17 -2
  119. package/src/internal-urls/docs-index.generated.ts +11 -11
  120. package/src/internal-urls/history-protocol.ts +113 -0
  121. package/src/internal-urls/index.ts +1 -0
  122. package/src/internal-urls/router.ts +3 -1
  123. package/src/internal-urls/types.ts +1 -1
  124. package/src/irc/bus.ts +292 -0
  125. package/src/main.ts +26 -66
  126. package/src/memories/index.ts +2 -0
  127. package/src/memory-backend/index.ts +1 -0
  128. package/src/memory-backend/local-backend.ts +9 -0
  129. package/src/memory-backend/off-backend.ts +9 -0
  130. package/src/memory-backend/runtime.ts +66 -0
  131. package/src/memory-backend/types.ts +81 -1
  132. package/src/mnemopi/backend.ts +151 -4
  133. package/src/modes/acp/acp-agent.ts +119 -11
  134. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  135. package/src/modes/components/assistant-message.ts +19 -21
  136. package/src/modes/components/compaction-summary-message.ts +68 -32
  137. package/src/modes/components/custom-editor.ts +10 -0
  138. package/src/modes/components/footer.ts +3 -1
  139. package/src/modes/components/status-line/component.ts +118 -34
  140. package/src/modes/components/tool-execution.ts +31 -1
  141. package/src/modes/components/ttsr-notification.ts +72 -30
  142. package/src/modes/components/welcome.ts +9 -33
  143. package/src/modes/controllers/command-controller.ts +1 -1
  144. package/src/modes/controllers/event-controller.ts +65 -0
  145. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  146. package/src/modes/controllers/input-controller.ts +19 -2
  147. package/src/modes/controllers/mcp-command-controller.ts +38 -3
  148. package/src/modes/controllers/selector-controller.ts +21 -17
  149. package/src/modes/index.ts +3 -21
  150. package/src/modes/interactive-mode.ts +47 -22
  151. package/src/modes/oauth-manual-input.ts +30 -3
  152. package/src/modes/rpc/rpc-client.ts +154 -3
  153. package/src/modes/rpc/rpc-mode.ts +97 -12
  154. package/src/modes/rpc/rpc-subagents.ts +265 -0
  155. package/src/modes/rpc/rpc-types.ts +81 -1
  156. package/src/modes/setup-wizard/index.ts +12 -2
  157. package/src/modes/setup-wizard/lazy.ts +16 -0
  158. package/src/modes/theme/theme.ts +18 -5
  159. package/src/modes/types.ts +5 -5
  160. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  161. package/src/modes/utils/ui-helpers.ts +51 -49
  162. package/src/prompts/system/irc-incoming.md +3 -4
  163. package/src/prompts/system/orchestrate-notice.md +2 -2
  164. package/src/prompts/system/subagent-system-prompt.md +0 -5
  165. package/src/prompts/system/system-prompt.md +1 -0
  166. package/src/prompts/system/workflow-notice.md +2 -2
  167. package/src/prompts/tools/eval.md +3 -3
  168. package/src/prompts/tools/irc.md +29 -19
  169. package/src/prompts/tools/read.md +2 -2
  170. package/src/prompts/tools/task-summary.md +5 -16
  171. package/src/prompts/tools/task.md +38 -29
  172. package/src/registry/agent-lifecycle.ts +218 -0
  173. package/src/registry/agent-registry.ts +16 -5
  174. package/src/sdk.ts +37 -10
  175. package/src/secrets/index.ts +8 -1
  176. package/src/secrets/obfuscator.ts +39 -18
  177. package/src/session/agent-session.ts +422 -291
  178. package/src/session/messages.ts +11 -78
  179. package/src/session/session-history-format.ts +246 -0
  180. package/src/session/session-manager.ts +59 -5
  181. package/src/session/streaming-output.ts +226 -10
  182. package/src/slash-commands/acp-builtins.ts +24 -0
  183. package/src/slash-commands/builtin-registry.ts +20 -0
  184. package/src/slash-commands/types.ts +1 -1
  185. package/src/system-prompt.ts +14 -0
  186. package/src/task/executor.ts +851 -461
  187. package/src/task/index.ts +721 -796
  188. package/src/task/output-manager.ts +0 -11
  189. package/src/task/render.ts +148 -63
  190. package/src/task/repair-args.ts +21 -9
  191. package/src/task/types.ts +82 -66
  192. package/src/thinking.ts +7 -0
  193. package/src/tiny/title-client.ts +34 -5
  194. package/src/tiny/title-protocol.ts +1 -1
  195. package/src/tiny/worker.ts +6 -4
  196. package/src/tools/ask.ts +4 -2
  197. package/src/tools/bash.ts +61 -10
  198. package/src/tools/browser/tab-worker.ts +26 -7
  199. package/src/tools/browser.ts +28 -1
  200. package/src/tools/find.ts +2 -27
  201. package/src/tools/grouped-file-output.ts +1 -118
  202. package/src/tools/image-gen.ts +11 -4
  203. package/src/tools/index.ts +17 -13
  204. package/src/tools/inspect-image.ts +1 -0
  205. package/src/tools/irc.ts +596 -171
  206. package/src/tools/job.ts +41 -7
  207. package/src/tools/read.ts +57 -1
  208. package/src/tools/renderers.ts +2 -0
  209. package/src/tools/resolve.ts +4 -1
  210. package/src/utils/commit-message-generator.ts +1 -0
  211. package/src/utils/git.ts +267 -13
  212. package/src/utils/title-generator.ts +24 -5
  213. package/dist/types/async/support.d.ts +0 -2
  214. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  215. package/dist/types/task/simple-mode.d.ts +0 -8
  216. package/src/async/support.ts +0 -5
  217. package/src/task/simple-mode.ts +0 -27
@@ -5,6 +5,8 @@ export interface ApiKeyResolverOptions {
5
5
  sessionId?: string;
6
6
  /** Provider base URL hint forwarded to the auth-storage cascade. */
7
7
  baseUrl?: string;
8
+ /** Provider model id forwarded to model-scoped usage ranking/backoff. */
9
+ modelId?: string;
8
10
  }
9
11
 
10
12
  /**
@@ -16,7 +18,7 @@ export interface ApiKeyResolverRegistry {
16
18
  getApiKeyForProvider(
17
19
  provider: string,
18
20
  sessionId?: string,
19
- options?: { baseUrl?: string; forceRefresh?: boolean; signal?: AbortSignal },
21
+ options?: { baseUrl?: string; modelId?: string; forceRefresh?: boolean; signal?: AbortSignal },
20
22
  ): Promise<string | undefined>;
21
23
  authStorage: Pick<AuthStorage, "rotateSessionCredential">;
22
24
  /**
@@ -39,10 +41,10 @@ export function createApiKeyResolver(
39
41
  provider: string,
40
42
  options: ApiKeyResolverOptions = {},
41
43
  ): ApiKeyResolver {
42
- const { sessionId, baseUrl } = options;
44
+ const { sessionId, baseUrl, modelId } = options;
43
45
  return async ({ lastChance, error, signal }) => {
44
46
  if (error === undefined) {
45
- return registry.getApiKeyForProvider(provider, sessionId, { baseUrl });
47
+ return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, modelId });
46
48
  }
47
49
  if (lastChance) {
48
50
  // Account constraint (401 / usage / account-rate-limit): rotate to a
@@ -50,9 +52,9 @@ export function createApiKeyResolver(
50
52
  // sibling exists we switch immediately; the precise no-sibling backoff
51
53
  // is owned by `markUsageLimitReached` (default + server usage-report
52
54
  // reset) and the outer whole-turn retry layer.
53
- await registry.authStorage.rotateSessionCredential(provider, sessionId, { error, signal });
54
- return registry.getApiKeyForProvider(provider, sessionId, { baseUrl });
55
+ await registry.authStorage.rotateSessionCredential(provider, sessionId, { error, modelId, signal });
56
+ return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, modelId });
55
57
  }
56
- return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, forceRefresh: true, signal });
58
+ return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, modelId, forceRefresh: true, signal });
57
59
  };
58
60
  }
@@ -36,6 +36,7 @@ interface AppKeybindings {
36
36
  "app.clipboard.pasteTextRaw": true;
37
37
  "app.clipboard.copyLine": true;
38
38
  "app.clipboard.copyPrompt": true;
39
+ "app.agents.hub": true;
39
40
  "app.session.new": true;
40
41
  "app.session.tree": true;
41
42
  "app.session.fork": true;
@@ -166,9 +167,13 @@ export const KEYBINDINGS = {
166
167
  defaultKeys: [],
167
168
  description: "Resume session",
168
169
  },
170
+ "app.agents.hub": {
171
+ defaultKeys: "alt+a",
172
+ description: "Open the agent hub",
173
+ },
169
174
  "app.session.observe": {
170
175
  defaultKeys: "ctrl+s",
171
- description: "Observe subagent sessions",
176
+ description: "Open the agent hub",
172
177
  },
173
178
  "app.session.togglePath": {
174
179
  defaultKeys: "ctrl+p",
@@ -1,3 +1,4 @@
1
+ import { execSync } from "node:child_process";
1
2
  import * as path from "node:path";
2
3
  import { registerCustomApi, unregisterCustomApis } from "@oh-my-pi/pi-ai/api-registry";
3
4
  import type { Api, Context, Model, ModelSpec, SimpleStreamOptions, ThinkingConfig } from "@oh-my-pi/pi-ai/types";
@@ -226,14 +227,50 @@ interface CustomModelsResult {
226
227
  found: boolean;
227
228
  }
228
229
 
230
+ const commandValueCache = new Map<string, string>();
231
+
232
+ function isCommandConfigValue(valueConfig: string | undefined): valueConfig is string {
233
+ return valueConfig?.startsWith("!") === true;
234
+ }
235
+
236
+ function resolveCommandConfig(command: string): string | undefined {
237
+ const cached = commandValueCache.get(command);
238
+ if (cached !== undefined) return cached;
239
+ try {
240
+ const stdout = execSync(command, { encoding: "utf8", timeout: 10_000, windowsHide: true });
241
+ const trimmed = stdout.trim();
242
+ if (trimmed.length === 0) return undefined;
243
+ commandValueCache.set(command, trimmed);
244
+ return trimmed;
245
+ } catch {
246
+ return undefined;
247
+ }
248
+ }
249
+
250
+ interface CommandApiKeyResolution {
251
+ configured: boolean;
252
+ value?: string;
253
+ }
229
254
  /**
230
- * Resolve an API key config value to an actual key.
231
- * Checks environment variable first, then treats as literal.
255
+ * Resolve a models.yml secret/config value to an actual value.
256
+ * `!cmd` runs a shell command and returns trimmed stdout, otherwise env vars are
257
+ * checked first and the input falls back to a literal value.
232
258
  */
233
- function resolveApiKeyConfig(keyConfig: string): string | undefined {
234
- const envValue = Bun.env[keyConfig];
259
+ function resolveConfigValue(valueConfig: string): string | undefined {
260
+ if (valueConfig.startsWith("!")) return resolveCommandConfig(valueConfig.slice(1).trim());
261
+ const envValue = Bun.env[valueConfig];
235
262
  if (envValue) return envValue;
236
- return keyConfig;
263
+ return valueConfig;
264
+ }
265
+
266
+ function resolveConfigHeaders(headers: Record<string, string> | undefined): Record<string, string> | undefined {
267
+ if (!headers) return undefined;
268
+ const resolved: Record<string, string> = {};
269
+ for (const [key, value] of Object.entries(headers)) {
270
+ const next = resolveConfigValue(value);
271
+ if (next) resolved[key] = next;
272
+ }
273
+ return Object.keys(resolved).length > 0 ? resolved : undefined;
237
274
  }
238
275
 
239
276
  function extractGoogleOAuthToken(value: string | undefined): string | undefined {
@@ -394,7 +431,8 @@ function mergeCustomModelHeaders(
394
431
  authHeader: boolean | undefined,
395
432
  apiKeyConfig: string | undefined,
396
433
  ): Record<string, string> | undefined {
397
- return mergeAuthHeader({ ...providerHeaders, ...modelHeaders }, authHeader, apiKeyConfig);
434
+ const resolvedModelHeaders = resolveConfigHeaders(modelHeaders);
435
+ return mergeAuthHeader({ ...providerHeaders, ...resolvedModelHeaders }, authHeader, apiKeyConfig);
398
436
  }
399
437
 
400
438
  function mergeAuthHeader(
@@ -406,7 +444,7 @@ function mergeAuthHeader(
406
444
  if (!authHeader || !apiKeyConfig) {
407
445
  return nextHeaders;
408
446
  }
409
- const resolvedKey = resolveApiKeyConfig(apiKeyConfig);
447
+ const resolvedKey = resolveConfigValue(apiKeyConfig);
410
448
  return resolvedKey ? { ...nextHeaders, Authorization: `Bearer ${resolvedKey}` } : nextHeaders;
411
449
  }
412
450
 
@@ -559,6 +597,28 @@ export class ModelRegistry {
559
597
  #rebuildSuspended: number = 0;
560
598
  #fetch: FetchImpl;
561
599
 
600
+ #resolveCommandBackedApiKey(provider: string): CommandApiKeyResolution {
601
+ const keyConfig = this.#customProviderApiKeys.get(provider);
602
+ if (!isCommandConfigValue(keyConfig)) return { configured: false };
603
+ const value = resolveConfigValue(keyConfig);
604
+ if (value) {
605
+ this.authStorage.setConfigApiKey(provider, value);
606
+ return { configured: true, value };
607
+ }
608
+ this.authStorage.removeConfigApiKey(provider);
609
+ return { configured: true };
610
+ }
611
+
612
+ #installProviderApiKey(provider: string, keyConfig: string): void {
613
+ this.#customProviderApiKeys.set(provider, keyConfig);
614
+ const resolved = resolveConfigValue(keyConfig);
615
+ if (resolved) {
616
+ this.authStorage.setConfigApiKey(provider, resolved);
617
+ } else if (isCommandConfigValue(keyConfig)) {
618
+ this.authStorage.removeConfigApiKey(provider);
619
+ }
620
+ }
621
+
562
622
  /**
563
623
  * @param authStorage - Auth storage for API key resolution
564
624
  *
@@ -579,10 +639,8 @@ export class ModelRegistry {
579
639
  // Set up fallback resolver for custom provider API keys
580
640
  this.authStorage.setFallbackResolver(provider => {
581
641
  const keyConfig = this.#customProviderApiKeys.get(provider);
582
- if (keyConfig) {
583
- return resolveApiKeyConfig(keyConfig);
584
- }
585
- return undefined;
642
+ if (!keyConfig) return undefined;
643
+ return resolveConfigValue(keyConfig);
586
644
  });
587
645
  // Load models synchronously in constructor.
588
646
  this.#loadModels();
@@ -673,7 +731,7 @@ export class ModelRegistry {
673
731
  // Restore runtime API keys before #loadModels — survives because
674
732
  // #loadModels only calls .set() on #customProviderApiKeys, never reassigns it.
675
733
  for (const [k, v] of this.#runtimeProviderApiKeys) {
676
- this.#customProviderApiKeys.set(k, v);
734
+ this.#installProviderApiKey(k, v);
677
735
  }
678
736
  this.#providerOverrides.clear();
679
737
  this.#modelOverrides.clear();
@@ -975,10 +1033,11 @@ export class ModelRegistry {
975
1033
  const configuredProviders = new Set(Object.keys(value.providers ?? {}));
976
1034
 
977
1035
  for (const [providerName, providerConfig] of providerEntries) {
1036
+ const resolvedProviderHeaders = resolveConfigHeaders(providerConfig.headers);
978
1037
  // Always set overrides when baseUrl/headers/apiKey/authHeader/compat/disableStrictTools/transport are present
979
1038
  if (
980
1039
  providerConfig.baseUrl ||
981
- providerConfig.headers ||
1040
+ resolvedProviderHeaders ||
982
1041
  providerConfig.apiKey ||
983
1042
  providerConfig.authHeader !== undefined ||
984
1043
  providerConfig.compat ||
@@ -988,7 +1047,7 @@ export class ModelRegistry {
988
1047
  const disableStrictCompat = providerConfig.disableStrictTools ? { disableStrictTools: true } : undefined;
989
1048
  overrides.set(providerName, {
990
1049
  baseUrl: providerConfig.baseUrl,
991
- headers: providerConfig.headers,
1050
+ headers: resolvedProviderHeaders,
992
1051
  apiKey: providerConfig.apiKey,
993
1052
  authHeader: providerConfig.authHeader,
994
1053
  compat: mergeCompat(providerConfig.compat, disableStrictCompat),
@@ -1010,7 +1069,7 @@ export class ModelRegistry {
1010
1069
  // fallback for entries that don't advertise one.
1011
1070
  api: (providerConfig.api ?? "openai-completions") as Api,
1012
1071
  baseUrl: providerConfig.baseUrl,
1013
- headers: providerConfig.headers,
1072
+ headers: resolvedProviderHeaders,
1014
1073
  compat: mergeCompat(providerConfig.compat, disableStrictCompat),
1015
1074
  discovery: providerConfig.discovery,
1016
1075
  optional: false,
@@ -1022,16 +1081,17 @@ export class ModelRegistry {
1022
1081
  // bearer in models.yml (e.g. for an auth-gateway baseUrl), that bearer
1023
1082
  // must authenticate the outbound request.
1024
1083
  if (providerConfig.apiKey) {
1025
- this.#customProviderApiKeys.set(providerName, providerConfig.apiKey);
1026
- const resolved = resolveApiKeyConfig(providerConfig.apiKey);
1027
- if (resolved) this.authStorage.setConfigApiKey(providerName, resolved);
1084
+ this.#installProviderApiKey(providerName, providerConfig.apiKey);
1028
1085
  }
1029
1086
 
1030
1087
  // Parse per-model overrides
1031
1088
  if (providerConfig.modelOverrides) {
1032
1089
  const perModel = new Map<string, ModelOverride>();
1033
1090
  for (const [modelId, override] of Object.entries(providerConfig.modelOverrides)) {
1034
- perModel.set(modelId, override);
1091
+ perModel.set(
1092
+ modelId,
1093
+ override.headers ? { ...override, headers: resolveConfigHeaders(override.headers) } : override,
1094
+ );
1035
1095
  }
1036
1096
  allModelOverrides.set(providerName, perModel);
1037
1097
  }
@@ -1179,7 +1239,7 @@ export class ModelRegistry {
1179
1239
  return {
1180
1240
  fetch: this.#fetch,
1181
1241
  getBearerApiKey: async provider => {
1182
- const apiKey = await this.authStorage.getApiKey(provider);
1242
+ const apiKey = await this.getApiKeyForProvider(provider);
1183
1243
  return apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth ? apiKey : undefined;
1184
1244
  },
1185
1245
  };
@@ -1443,10 +1503,9 @@ export class ModelRegistry {
1443
1503
  for (const [providerName, providerConfig] of Object.entries(config.providers ?? {})) {
1444
1504
  const modelDefs = providerConfig.models ?? [];
1445
1505
  if (modelDefs.length === 0) continue; // Override-only, no custom models
1506
+ const resolvedProviderHeaders = resolveConfigHeaders(providerConfig.headers);
1446
1507
  if (providerConfig.apiKey) {
1447
- this.#customProviderApiKeys.set(providerName, providerConfig.apiKey);
1448
- const resolved = resolveApiKeyConfig(providerConfig.apiKey);
1449
- if (resolved) this.authStorage.setConfigApiKey(providerName, resolved);
1508
+ this.#installProviderApiKey(providerName, providerConfig.apiKey);
1450
1509
  }
1451
1510
  for (const modelDef of modelDefs) {
1452
1511
  const providerCompat = providerConfig.disableStrictTools
@@ -1456,7 +1515,7 @@ export class ModelRegistry {
1456
1515
  providerName,
1457
1516
  providerConfig.baseUrl!,
1458
1517
  providerConfig.api as Api | undefined,
1459
- providerConfig.headers,
1518
+ resolvedProviderHeaders,
1460
1519
  providerConfig.apiKey,
1461
1520
  providerConfig.authHeader,
1462
1521
  providerCompat,
@@ -1626,7 +1685,10 @@ export class ModelRegistry {
1626
1685
  * as providers with stored credentials. See issue #993.
1627
1686
  */
1628
1687
  hasConfiguredAuth(model: Model<Api>): boolean {
1629
- return this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider);
1688
+ const commandKey = this.#resolveCommandBackedApiKey(model.provider);
1689
+ return (
1690
+ commandKey.configured || this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider)
1691
+ );
1630
1692
  }
1631
1693
 
1632
1694
  getDiscoverableProviders(): string[] {
@@ -1658,6 +1720,8 @@ export class ModelRegistry {
1658
1720
  * Get API key for a model.
1659
1721
  */
1660
1722
  async getApiKey(model: Model<Api>, sessionId?: string): Promise<string | undefined> {
1723
+ const commandKey = this.#resolveCommandBackedApiKey(model.provider);
1724
+ if (commandKey.configured) return commandKey.value;
1661
1725
  if (this.#keylessProviders.has(model.provider) && !this.authStorage.hasAuth(model.provider)) {
1662
1726
  return kNoAuth;
1663
1727
  }
@@ -1674,13 +1738,16 @@ export class ModelRegistry {
1674
1738
  async getApiKeyForProvider(
1675
1739
  provider: string,
1676
1740
  sessionId?: string,
1677
- options?: { baseUrl?: string; forceRefresh?: boolean; signal?: AbortSignal },
1741
+ options?: { baseUrl?: string; modelId?: string; forceRefresh?: boolean; signal?: AbortSignal },
1678
1742
  ): Promise<string | undefined> {
1743
+ const commandKey = this.#resolveCommandBackedApiKey(provider);
1744
+ if (commandKey.configured) return commandKey.value;
1679
1745
  if (this.#keylessProviders.has(provider) && !this.authStorage.hasAuth(provider)) {
1680
1746
  return kNoAuth;
1681
1747
  }
1682
1748
  return this.authStorage.getApiKey(provider, sessionId, {
1683
1749
  baseUrl: options?.baseUrl,
1750
+ modelId: options?.modelId,
1684
1751
  forceRefresh: options?.forceRefresh,
1685
1752
  signal: options?.signal,
1686
1753
  });
@@ -1696,6 +1763,8 @@ export class ModelRegistry {
1696
1763
  }
1697
1764
 
1698
1765
  async #peekApiKeyForProvider(provider: string): Promise<string | undefined> {
1766
+ const commandKey = this.#resolveCommandBackedApiKey(provider);
1767
+ if (commandKey.configured) return commandKey.value;
1699
1768
  if (this.#keylessProviders.has(provider) && !this.authStorage.hasAuth(provider)) {
1700
1769
  return kNoAuth;
1701
1770
  }
@@ -1819,11 +1888,9 @@ export class ModelRegistry {
1819
1888
  }
1820
1889
 
1821
1890
  if (config.apiKey) {
1822
- this.#customProviderApiKeys.set(providerName, config.apiKey);
1891
+ this.#installProviderApiKey(providerName, config.apiKey);
1823
1892
  // Persist runtime API keys so they survive #reloadStaticModels() cycles
1824
1893
  this.#runtimeProviderApiKeys.set(providerName, config.apiKey);
1825
- const resolved = resolveApiKeyConfig(config.apiKey);
1826
- if (resolved) this.authStorage.setConfigApiKey(providerName, resolved);
1827
1894
  }
1828
1895
 
1829
1896
  if (config.models && config.models.length > 0) {
@@ -1892,7 +1959,7 @@ export class ModelRegistry {
1892
1959
  cacheTtlMs: 24 * 60 * 60 * 1000,
1893
1960
  dynamicModelsAuthoritative: true,
1894
1961
  fetchDynamicModels: async () => {
1895
- const apiKey = await this.authStorage.peekApiKey(providerName);
1962
+ const apiKey = await this.#peekApiKeyForProvider(providerName);
1896
1963
  const resolvedKey = isAuthenticated(apiKey) ? apiKey : undefined;
1897
1964
  const modelDefs = await fetcher(resolvedKey);
1898
1965
  const results: Model<Api>[] = [];
@@ -1058,6 +1058,66 @@ export async function resolveAllowedModels(
1058
1058
  return available.filter(model => allowed.has(`${model.provider}/${model.id}`));
1059
1059
  }
1060
1060
 
1061
+ /**
1062
+ * Synchronous subset of {@link resolveAllowedModels} for contexts where async is unavailable
1063
+ * (e.g. `getAvailableModels()` which is called from the ACP model-list advertisement, RPC
1064
+ * `get_available_models`, and the `/model` slash command). Uses the same effective
1065
+ * `enabledModels` scope semantics as startup resolution:
1066
+ *
1067
+ * - Glob selectors match `provider/modelId` and bare model id
1068
+ * - Exact canonical ids expand to all available concrete variants
1069
+ * - Exact `provider/modelId`, bare ids, provider-scoped fuzzy, and substring selectors
1070
+ * resolve through the shared model-pattern matcher
1071
+ * - Optional `:thinkingLevel` suffixes are stripped only when valid
1072
+ *
1073
+ * When no pattern resolves to any model (misconfiguration / typo) an empty list is returned,
1074
+ * consistent with the empty-list contract of {@link resolveAllowedModels}. Callers that render
1075
+ * a UI picker should treat an empty list as "hide the picker entry", matching how the SDK
1076
+ * surfaces the same misconfiguration during session initialization.
1077
+ */
1078
+ export function filterAvailableModelsByEnabledPatterns(
1079
+ available: Model<Api>[],
1080
+ patterns: readonly string[],
1081
+ registry: Pick<ModelRegistry, "getCanonicalVariants">,
1082
+ ): Model<Api>[] {
1083
+ if (patterns.length === 0) return available;
1084
+
1085
+ const context = buildPreferenceContext(available, undefined);
1086
+ const allowed = new Set<string>();
1087
+ const addAllowed = (model: Model<Api>) => {
1088
+ allowed.add(`${model.provider}/${model.id}`);
1089
+ };
1090
+
1091
+ for (const pattern of patterns) {
1092
+ if (pattern.includes("*") || pattern.includes("?") || pattern.includes("[")) {
1093
+ const { base: globPattern } = splitThinkingSuffix(pattern);
1094
+ const glob = new Bun.Glob(globPattern.toLowerCase());
1095
+ for (const model of available) {
1096
+ const fullId = `${model.provider}/${model.id}`.toLowerCase();
1097
+ if (glob.match(fullId) || glob.match(model.id.toLowerCase())) {
1098
+ addAllowed(model);
1099
+ }
1100
+ }
1101
+ continue;
1102
+ }
1103
+
1104
+ const exactCanonical = resolveExactCanonicalScopePattern(pattern, registry, available);
1105
+ if (exactCanonical) {
1106
+ for (const model of exactCanonical.models) {
1107
+ addAllowed(model);
1108
+ }
1109
+ continue;
1110
+ }
1111
+
1112
+ const { model } = parseModelPatternWithContext(pattern, available, context, { modelRegistry: registry });
1113
+ if (model) {
1114
+ addAllowed(model);
1115
+ }
1116
+ }
1117
+
1118
+ return allowed.size === 0 ? [] : available.filter(model => allowed.has(`${model.provider}/${model.id}`));
1119
+ }
1120
+
1061
1121
  export interface ResolveCliModelResult {
1062
1122
  model: Model<Api> | undefined;
1063
1123
  selector?: string;
@@ -1,5 +1,4 @@
1
1
  import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
2
- import { TASK_SIMPLE_MODES } from "../task/simple-mode";
3
2
  import { AUTO_THINKING, getConfiguredThinkingLevelMetadata, getThinkingLevelMetadata } from "../thinking";
4
3
  import {
5
4
  TINY_MODEL_DEVICE_DEFAULT,
@@ -151,7 +150,7 @@ export type AnyUiMetadata = UiBase & {
151
150
 
152
151
  interface BooleanDef {
153
152
  type: "boolean";
154
- default: boolean;
153
+ default: boolean | undefined;
155
154
  ui?: UiBoolean;
156
155
  }
157
156
 
@@ -1177,13 +1176,13 @@ export const SETTINGS_SCHEMA = {
1177
1176
 
1178
1177
  "compaction.strategy": {
1179
1178
  type: "enum",
1180
- values: ["context-full", "handoff", "shake", "off"] as const,
1179
+ values: ["context-full", "handoff", "shake", "snapcompact", "off"] as const,
1181
1180
  default: "context-full",
1182
1181
  ui: {
1183
1182
  tab: "context",
1184
1183
  label: "Compaction Strategy",
1185
1184
  description:
1186
- "Choose in-place context-full maintenance, auto-handoff, surgical shake (drop heavy content), or disable auto maintenance (off)",
1185
+ "Choose in-place context-full maintenance, auto-handoff, surgical shake (drop heavy content), snapcompact (archive history as dense images), or disable auto maintenance (off)",
1187
1186
  options: [
1188
1187
  {
1189
1188
  value: "context-full",
@@ -1196,6 +1195,11 @@ export const SETTINGS_SCHEMA = {
1196
1195
  label: "Shake",
1197
1196
  description: "Drop heavy content (tool results + large blocks) in place; recover via artifact",
1198
1197
  },
1198
+ {
1199
+ value: "snapcompact",
1200
+ label: "Snapcompact",
1201
+ description: "Archive history onto dense bitmap images the model reads back; no LLM call",
1202
+ },
1199
1203
  {
1200
1204
  value: "off",
1201
1205
  label: "Off",
@@ -1326,6 +1330,17 @@ export const SETTINGS_SCHEMA = {
1326
1330
  ],
1327
1331
  },
1328
1332
  },
1333
+
1334
+ "compaction.supersedeReads": {
1335
+ type: "boolean",
1336
+ default: true,
1337
+ ui: {
1338
+ tab: "context",
1339
+ label: "Supersede Stale Reads",
1340
+ description: "Prune older read results when the same file is read again (cache-aware, runs every turn)",
1341
+ },
1342
+ },
1343
+
1329
1344
  // Branch summaries
1330
1345
  "branchSummary.enabled": {
1331
1346
  type: "boolean",
@@ -2067,6 +2082,20 @@ export const SETTINGS_SCHEMA = {
2067
2082
  type: "number",
2068
2083
  default: 4 * 1024 * 1024,
2069
2084
  },
2085
+ "shellMinimizer.sourceOutlineLevel": {
2086
+ type: "enum",
2087
+ values: ["default", "aggressive"] as const,
2088
+ default: "default",
2089
+ ui: {
2090
+ tab: "editing",
2091
+ label: "Shell Minimizer Source Outline",
2092
+ description: "Source outline mode for cat/read of source files: default or aggressive",
2093
+ },
2094
+ },
2095
+ "shellMinimizer.legacyFilters": {
2096
+ type: "boolean",
2097
+ default: undefined,
2098
+ },
2070
2099
 
2071
2100
  // Eval (per-backend toggles; add more as new backends ship, e.g. eval.ts)
2072
2101
  "eval.py": {
@@ -2100,6 +2129,16 @@ export const SETTINGS_SCHEMA = {
2100
2129
  description: "Whether to keep IPython kernel alive across calls",
2101
2130
  },
2102
2131
  },
2132
+ "python.interpreter": {
2133
+ type: "string",
2134
+ default: "",
2135
+ ui: {
2136
+ tab: "editing",
2137
+ label: "Python Interpreter",
2138
+ description:
2139
+ "Optional path to an exact Python executable. When set, automatic Python runtime discovery is skipped.",
2140
+ },
2141
+ },
2103
2142
 
2104
2143
  // ────────────────────────────────────────────────────────────────────────
2105
2144
  // Tools
@@ -2265,24 +2304,13 @@ export const SETTINGS_SCHEMA = {
2265
2304
  },
2266
2305
  },
2267
2306
 
2268
- "irc.enabled": {
2269
- type: "boolean",
2270
- default: true,
2271
- ui: {
2272
- tab: "tools",
2273
- label: "IRC",
2274
- description: "Enable agent-to-agent IRC messaging via the irc tool",
2275
- },
2276
- },
2277
-
2278
2307
  "irc.timeoutMs": {
2279
2308
  type: "number",
2280
2309
  default: 120_000,
2281
2310
  ui: {
2282
2311
  tab: "tools",
2283
2312
  label: "IRC Timeout",
2284
- description:
2285
- "Drop IRC messages whose recipient does not respond within this many milliseconds (0 disables the timeout)",
2313
+ description: "Default timeout for irc wait (and send await:true) in milliseconds; 0 disables the timeout",
2286
2314
  options: [
2287
2315
  { value: "0", label: "Disabled" },
2288
2316
  { value: "30000", label: "30 seconds" },
@@ -2477,7 +2505,7 @@ export const SETTINGS_SCHEMA = {
2477
2505
  ui: {
2478
2506
  tab: "tools",
2479
2507
  label: "Async Execution",
2480
- description: "Enable async bash commands and background task execution",
2508
+ description: "Enable async bash commands",
2481
2509
  },
2482
2510
  },
2483
2511
 
@@ -2723,31 +2751,14 @@ export const SETTINGS_SCHEMA = {
2723
2751
  },
2724
2752
  },
2725
2753
 
2726
- "task.simple": {
2727
- type: "enum",
2728
- values: TASK_SIMPLE_MODES,
2729
- default: "schema-free",
2754
+ "task.batch": {
2755
+ type: "boolean",
2756
+ default: true,
2730
2757
  ui: {
2731
2758
  tab: "tasks",
2732
- label: "Task Input Mode",
2733
- description: "How much shared structure the task tool accepts (default, schema-free, or independent)",
2734
- options: [
2735
- {
2736
- value: "default",
2737
- label: "Default",
2738
- description: "Shared context and custom task schema are available",
2739
- },
2740
- {
2741
- value: "schema-free",
2742
- label: "Schema-free",
2743
- description: "Shared context stays available, but custom task schema is disabled",
2744
- },
2745
- {
2746
- value: "independent",
2747
- label: "Independent",
2748
- description: "No shared context or custom task schema; each task must stand alone",
2749
- },
2750
- ],
2759
+ label: "Batch Task Calls",
2760
+ description:
2761
+ "Switch the task tool to its batch shape: one call carries { agent, context, tasks[] } — one subagent per item (with per-item isolation) and a required shared context prepended to every assignment. Each spawn still runs as an independent background agent with the normal idle/parked lifecycle. Disable to restore the flat single-spawn schema.",
2751
2762
  },
2752
2763
  },
2753
2764
 
@@ -2817,6 +2828,34 @@ export const SETTINGS_SCHEMA = {
2817
2828
  },
2818
2829
  },
2819
2830
 
2831
+ "task.agentIdleTtlMs": {
2832
+ type: "number",
2833
+ default: 420_000,
2834
+ ui: {
2835
+ tab: "tasks",
2836
+ label: "Agent Idle TTL",
2837
+ description:
2838
+ "How long an idle subagent stays live in memory before being parked to disk (ms). Parked agents are revived automatically when messaged or resumed. 0 keeps idle agents live until exit.",
2839
+ },
2840
+ },
2841
+
2842
+ "task.softRequestBudget": {
2843
+ type: "number",
2844
+ default: 90,
2845
+ ui: {
2846
+ tab: "tasks",
2847
+ label: "Soft Subagent Request Budget",
2848
+ description:
2849
+ "Soft per-subagent request budget (assistant requests per run). Crossing it injects one steering notice asking the subagent to wrap up; at 1.5x the budget the run is aborted gracefully, salvaging partial output. 0 disables the guard. Bundled explore/quick_task agents use a lower built-in budget.",
2850
+ options: [
2851
+ { value: "0", label: "Disabled" },
2852
+ { value: "40", label: "40 requests" },
2853
+ { value: "90", label: "90 requests", description: "Default" },
2854
+ { value: "150", label: "150 requests" },
2855
+ ],
2856
+ },
2857
+ },
2858
+
2820
2859
  "task.disabledAgents": {
2821
2860
  type: "array",
2822
2861
  default: [] as string[],
@@ -3257,21 +3296,23 @@ type Schema = typeof SETTINGS_SCHEMA;
3257
3296
  export type SettingPath = keyof Schema;
3258
3297
 
3259
3298
  /** Infer the value type for a setting path */
3260
- export type SettingValue<P extends SettingPath> = Schema[P] extends { type: "boolean" }
3261
- ? boolean
3262
- : Schema[P] extends { type: "string" }
3263
- ? string | undefined
3264
- : Schema[P] extends { type: "number" }
3265
- ? number
3266
- : Schema[P] extends { type: "enum"; values: infer V }
3267
- ? V extends readonly string[]
3268
- ? V[number]
3269
- : never
3270
- : Schema[P] extends { type: "array"; default: infer D }
3271
- ? D
3272
- : Schema[P] extends { type: "record"; default: infer D }
3299
+ export type SettingValue<P extends SettingPath> = Schema[P] extends { type: "boolean"; default: undefined }
3300
+ ? boolean | undefined
3301
+ : Schema[P] extends { type: "boolean" }
3302
+ ? boolean
3303
+ : Schema[P] extends { type: "string" }
3304
+ ? string | undefined
3305
+ : Schema[P] extends { type: "number" }
3306
+ ? number
3307
+ : Schema[P] extends { type: "enum"; values: infer V }
3308
+ ? V extends readonly string[]
3309
+ ? V[number]
3310
+ : never
3311
+ : Schema[P] extends { type: "array"; default: infer D }
3273
3312
  ? D
3274
- : never;
3313
+ : Schema[P] extends { type: "record"; default: infer D }
3314
+ ? D
3315
+ : never;
3275
3316
 
3276
3317
  /** Get the default value for a setting path */
3277
3318
  export function getDefault<P extends SettingPath>(path: P): SettingValue<P> {
@@ -3327,7 +3368,7 @@ export type TreeFilterMode = SettingValue<"treeFilterMode">;
3327
3368
 
3328
3369
  export interface CompactionSettings {
3329
3370
  enabled: boolean;
3330
- strategy: "context-full" | "handoff" | "shake" | "off";
3371
+ strategy: "context-full" | "handoff" | "shake" | "snapcompact" | "off";
3331
3372
  thresholdPercent: number;
3332
3373
  thresholdTokens: number;
3333
3374
  reserveTokens: number;
@@ -3339,6 +3380,7 @@ export interface CompactionSettings {
3339
3380
  idleEnabled: boolean;
3340
3381
  idleThresholdTokens: number;
3341
3382
  idleTimeoutSeconds: number;
3383
+ supersedeReads: boolean;
3342
3384
  }
3343
3385
 
3344
3386
  export interface ContextPromotionSettings {
@@ -3461,6 +3503,8 @@ export interface ShellMinimizerSettings {
3461
3503
  only: string[];
3462
3504
  except: string[];
3463
3505
  maxCaptureBytes: number;
3506
+ sourceOutlineLevel: "default" | "aggressive";
3507
+ legacyFilters: boolean | undefined;
3464
3508
  }
3465
3509
 
3466
3510
  /** Map group prefix -> typed settings interface */