@oh-my-pi/pi-coding-agent 17.2.8 → 17.2.10

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 (246) hide show
  1. package/CHANGELOG.md +94 -1
  2. package/dist/{CHANGELOG-0b0w17se.md → CHANGELOG-rhwzpexa.md} +94 -1
  3. package/dist/cli.js +13216 -15614
  4. package/dist/types/async/job-manager.d.ts +12 -0
  5. package/dist/types/cli/args.d.ts +1 -0
  6. package/dist/types/config/model-resolver.d.ts +13 -0
  7. package/dist/types/discovery/agents.d.ts +11 -0
  8. package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
  9. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  10. package/dist/types/extensibility/extensions/loader.d.ts +2 -2
  11. package/dist/types/extensibility/extensions/types.d.ts +34 -8
  12. package/dist/types/extensibility/hooks/types.d.ts +5 -5
  13. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
  14. package/dist/types/hindsight/state.d.ts +0 -1
  15. package/dist/types/index.d.ts +2 -2
  16. package/dist/types/lsp/utils.d.ts +8 -0
  17. package/dist/types/main.d.ts +1 -1
  18. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  19. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  20. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
  21. package/dist/types/modes/acp/acp-mode.d.ts +1 -1
  22. package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
  23. package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
  24. package/dist/types/modes/components/agent-hub.d.ts +12 -5
  25. package/dist/types/modes/components/model-browser.d.ts +9 -1
  26. package/dist/types/modes/interactive-mode.d.ts +8 -1
  27. package/dist/types/modes/session-observer-registry.d.ts +2 -0
  28. package/dist/types/registry/agent-lifecycle.d.ts +1 -1
  29. package/dist/types/registry/agent-registry.d.ts +47 -0
  30. package/dist/types/registry/persisted-agents.d.ts +3 -1
  31. package/dist/types/session/agent-session-types.d.ts +7 -0
  32. package/dist/types/session/agent-session.d.ts +8 -4
  33. package/dist/types/session/session-entries.d.ts +10 -0
  34. package/dist/types/session/session-loader.d.ts +11 -1
  35. package/dist/types/session/session-manager.d.ts +9 -1
  36. package/dist/types/session/session-tools.d.ts +10 -1
  37. package/dist/types/session/turn-recovery.d.ts +6 -2
  38. package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
  39. package/dist/types/slash-commands/available-commands.d.ts +1 -1
  40. package/dist/types/task/executor.d.ts +10 -0
  41. package/dist/types/task/index.d.ts +2 -3
  42. package/dist/types/task/read-only-policy.d.ts +3 -0
  43. package/dist/types/task/structured-subagent.d.ts +2 -0
  44. package/dist/types/task/types.d.ts +4 -0
  45. package/dist/types/tools/bash.d.ts +3 -3
  46. package/dist/types/tools/browser/launch.d.ts +3 -0
  47. package/dist/types/tools/browser/tab-worker.d.ts +2 -2
  48. package/dist/types/tools/path-utils.d.ts +8 -0
  49. package/dist/types/tools/read.d.ts +3 -5
  50. package/dist/types/tools/run-scope.d.ts +27 -1
  51. package/dist/types/tools/shell-tokenize.d.ts +21 -3
  52. package/dist/types/tools/terminal-output.d.ts +1 -1
  53. package/dist/types/utils/changelog.d.ts +3 -2
  54. package/dist/types/utils/late-cleanup.d.ts +2 -0
  55. package/dist/types/utils/turndown.d.ts +1 -1
  56. package/dist/types/vibe/runtime.d.ts +4 -3
  57. package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
  58. package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
  59. package/examples/custom-tools/README.md +1 -1
  60. package/examples/extensions/README.md +1 -1
  61. package/examples/extensions/api-demo.ts +5 -6
  62. package/examples/extensions/chalk-logger.ts +1 -1
  63. package/examples/extensions/hello.ts +2 -2
  64. package/examples/extensions/reload-runtime.ts +1 -1
  65. package/examples/extensions/tools.ts +2 -2
  66. package/examples/extensions/with-deps/index.ts +1 -1
  67. package/examples/sdk/06-extensions.ts +1 -1
  68. package/package.json +15 -31
  69. package/scripts/bundle-dist.ts +1 -11
  70. package/scripts/omp +11 -2
  71. package/src/advisor/runtime.ts +54 -0
  72. package/src/async/job-manager.ts +26 -0
  73. package/src/cli/agents-cli.ts +1 -1
  74. package/src/cli/args.ts +24 -2
  75. package/src/cli/auth-broker-cli.ts +1 -1
  76. package/src/cli/auth-gateway-cli.ts +1 -1
  77. package/src/cli/bench-cli.ts +1 -1
  78. package/src/cli/claude-trace-cli.ts +1 -1
  79. package/src/cli/config-cli.ts +1 -1
  80. package/src/cli/dry-balance-cli.ts +1 -1
  81. package/src/cli/file-processor.ts +1 -1
  82. package/src/cli/flag-tables.ts +4 -0
  83. package/src/cli/grep-cli.ts +1 -1
  84. package/src/cli/grievances-cli.ts +1 -1
  85. package/src/cli/help-extra.ts +1 -1
  86. package/src/cli/models-cli.ts +1 -1
  87. package/src/cli/plugin-cli.ts +1 -1
  88. package/src/cli/read-cli.ts +1 -1
  89. package/src/cli/setup-cli.ts +1 -1
  90. package/src/cli/shell-cli.ts +1 -1
  91. package/src/cli/ssh-cli.ts +1 -1
  92. package/src/cli/stats-cli.ts +4 -4
  93. package/src/cli/tiny-models-cli.ts +1 -1
  94. package/src/cli/ttsr-cli.ts +1 -1
  95. package/src/cli/update-cli.ts +3 -57
  96. package/src/cli/usage-cli.ts +1 -1
  97. package/src/cli/web-search-cli.ts +1 -1
  98. package/src/cli/worktree-cli.ts +1 -1
  99. package/src/commands/say.ts +1 -1
  100. package/src/commands/token.ts +1 -1
  101. package/src/commit/agentic/agent.ts +1 -1
  102. package/src/commit/cli.ts +1 -1
  103. package/src/config/config-file.ts +2 -2
  104. package/src/config/keybindings.ts +3 -3
  105. package/src/config/model-discovery.ts +36 -2
  106. package/src/config/model-registry.ts +4 -1
  107. package/src/config/model-resolver.ts +54 -7
  108. package/src/discovery/agents.ts +73 -3
  109. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
  110. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
  111. package/src/discovery/claude.ts +8 -5
  112. package/src/discovery/cursor.ts +8 -5
  113. package/src/discovery/gemini.ts +11 -8
  114. package/src/discovery/vscode.ts +1 -0
  115. package/src/discovery/windsurf.ts +4 -2
  116. package/src/edit/hashline/filesystem.ts +7 -7
  117. package/src/eval/agent-bridge.ts +1 -3
  118. package/src/eval/jl/prelude.jl +4 -4
  119. package/src/eval/js/shared/prelude.txt +2 -2
  120. package/src/eval/py/prelude.py +0 -3
  121. package/src/eval/py/runner.py +38 -1
  122. package/src/eval/rb/prelude.rb +1 -2
  123. package/src/extensibility/custom-commands/loader.ts +4 -4
  124. package/src/extensibility/custom-commands/types.ts +5 -5
  125. package/src/extensibility/custom-tools/loader.ts +4 -4
  126. package/src/extensibility/custom-tools/types.ts +1 -1
  127. package/src/extensibility/extensions/loader.ts +7 -6
  128. package/src/extensibility/extensions/types.ts +39 -8
  129. package/src/extensibility/hooks/loader.ts +4 -5
  130. package/src/extensibility/hooks/types.ts +5 -5
  131. package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
  132. package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
  133. package/src/hindsight/state.ts +2 -22
  134. package/src/index.ts +2 -2
  135. package/src/internal-urls/memory-protocol.ts +33 -0
  136. package/src/launch/terminal-output.ts +1 -1
  137. package/src/lsp/client.ts +2 -2
  138. package/src/lsp/utils.ts +29 -0
  139. package/src/main.ts +85 -21
  140. package/src/markit/converters/docx.ts +1 -1
  141. package/src/markit/converters/epub.ts +1 -1
  142. package/src/markit/converters/pptx.ts +1 -1
  143. package/src/markit/converters/xlsx.ts +1 -1
  144. package/src/modes/acp/acp-agent.ts +7 -6
  145. package/src/modes/acp/acp-client-bridge.ts +1 -1
  146. package/src/modes/acp/acp-event-mapper.ts +1 -1
  147. package/src/modes/acp/acp-mode.ts +1 -1
  148. package/src/modes/components/agent-hub-projection.ts +248 -0
  149. package/src/modes/components/agent-hub-renderer.ts +194 -0
  150. package/src/modes/components/agent-hub.ts +670 -192
  151. package/src/modes/components/agent-transcript-viewer.ts +1 -3
  152. package/src/modes/components/assistant-message.ts +1 -1
  153. package/src/modes/components/bordered-loader.ts +1 -1
  154. package/src/modes/components/custom-editor.ts +2 -2
  155. package/src/modes/components/extensions/inspector-panel.ts +11 -6
  156. package/src/modes/components/footer.ts +1 -3
  157. package/src/modes/components/hook-input.ts +1 -1
  158. package/src/modes/components/hook-selector.ts +2 -2
  159. package/src/modes/components/model-browser.ts +11 -3
  160. package/src/modes/components/model-hub.ts +4 -1
  161. package/src/modes/components/session-selector.ts +2 -2
  162. package/src/modes/components/status-line/segments.ts +4 -3
  163. package/src/modes/controllers/event-controller.ts +188 -7
  164. package/src/modes/controllers/extension-ui-controller.ts +4 -3
  165. package/src/modes/controllers/mcp-command-controller.ts +81 -20
  166. package/src/modes/controllers/selector-controller.ts +40 -49
  167. package/src/modes/controllers/streaming-reveal.ts +1 -1
  168. package/src/modes/interactive-mode.ts +13 -2
  169. package/src/modes/print-mode.ts +29 -12
  170. package/src/modes/rpc/rpc-mode.ts +2 -2
  171. package/src/modes/runtime-init.ts +1 -1
  172. package/src/modes/session-observer-registry.ts +5 -0
  173. package/src/modes/theme/theme.ts +2 -2
  174. package/src/prompts/tools/computer.md +1 -1
  175. package/src/registry/agent-lifecycle.ts +35 -10
  176. package/src/registry/agent-registry.ts +65 -2
  177. package/src/registry/persisted-agents.ts +341 -16
  178. package/src/sdk.ts +11 -5
  179. package/src/session/agent-session-types.ts +8 -0
  180. package/src/session/agent-session.ts +177 -188
  181. package/src/session/session-entries.ts +10 -0
  182. package/src/session/session-listing.ts +1 -1
  183. package/src/session/session-loader.ts +74 -8
  184. package/src/session/session-manager.ts +22 -2
  185. package/src/session/session-paths.ts +105 -110
  186. package/src/session/session-tools.ts +28 -1
  187. package/src/session/settings-stream-fn.ts +7 -3
  188. package/src/session/turn-recovery.ts +236 -18
  189. package/src/session/unexpected-stop-classifier.ts +12 -3
  190. package/src/slash-commands/acp-builtins.ts +1 -1
  191. package/src/slash-commands/available-commands.ts +1 -1
  192. package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
  193. package/src/task/agents.ts +1 -1
  194. package/src/task/executor.ts +173 -47
  195. package/src/task/index.ts +9 -28
  196. package/src/task/isolation-runner.ts +43 -12
  197. package/src/task/persisted-revive.ts +12 -5
  198. package/src/task/read-only-policy.ts +27 -0
  199. package/src/task/structured-subagent.ts +31 -6
  200. package/src/task/types.ts +4 -0
  201. package/src/tiny/worker.ts +1 -1
  202. package/src/tools/auto-generated-guard.ts +1 -1
  203. package/src/tools/bash-interactive.ts +4 -4
  204. package/src/tools/bash-interceptor.ts +8 -4
  205. package/src/tools/bash.ts +6 -6
  206. package/src/tools/browser/cmux/cmux-tab.ts +66 -18
  207. package/src/tools/browser/launch.ts +76 -19
  208. package/src/tools/browser/readable.ts +9 -9
  209. package/src/tools/browser/tab-supervisor.ts +12 -3
  210. package/src/tools/browser/tab-worker-entry.ts +1 -1
  211. package/src/tools/browser/tab-worker.ts +101 -12
  212. package/src/tools/fetch.ts +1 -1
  213. package/src/tools/glob.ts +4 -2
  214. package/src/tools/path-utils.ts +22 -5
  215. package/src/tools/read.ts +22 -5
  216. package/src/tools/run-scope.ts +290 -4
  217. package/src/tools/shell-tokenize.ts +38 -9
  218. package/src/tools/terminal-output.ts +1 -1
  219. package/src/tools/todo.ts +1 -1
  220. package/src/utils/changelog.ts +5 -4
  221. package/src/utils/clipboard.ts +10 -2
  222. package/src/utils/external-editor.ts +4 -2
  223. package/src/utils/jj.ts +1 -1
  224. package/src/utils/late-cleanup.ts +17 -0
  225. package/src/utils/turndown.ts +1 -2
  226. package/src/vibe/runtime.ts +202 -58
  227. package/src/web/scrapers/arxiv.ts +1 -1
  228. package/src/web/scrapers/go-pkg.ts +1 -1
  229. package/src/web/scrapers/hackage.ts +1 -12
  230. package/src/web/scrapers/iacr.ts +1 -1
  231. package/src/web/scrapers/readthedocs.ts +2 -1
  232. package/src/web/scrapers/twitter.ts +2 -2
  233. package/src/web/scrapers/types.ts +1 -1
  234. package/src/web/scrapers/wikipedia.ts +1 -1
  235. package/src/web/search/providers/browser-headers.ts +12 -39
  236. package/src/web/search/providers/codex.ts +3 -26
  237. package/src/web/search/providers/duckduckgo.ts +1 -1
  238. package/src/web/search/providers/ecosia.ts +2 -2
  239. package/src/web/search/providers/exa.ts +1 -1
  240. package/src/web/search/providers/firecrawl.ts +1 -1
  241. package/src/web/search/providers/google.ts +1 -1
  242. package/src/web/search/providers/mojeek.ts +2 -2
  243. package/src/web/search/providers/perplexity.ts +1 -1
  244. package/src/web/search/providers/public.ts +1 -1
  245. package/src/web/search/providers/startpage.ts +2 -2
  246. package/src/markit/converters/mammoth.d.ts +0 -24
@@ -10,9 +10,9 @@ import * as os from "node:os";
10
10
  import * as path from "node:path";
11
11
  import { Transform } from "node:stream";
12
12
  import { pipeline } from "node:stream/promises";
13
- import { $env, $which, APP_NAME, isEnoent, VERSION } from "@oh-my-pi/pi-utils";
13
+ import { $env, $which, APP_NAME, compareVersions, isEnoent, VERSION } from "@oh-my-pi/pi-utils";
14
+ import chalk from "@oh-my-pi/pi-utils/chalk";
14
15
  import { $ } from "bun";
15
- import chalk from "chalk";
16
16
  import { theme } from "../modes/theme/theme";
17
17
  import { isTimeoutError, withTimeoutSignal } from "../utils/fetch-timeout";
18
18
 
@@ -498,24 +498,6 @@ async function getLatestRelease(): Promise<ReleaseInfo> {
498
498
  };
499
499
  }
500
500
 
501
- /**
502
- * Compare semver versions. Returns:
503
- * - negative if a < b
504
- * - 0 if a == b
505
- * - positive if a > b
506
- */
507
- function compareVersions(a: string, b: string): number {
508
- const pa = a.split(".").map(Number);
509
- const pb = b.split(".").map(Number);
510
-
511
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
512
- const na = pa[i] || 0;
513
- const nb = pb[i] || 0;
514
- if (na !== nb) return na - nb;
515
- }
516
- return 0;
517
- }
518
-
519
501
  interface BunInstallCachePruneResult {
520
502
  scannedPackages: number;
521
503
  removedEntries: number;
@@ -532,42 +514,6 @@ function stripBunCacheVersionSuffix(name: string): string {
532
514
  return metadataIndex === -1 ? name : name.slice(0, metadataIndex);
533
515
  }
534
516
 
535
- function compareSemverIdentifier(a: string, b: string): number {
536
- const aNumber = /^\d+$/.test(a);
537
- const bNumber = /^\d+$/.test(b);
538
- if (aNumber && bNumber) return Number(a) - Number(b);
539
- if (aNumber) return -1;
540
- if (bNumber) return 1;
541
- return a.localeCompare(b);
542
- }
543
-
544
- function compareSemverLikeVersions(a: string, b: string): number {
545
- const [aCoreWithPrerelease] = a.split("+", 1);
546
- const [bCoreWithPrerelease] = b.split("+", 1);
547
- const [aCore, aPrerelease] = aCoreWithPrerelease.split("-", 2);
548
- const [bCore, bPrerelease] = bCoreWithPrerelease.split("-", 2);
549
- const aParts = aCore.split(".");
550
- const bParts = bCore.split(".");
551
- for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
552
- const diff = Number(aParts[i] ?? 0) - Number(bParts[i] ?? 0);
553
- if (diff !== 0 && Number.isFinite(diff)) return diff;
554
- }
555
- if (!aPrerelease && !bPrerelease) return 0;
556
- if (!aPrerelease) return 1;
557
- if (!bPrerelease) return -1;
558
- const aPrereleaseParts = aPrerelease.split(".");
559
- const bPrereleaseParts = bPrerelease.split(".");
560
- for (let i = 0; i < Math.max(aPrereleaseParts.length, bPrereleaseParts.length); i++) {
561
- const aPart = aPrereleaseParts[i];
562
- const bPart = bPrereleaseParts[i];
563
- if (aPart === undefined) return -1;
564
- if (bPart === undefined) return 1;
565
- const diff = compareSemverIdentifier(aPart, bPart);
566
- if (diff !== 0) return diff;
567
- }
568
- return 0;
569
- }
570
-
571
517
  async function readdirIfExists(dir: string): Promise<fs.Dirent[]> {
572
518
  try {
573
519
  return await fs.promises.readdir(dir, { withFileTypes: true });
@@ -689,7 +635,7 @@ export async function pruneBunInstallCache(
689
635
  scannedPackages++;
690
636
  let latestVersion: string | undefined;
691
637
  for (const version of group.actualDirs.keys()) {
692
- if (!latestVersion || compareSemverLikeVersions(version, latestVersion) > 0) latestVersion = version;
638
+ if (!latestVersion || compareVersions(version, latestVersion) > 0) latestVersion = version;
693
639
  }
694
640
  if (!latestVersion) continue;
695
641
  for (const [version, paths] of group.actualDirs) {
@@ -18,7 +18,7 @@ import {
18
18
  type UsageUnit,
19
19
  } from "@oh-my-pi/pi-ai";
20
20
  import { formatDuration, formatNumber, sanitizeText } from "@oh-my-pi/pi-utils";
21
- import chalk from "chalk";
21
+ import chalk from "@oh-my-pi/pi-utils/chalk";
22
22
  import { ModelRegistry } from "../config/model-registry";
23
23
  import { discoverAuthStorage } from "../sdk";
24
24
 
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { APP_NAME, getProjectDir } from "@oh-my-pi/pi-utils";
8
- import chalk from "chalk";
8
+ import chalk from "@oh-my-pi/pi-utils/chalk";
9
9
  import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
10
10
  import { Settings } from "../config/settings";
11
11
  import { initTheme, theme } from "../modes/theme/theme";
@@ -19,7 +19,7 @@
19
19
  import * as fs from "node:fs/promises";
20
20
  import * as path from "node:path";
21
21
  import { getWorktreesDir, isEnoent } from "@oh-my-pi/pi-utils";
22
- import chalk from "chalk";
22
+ import chalk from "@oh-my-pi/pi-utils/chalk";
23
23
  import { hasLiveIsolationOwner, ISOLATION_OWNER_FILE } from "../task/isolation-ownership";
24
24
  import * as git from "../utils/git";
25
25
 
@@ -10,8 +10,8 @@
10
10
  */
11
11
 
12
12
  import { getProjectDir } from "@oh-my-pi/pi-utils";
13
+ import chalk from "@oh-my-pi/pi-utils/chalk";
13
14
  import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
14
- import chalk from "chalk";
15
15
  import { sayHelp as commandHelp } from "../cli/command-help";
16
16
  import { Settings, settings } from "../config/settings";
17
17
  import { TTS_LOCAL_VOICE_VALUES } from "../tts/models";
@@ -3,8 +3,8 @@
3
3
  */
4
4
 
5
5
  import { PROVIDER_REGISTRY } from "@oh-my-pi/pi-ai";
6
+ import chalk from "@oh-my-pi/pi-utils/chalk";
6
7
  import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
7
- import chalk from "chalk";
8
8
  import { tokenHelp as commandHelp } from "../cli/command-help";
9
9
  import { isAuthenticated, ModelRegistry } from "../config/model-registry";
10
10
  import { discoverAuthStorage } from "../sdk";
@@ -2,8 +2,8 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, Model } from "@oh-my-pi/pi-ai";
3
3
  import { Markdown } from "@oh-my-pi/pi-tui";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
+ import chalk from "@oh-my-pi/pi-utils/chalk";
5
6
  import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
6
- import chalk from "chalk";
7
7
  import typesDescriptionPrompt from "../../commit/prompts/types-description.md" with { type: "text" };
8
8
  import type { ModelRegistry } from "../../config/model-registry";
9
9
  import type { Settings } from "../../config/settings";
package/src/commit/cli.ts CHANGED
@@ -1,4 +1,4 @@
1
- import chalk from "chalk";
1
+ import chalk from "@oh-my-pi/pi-utils/chalk";
2
2
  import type { CommitCommandArgs } from "./types";
3
3
 
4
4
  const FLAG_ALIASES = new Map<string, string>([
@@ -109,11 +109,11 @@ export class ConfigError extends Error {
109
109
  this.#message = message;
110
110
  }
111
111
 
112
- get message(): string {
112
+ override get message(): string {
113
113
  return this.#message;
114
114
  }
115
115
 
116
- toString(): string {
116
+ override toString(): string {
117
117
  return this.message;
118
118
  }
119
119
  }
@@ -607,12 +607,12 @@ export class KeybindingsManager extends TuiKeybindingsManager {
607
607
  this.setUserBindings(mergeKeybindingsConfig(inheritedConfig, profileConfig));
608
608
  }
609
609
 
610
- setUserBindings(userBindings: KeybindingsConfig): void {
610
+ override setUserBindings(userBindings: KeybindingsConfig): void {
611
611
  this.#userBindings = userBindings;
612
612
  super.setUserBindings(userBindings);
613
613
  }
614
614
 
615
- getKeys(keybinding: Keybinding): KeyId[] {
615
+ override getKeys(keybinding: Keybinding): KeyId[] {
616
616
  const keys = super.getKeys(keybinding);
617
617
  const fallbackKey = getFallbackKey(keybinding);
618
618
  if (fallbackKey === undefined || this.#userBindings[keybinding] !== undefined) return keys;
@@ -620,7 +620,7 @@ export class KeybindingsManager extends TuiKeybindingsManager {
620
620
  return removeKey(keys, fallbackKey);
621
621
  }
622
622
 
623
- getResolvedBindings(): KeybindingsConfig {
623
+ override getResolvedBindings(): KeybindingsConfig {
624
624
  const resolved = super.getResolvedBindings();
625
625
  resolved[FOLLOW_UP_KEYBINDING] = keyConfigValue(this.getKeys(FOLLOW_UP_KEYBINDING));
626
626
  return resolved;
@@ -724,6 +724,31 @@ export async function discoverLlamaCppModelRuntimeMetadata(
724
724
  }
725
725
  }
726
726
 
727
+ /**
728
+ * Read image-input support from an OpenAI-compatible `/v1/models` row. Handles
729
+ * direct `input` arrays, Synthetic-style top-level `input_modalities`, and
730
+ * OpenRouter-style `architecture.input_modalities`; returns undefined when none
731
+ * is present so the bundled reference (or the `["text"]` default) can take over.
732
+ */
733
+ function extractOpenAIModelsListInputCapabilities(item: {
734
+ input?: unknown;
735
+ input_modalities?: unknown;
736
+ architecture?: unknown;
737
+ }): ("text" | "image")[] | undefined {
738
+ const modalities = new Set<string>();
739
+ const collect = (value: unknown): void => {
740
+ if (!Array.isArray(value)) return;
741
+ for (const entry of value) {
742
+ if (typeof entry === "string") modalities.add(entry.toLowerCase());
743
+ }
744
+ };
745
+ collect(item.input);
746
+ collect(item.input_modalities);
747
+ if (isRecord(item.architecture)) collect(item.architecture.input_modalities);
748
+ if (modalities.size === 0) return undefined;
749
+ return modalities.has("image") ? ["text", "image"] : ["text"];
750
+ }
751
+
727
752
  export async function discoverOpenAIModelsList(
728
753
  providerConfig: DiscoveryProviderConfig,
729
754
  ctx: DiscoveryContext,
@@ -752,7 +777,14 @@ export async function discoverOpenAIModelsList(
752
777
  }
753
778
  headers = h;
754
779
  return (await res.json()) as {
755
- data?: Array<{ id?: string; max_model_len?: unknown; context_length?: unknown }>;
780
+ data?: Array<{
781
+ id?: string;
782
+ max_model_len?: unknown;
783
+ context_length?: unknown;
784
+ input?: unknown;
785
+ input_modalities?: unknown;
786
+ architecture?: unknown;
787
+ }>;
756
788
  };
757
789
  }),
758
790
  nativeMetadataPromise,
@@ -796,7 +828,9 @@ export async function discoverOpenAIModelsList(
796
828
  baseUrl,
797
829
  reasoning: reference?.reasoning ?? false,
798
830
  thinking: inheritReferenceThinking(undefined, reference, providerConfig.provider),
799
- input: nativeMetadataForModel?.input ?? reference?.input ?? ["text"],
831
+ input: nativeMetadataForModel?.input ??
832
+ extractOpenAIModelsListInputCapabilities(item) ??
833
+ reference?.input ?? ["text"],
800
834
  ...(providerConfig.discovery.type === "lm-studio" ? { imageInputDecoder: "stb" as const } : {}),
801
835
  // Proxy/gateway pricing is provider-specific and rarely matches
802
836
  // upstream bundled catalogs, so keep costs local-unknown even
@@ -1715,7 +1715,10 @@ export class ModelRegistry {
1715
1715
  return resolveOllamaModelCacheProviderId(providerConfig.provider, providerConfig.baseUrl);
1716
1716
  }
1717
1717
  if (providerConfig.discovery.type === "openai-models-list") {
1718
- return `${providerConfig.provider}:openai-models-list-context-v2`;
1718
+ // context-v3 invalidates rows cached before server-advertised input
1719
+ // modalities were parsed from `/v1/models`; warm v2 rows pinned
1720
+ // vision-capable ids at `input: ["text"]` until a forced refresh.
1721
+ return `${providerConfig.provider}:openai-models-list-context-v3`;
1719
1722
  }
1720
1723
  if (providerConfig.discovery.type === "litellm") {
1721
1724
  // rich-v2 invalidates rows cached before reseller usage-suffix stripping
@@ -27,7 +27,7 @@ import { DEFAULT_MODEL_PER_PROVIDER } from "@oh-my-pi/pi-catalog/provider-models
27
27
  import { resolveBareVariantAlias, resolveVariantAlias } from "@oh-my-pi/pi-catalog/variant-collapse";
28
28
  import { fuzzyMatch } from "@oh-my-pi/pi-tui";
29
29
  import { logger } from "@oh-my-pi/pi-utils";
30
- import chalk from "chalk";
30
+ import chalk from "@oh-my-pi/pi-utils/chalk";
31
31
  import MODEL_PRIO from "../priority.json" with { type: "json" };
32
32
  import {
33
33
  AUTO_THINKING,
@@ -932,6 +932,28 @@ function normalizeModelPatternList(value: string | string[] | undefined): string
932
932
  return patterns.map(pattern => pattern.trim()).filter(Boolean);
933
933
  }
934
934
 
935
+ /**
936
+ * Extract the first explicit model-role alias from a raw model selection.
937
+ *
938
+ * This intentionally runs before role expansion so callers can retain the
939
+ * source identity (`@smol`, `pi/slow`, or `*`) even when it resolves to a
940
+ * concrete provider/model or inherited fallback. Bare role names and explicit
941
+ * provider/model selectors are not role aliases.
942
+ */
943
+ export function resolveExplicitModelRole(
944
+ value: string | string[] | undefined,
945
+ settings?: ModelRoleLookup,
946
+ ): string | undefined {
947
+ for (const pattern of normalizeModelPatternList(value)) {
948
+ const prefixLength = modelRoleAliasPrefixLength(pattern);
949
+ if (prefixLength === undefined) continue;
950
+ const { base } = splitThinkingSuffix(pattern, prefixLength, MAX_THINKING_SUFFIX_OPTIONS);
951
+ const role = getModelRoleAlias(base, settings);
952
+ if (role) return role;
953
+ }
954
+ return undefined;
955
+ }
956
+
935
957
  function isSessionInheritedAgentPattern(value: string): boolean {
936
958
  return (
937
959
  value === DEFAULT_MODEL_ROLE ||
@@ -1068,6 +1090,8 @@ export function resolveConfiguredModelPatterns(
1068
1090
  });
1069
1091
  }
1070
1092
  export interface AgentModelPatternResolutionOptions {
1093
+ /** Highest-priority request selector, when supplied by a caller. */
1094
+ requestModel?: string | string[];
1071
1095
  settingsOverride?: string | string[];
1072
1096
  agentModel?: string | string[];
1073
1097
  settings?: Settings;
@@ -1075,11 +1099,25 @@ export interface AgentModelPatternResolutionOptions {
1075
1099
  fallbackModelPattern?: string;
1076
1100
  }
1077
1101
 
1078
- export function resolveAgentModelPatterns(options: AgentModelPatternResolutionOptions): string[] {
1079
- const { settingsOverride, agentModel, settings, activeModelPattern, fallbackModelPattern } = options;
1102
+ interface EffectiveAgentModelSelection {
1103
+ source?: string | string[];
1104
+ patterns: string[];
1105
+ }
1106
+
1107
+ function resolveEffectiveAgentModelSelection(
1108
+ options: AgentModelPatternResolutionOptions,
1109
+ ): EffectiveAgentModelSelection {
1110
+ const { requestModel, settingsOverride, agentModel, settings, activeModelPattern, fallbackModelPattern } = options;
1111
+
1112
+ const requestPatterns = resolveConfiguredModelPatterns(requestModel, settings);
1113
+ if (requestPatterns.length > 0) {
1114
+ return { source: requestModel, patterns: requestPatterns };
1115
+ }
1080
1116
 
1081
1117
  const overridePatterns = resolveConfiguredModelPatterns(settingsOverride, settings);
1082
- if (overridePatterns.length > 0) return overridePatterns;
1118
+ if (overridePatterns.length > 0) {
1119
+ return { source: settingsOverride, patterns: overridePatterns };
1120
+ }
1083
1121
 
1084
1122
  const normalizedAgentPatterns = normalizeModelPatternList(agentModel);
1085
1123
  const configuredAgentPatterns = resolveConfiguredModelPatterns(agentModel, settings);
@@ -1090,14 +1128,23 @@ export function resolveAgentModelPatterns(options: AgentModelPatternResolutionOp
1090
1128
  singleAgentPattern === formatModelRoleAlias("task") ||
1091
1129
  singleAgentPattern === `${LEGACY_MODEL_ROLE_ALIAS_PREFIX}task`
1092
1130
  ) {
1093
- return configuredAgentPatterns;
1131
+ return { source: agentModel, patterns: configuredAgentPatterns };
1094
1132
  }
1095
- if (!agentInheritsSessionModel) return configuredAgentPatterns;
1133
+ if (!agentInheritsSessionModel) return { source: agentModel, patterns: configuredAgentPatterns };
1096
1134
  }
1097
1135
 
1098
1136
  const fallback =
1099
1137
  activeModelPattern?.trim() || fallbackModelPattern?.trim() || settings?.getModelRole("default")?.trim() || "";
1100
- return resolveConfiguredModelPatterns(fallback, settings);
1138
+ return { patterns: resolveConfiguredModelPatterns(fallback, settings) };
1139
+ }
1140
+
1141
+ /** Return the raw selector source that supplies the effective agent patterns. */
1142
+ export function resolveAgentModelSource(options: AgentModelPatternResolutionOptions): string | string[] | undefined {
1143
+ return resolveEffectiveAgentModelSelection(options).source;
1144
+ }
1145
+
1146
+ export function resolveAgentModelPatterns(options: AgentModelPatternResolutionOptions): string[] {
1147
+ return resolveEffectiveAgentModelSelection(options).patterns;
1101
1148
  }
1102
1149
  /** Default prewalk hand-off target when no explicit target is configured. */
1103
1150
  export const DEFAULT_PREWALK_TARGET = "@smol";
@@ -28,9 +28,79 @@ const DISPLAY_NAME = "Agent Dirs (.agent/.agents)";
28
28
  const PRIORITY = 70;
29
29
  const AGENT_DIR_CANDIDATES = [".agent", ".agents"] as const;
30
30
 
31
- /** User-level paths: ~/.agent/<segments> and ~/.agents/<segments>. */
32
- function getUserPathCandidates(ctx: LoadContext, ...segments: string[]): string[] {
33
- return AGENT_DIR_CANDIDATES.map(baseDir => path.join(ctx.home, baseDir, ...segments));
31
+ interface UserPathCandidateOptions {
32
+ platform?: NodeJS.Platform;
33
+ env?: NodeJS.ProcessEnv;
34
+ windowsUserProfile?: () => string | undefined;
35
+ wslPath?: (windowsPath: string) => string | undefined;
36
+ }
37
+
38
+ const WINDOWS_DRIVE_PROFILE_PATTERN = /^([A-Za-z]):[\\/](.*)$/;
39
+
40
+ function isWsl(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): boolean {
41
+ return platform === "linux" && Boolean(env.WSL_DISTRO_NAME || env.WSL_INTEROP);
42
+ }
43
+
44
+ function convertWindowsPathToDefaultWslMount(windowsPath: string): string | undefined {
45
+ const trimmed = windowsPath.trim();
46
+ if (trimmed.length === 0) return undefined;
47
+ // The result is always a WSL (POSIX) path, so build it with posix
48
+ // semantics regardless of the host platform.
49
+ if (path.posix.isAbsolute(trimmed)) return path.posix.normalize(trimmed);
50
+ const match = WINDOWS_DRIVE_PROFILE_PATTERN.exec(trimmed);
51
+ if (!match) return undefined;
52
+ const [, drive, rest] = match;
53
+ const segments = rest.replace(/\\/g, "/").split("/").filter(Boolean);
54
+ return path.posix.join("/mnt", drive.toLowerCase(), ...segments);
55
+ }
56
+
57
+ function resolveWithWslPath(windowsPath: string): string | undefined {
58
+ try {
59
+ const result = Bun.spawnSync(["wslpath", "-u", windowsPath], { stdout: "pipe", stderr: "ignore" });
60
+ if (result.exitCode !== 0) return undefined;
61
+ const resolved = result.stdout.toString().trim();
62
+ return resolved.length > 0 ? resolved : undefined;
63
+ } catch {
64
+ return undefined;
65
+ }
66
+ }
67
+
68
+ function resolveWindowsUserProfile(): string | undefined {
69
+ try {
70
+ const result = Bun.spawnSync(["cmd.exe", "/d", "/c", "echo", "%USERPROFILE%"], {
71
+ stdout: "pipe",
72
+ stderr: "ignore",
73
+ });
74
+ if (result.exitCode !== 0) return undefined;
75
+ const resolved = result.stdout.toString().trim();
76
+ return resolved.length > 0 && resolved !== "%USERPROFILE%" ? resolved : undefined;
77
+ } catch {
78
+ return undefined;
79
+ }
80
+ }
81
+
82
+ /** Resolve the Windows host profile home exposed to WSL, if available. */
83
+ export function getWslWindowsHomeCandidate(options: UserPathCandidateOptions = {}): string | undefined {
84
+ const platform = options.platform ?? process.platform;
85
+ const env = options.env ?? process.env;
86
+ if (!isWsl(platform, env)) return undefined;
87
+ const userProfile = env.USERPROFILE ?? (options.windowsUserProfile ?? resolveWindowsUserProfile)();
88
+ if (!userProfile) return undefined;
89
+ return (options.wslPath ?? resolveWithWslPath)(userProfile) ?? convertWindowsPathToDefaultWslMount(userProfile);
90
+ }
91
+
92
+ function getUserHomeCandidates(ctx: LoadContext): string[] {
93
+ const homes = [ctx.home];
94
+ const wslHome = getWslWindowsHomeCandidate();
95
+ if (wslHome && !homes.includes(wslHome)) homes.push(wslHome);
96
+ return homes;
97
+ }
98
+
99
+ /** User-level paths: ~/.agent[s]/<segments>, plus the Windows host profile under WSL. */
100
+ export function getUserPathCandidates(ctx: LoadContext, ...segments: string[]): string[] {
101
+ return getUserHomeCandidates(ctx).flatMap(home =>
102
+ AGENT_DIR_CANDIDATES.map(baseDir => path.join(home, baseDir, ...segments)),
103
+ );
34
104
  }
35
105
 
36
106
  /**
@@ -27,14 +27,15 @@ const flag = (opts as { enabled: boolean })["enabled"];
27
27
 
28
28
  ## Use
29
29
 
30
- Prefer a schema parse at the boundary when a validator is available (Zod, Valibot, …) — validate once, then read from a fully typed value. If Zod is already in the project (e.g. Zod v4):
30
+ Prefer a schema parse at the boundary when a validator is available — validate
31
+ once, then read from a fully typed value:
31
32
 
32
33
  ```ts
33
- import { z } from "zod/v4";
34
+ import { type } from "@oh-my-pi/omptype";
34
35
 
35
- const Resp = z.object({ data: z.object({ id: z.string() }) });
36
+ const Resp = type({ data: { id: "string" } });
36
37
 
37
- const resp = Resp.parse(raw); // throws on bad input; resp.data.id is typed string
38
+ const resp = Resp.assert(raw); // throws on bad input; resp.data.id is typed string
38
39
  const id = resp.data.id;
39
40
  ```
40
41
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: "Do not extract 1-2 line functions that only wrap an expression — inline them"
3
- condition: "\\{\\s*return [^;{}\\n]+;?\\s*\\}|\\b(?:const|let|var)\\s+[\\w$]+\\s*=\\s*(\\([^)]*\\)|[a-zA-Z_$][\\w$]*)\\s*=>\\s*[^{\\n]+$"
3
+ condition: "(?m)\\{\\s*return [^;{}\\n]+;?\\s*\\}|\\b(?:const|let|var)\\s+[\\w$]+\\s*=\\s*(\\([^)]*\\)|[a-zA-Z_$][\\w$]*)\\s*=>\\s*[^{\\n]+$"
4
4
  scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
5
5
  interruptMode: never
6
6
  ---
@@ -90,6 +90,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
90
90
  const serverConfig = config as Record<string, unknown>;
91
91
  return {
92
92
  name,
93
+ enabled: typeof serverConfig.enabled === "boolean" ? serverConfig.enabled : undefined,
93
94
  timeout: typeof serverConfig.timeout === "number" ? serverConfig.timeout : undefined,
94
95
  command: serverConfig.command as string | undefined,
95
96
  args: serverConfig.args as string[] | undefined,
@@ -102,17 +103,19 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
102
103
  });
103
104
  };
104
105
 
105
- for (let i = 0; i < userPaths.length; i++) {
106
- const servers = parseMcpServers(contents[i], userPaths[i].path, userPaths[i].level);
106
+ // Load project entries before user entries so a project `enabled: false`
107
+ // claims its dedupe key before a same-named user server can survive (#7654).
108
+ const projectOffset = userPaths.length;
109
+ for (let i = 0; i < projectPaths.length; i++) {
110
+ const servers = parseMcpServers(contents[projectOffset + i], projectPaths[i].path, projectPaths[i].level);
107
111
  if (servers.length > 0) {
108
112
  items.push(...servers);
109
113
  break;
110
114
  }
111
115
  }
112
116
 
113
- const projectOffset = userPaths.length;
114
- for (let i = 0; i < projectPaths.length; i++) {
115
- const servers = parseMcpServers(contents[projectOffset + i], projectPaths[i].path, projectPaths[i].level);
117
+ for (let i = 0; i < userPaths.length; i++) {
118
+ const servers = parseMcpServers(contents[i], userPaths[i].path, userPaths[i].level);
116
119
  if (servers.length > 0) {
117
120
  items.push(...servers);
118
121
  break;
@@ -57,6 +57,7 @@ function parseMCPServers(
57
57
  const serverConfig = config as Record<string, unknown>;
58
58
  items.push({
59
59
  name,
60
+ enabled: typeof serverConfig.enabled === "boolean" ? serverConfig.enabled : undefined,
60
61
  command: serverConfig.command as string | undefined,
61
62
  args: serverConfig.args as string[] | undefined,
62
63
  env: serverConfig.env as Record<string, string> | undefined,
@@ -86,15 +87,17 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
86
87
 
87
88
  const projectContentPromise = projectPath ? readFile(projectPath) : Promise.resolve(null);
88
89
 
89
- if (userContent && userPath) {
90
- const result = parseMCPServers(userContent, userPath, "user");
90
+ // Load project entries before user entries so a project `enabled: false`
91
+ // claims its dedupe key before a same-named user server can survive (#7654).
92
+ const projectContent = await projectContentPromise;
93
+ if (projectContent && projectPath) {
94
+ const result = parseMCPServers(projectContent, projectPath, "project");
91
95
  items.push(...result.items);
92
96
  if (result.warning) warnings.push(result.warning);
93
97
  }
94
98
 
95
- const projectContent = await projectContentPromise;
96
- if (projectContent && projectPath) {
97
- const result = parseMCPServers(projectContent, projectPath, "project");
99
+ if (userContent && userPath) {
100
+ const result = parseMCPServers(userContent, userPath, "user");
98
101
  items.push(...result.items);
99
102
  if (result.warning) warnings.push(result.warning);
100
103
  }
@@ -48,14 +48,8 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
48
48
  const items: MCPServer[] = [];
49
49
  const warnings: string[] = [];
50
50
 
51
- // User-level: ~/.gemini/settings.json mcpServers
52
- const userPath = getUserPath(ctx, "gemini", "settings.json");
53
- if (userPath) {
54
- const result = await loadMCPFromSettings(ctx, userPath, "user");
55
- items.push(...result.items);
56
- if (result.warnings) warnings.push(...result.warnings);
57
- }
58
-
51
+ // Load project entries before user entries so a project `enabled: false`
52
+ // claims its dedupe key before a same-named user server can survive (#7654).
59
53
  // Project-level: .gemini/settings.json → mcpServers
60
54
  const projectPath = getProjectPath(ctx, "gemini", "settings.json");
61
55
  if (projectPath) {
@@ -64,6 +58,14 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
64
58
  if (result.warnings) warnings.push(...result.warnings);
65
59
  }
66
60
 
61
+ // User-level: ~/.gemini/settings.json → mcpServers
62
+ const userPath = getUserPath(ctx, "gemini", "settings.json");
63
+ if (userPath) {
64
+ const result = await loadMCPFromSettings(ctx, userPath, "user");
65
+ items.push(...result.items);
66
+ if (result.warnings) warnings.push(...result.warnings);
67
+ }
68
+
67
69
  return { items, warnings };
68
70
  }
69
71
 
@@ -102,6 +104,7 @@ async function loadMCPFromSettings(
102
104
 
103
105
  items.push({
104
106
  name,
107
+ enabled: typeof raw.enabled === "boolean" ? raw.enabled : undefined,
105
108
  command: typeof raw.command === "string" ? raw.command : undefined,
106
109
  args: Array.isArray(raw.args) ? (raw.args as string[]) : undefined,
107
110
  env: raw.env && typeof raw.env === "object" ? (raw.env as Record<string, string>) : undefined,
@@ -83,6 +83,7 @@ async function loadMCPConfig(
83
83
 
84
84
  const server: MCPServer = {
85
85
  name,
86
+ enabled: typeof expanded.enabled === "boolean" ? expanded.enabled : undefined,
86
87
  command: typeof expanded.command === "string" ? expanded.command : undefined,
87
88
  args: Array.isArray(expanded.args) ? (expanded.args as string[]) : undefined,
88
89
  env: expanded.env && typeof expanded.env === "object" ? (expanded.env as Record<string, string>) : undefined,
@@ -48,6 +48,7 @@ function parseServerConfig(
48
48
  return {
49
49
  server: {
50
50
  name,
51
+ enabled: typeof server.enabled === "boolean" ? server.enabled : undefined,
51
52
  command: server.command as string | undefined,
52
53
  args: server.args as string[] | undefined,
53
54
  env: server.env as Record<string, string> | undefined,
@@ -71,10 +72,11 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
71
72
  ]);
72
73
 
73
74
  const projectContent = projectPath ? await readFile(projectPath) : null;
74
-
75
+ // Load project entries before user entries so a project `enabled: false`
76
+ // claims its dedupe key before a same-named user server can survive (#7654).
75
77
  const configs: Array<{ content: string | null; path: string | null; scope: "user" | "project" }> = [
76
- { content: userContent, path: userPath, scope: "user" },
77
78
  { content: projectContent, path: projectPath, scope: "project" },
79
+ { content: userContent, path: userPath, scope: "user" },
78
80
  ];
79
81
 
80
82
  for (const { content, path, scope } of configs) {