@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
  3. package/dist/cli.js +10009 -9654
  4. package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
  5. package/dist/types/advisor/runtime.d.ts +8 -2
  6. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  7. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  8. package/dist/types/cli/args.d.ts +2 -0
  9. package/dist/types/cli/session-picker.d.ts +14 -7
  10. package/dist/types/collab/guest.d.ts +21 -2
  11. package/dist/types/commands/launch.d.ts +6 -0
  12. package/dist/types/config/model-discovery.d.ts +12 -0
  13. package/dist/types/config/settings-schema.d.ts +73 -8
  14. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  15. package/dist/types/cursor.d.ts +193 -8
  16. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  17. package/dist/types/edit/hashline/diff.d.ts +9 -1
  18. package/dist/types/edit/index.d.ts +8 -1
  19. package/dist/types/edit/streaming.d.ts +8 -1
  20. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  21. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  22. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
  23. package/dist/types/internal-urls/index.d.ts +1 -0
  24. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  25. package/dist/types/internal-urls/security-protocol.d.ts +15 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  28. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  29. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  30. package/dist/types/lsp/client.d.ts +2 -0
  31. package/dist/types/main.d.ts +2 -0
  32. package/dist/types/mcp/manager.d.ts +33 -4
  33. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  35. package/dist/types/modes/components/session-selector.d.ts +6 -0
  36. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  38. package/dist/types/modes/components/usage-row.d.ts +2 -0
  39. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  40. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  41. package/dist/types/modes/interactive-mode.d.ts +4 -2
  42. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  43. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  44. package/dist/types/modes/theme/theme.d.ts +12 -1
  45. package/dist/types/modes/types.d.ts +2 -1
  46. package/dist/types/sdk.d.ts +18 -0
  47. package/dist/types/security/auth.d.ts +30 -0
  48. package/dist/types/security/cloud.d.ts +79 -0
  49. package/dist/types/security/comparison.d.ts +49 -0
  50. package/dist/types/security/contracts/ids.d.ts +15 -0
  51. package/dist/types/security/contracts/index.d.ts +4 -0
  52. package/dist/types/security/contracts/schemas.d.ts +660 -0
  53. package/dist/types/security/contracts/types.d.ts +234 -0
  54. package/dist/types/security/contracts/validation.d.ts +5 -0
  55. package/dist/types/security/coordinator.d.ts +100 -0
  56. package/dist/types/security/importers/codex-security.d.ts +7 -0
  57. package/dist/types/security/importers/index.d.ts +2 -0
  58. package/dist/types/security/importers/sarif.d.ts +9 -0
  59. package/dist/types/security/index.d.ts +13 -0
  60. package/dist/types/security/preflight.d.ts +61 -0
  61. package/dist/types/security/provenance.d.ts +24 -0
  62. package/dist/types/security/publication.d.ts +78 -0
  63. package/dist/types/security/remediation.d.ts +27 -0
  64. package/dist/types/security/resource-output.d.ts +8 -0
  65. package/dist/types/security/sarif.d.ts +2 -0
  66. package/dist/types/security/store.d.ts +40 -0
  67. package/dist/types/session/agent-session-events.d.ts +2 -0
  68. package/dist/types/session/agent-session-types.d.ts +36 -1
  69. package/dist/types/session/agent-session.d.ts +4 -2
  70. package/dist/types/session/claude-session-store.d.ts +13 -0
  71. package/dist/types/session/codex-session-store.d.ts +14 -0
  72. package/dist/types/session/credential-pin.d.ts +58 -0
  73. package/dist/types/session/foreign-session-import.d.ts +15 -0
  74. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  75. package/dist/types/session/foreign-session-store.d.ts +23 -0
  76. package/dist/types/session/session-advisors.d.ts +47 -2
  77. package/dist/types/session/session-entries.d.ts +19 -1
  78. package/dist/types/session/session-handoff.d.ts +4 -1
  79. package/dist/types/session/session-loader.d.ts +2 -0
  80. package/dist/types/session/session-maintenance.d.ts +1 -1
  81. package/dist/types/session/session-manager.d.ts +21 -0
  82. package/dist/types/session/session-tools.d.ts +25 -1
  83. package/dist/types/session/turn-recovery.d.ts +10 -4
  84. package/dist/types/slash-commands/helpers/security.d.ts +2 -0
  85. package/dist/types/system-prompt.d.ts +4 -1
  86. package/dist/types/task/executor.d.ts +14 -2
  87. package/dist/types/task/render.d.ts +7 -0
  88. package/dist/types/thinking.d.ts +18 -6
  89. package/dist/types/tools/acp-bridge.d.ts +28 -5
  90. package/dist/types/tools/builtin-names.d.ts +1 -1
  91. package/dist/types/tools/grep.d.ts +17 -1
  92. package/dist/types/tools/hub/launch.d.ts +5 -1
  93. package/dist/types/tools/index.d.ts +10 -2
  94. package/dist/types/tools/path-utils.d.ts +19 -0
  95. package/dist/types/tools/read.d.ts +2 -0
  96. package/dist/types/tools/security-scan.d.ts +96 -0
  97. package/dist/types/tools/xdev.d.ts +2 -1
  98. package/dist/types/utils/changelog.d.ts +19 -0
  99. package/dist/types/utils/git.d.ts +10 -0
  100. package/dist/types/utils/jj.d.ts +11 -4
  101. package/dist/types/web/search/types.d.ts +2 -2
  102. package/package.json +12 -12
  103. package/scripts/security-compare.ts +40 -0
  104. package/src/advisor/runtime.ts +138 -12
  105. package/src/advisor/transcript-recorder.ts +56 -0
  106. package/src/auto-thinking/classifier.ts +48 -10
  107. package/src/cli/args.ts +6 -0
  108. package/src/cli/flag-tables.ts +2 -0
  109. package/src/cli/gallery-fixtures/fs.ts +25 -0
  110. package/src/cli/session-picker.ts +34 -17
  111. package/src/cli/ttsr-cli.ts +19 -1
  112. package/src/cli.ts +9 -0
  113. package/src/collab/guest.ts +37 -4
  114. package/src/collab/host.ts +1 -0
  115. package/src/commands/launch.ts +6 -0
  116. package/src/config/model-discovery.ts +32 -5
  117. package/src/config/model-registry.ts +107 -25
  118. package/src/config/settings-schema.ts +77 -6
  119. package/src/config/settings.ts +25 -0
  120. package/src/cursor-bridge-tools.ts +81 -0
  121. package/src/cursor.ts +463 -12
  122. package/src/edit/edit-clipboard.ts +23 -0
  123. package/src/edit/hashline/diff.ts +49 -10
  124. package/src/edit/hashline/execute.ts +38 -6
  125. package/src/edit/hashline/filesystem.ts +27 -3
  126. package/src/edit/index.ts +12 -2
  127. package/src/edit/renderer.ts +4 -4
  128. package/src/edit/streaming.ts +17 -2
  129. package/src/eval/py/prelude.py +7 -3
  130. package/src/export/html/template.js +1 -1
  131. package/src/extensibility/extensions/runner.ts +70 -0
  132. package/src/extensibility/extensions/types.ts +30 -0
  133. package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
  134. package/src/internal-urls/index.ts +1 -0
  135. package/src/internal-urls/local-protocol.ts +3 -2
  136. package/src/internal-urls/mcp-protocol.ts +4 -1
  137. package/src/internal-urls/router.ts +4 -1
  138. package/src/internal-urls/security-protocol.ts +261 -0
  139. package/src/internal-urls/types.ts +1 -1
  140. package/src/launch/terminal-output-worker-client.ts +53 -0
  141. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  142. package/src/launch/terminal-output-worker.ts +23 -0
  143. package/src/lsp/client.ts +18 -3
  144. package/src/lsp/index.ts +7 -0
  145. package/src/lsp/render.ts +1 -1
  146. package/src/main.ts +127 -56
  147. package/src/mcp/manager.ts +120 -18
  148. package/src/modes/acp/acp-agent.ts +15 -9
  149. package/src/modes/components/chat-transcript-builder.ts +30 -11
  150. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  151. package/src/modes/components/read-tool-group.ts +159 -17
  152. package/src/modes/components/session-selector.ts +19 -3
  153. package/src/modes/components/status-line/component.ts +381 -54
  154. package/src/modes/components/tool-execution.ts +78 -31
  155. package/src/modes/components/usage-row.ts +10 -5
  156. package/src/modes/controllers/event-controller.ts +26 -4
  157. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  158. package/src/modes/controllers/selector-controller.ts +94 -36
  159. package/src/modes/controllers/tan-command-controller.ts +14 -0
  160. package/src/modes/interactive-mode.ts +31 -17
  161. package/src/modes/rpc/host-uris.ts +6 -0
  162. package/src/modes/rpc/rpc-client.ts +18 -0
  163. package/src/modes/rpc/rpc-mode.ts +16 -1
  164. package/src/modes/rpc/rpc-types.ts +11 -0
  165. package/src/modes/theme/theme.ts +151 -6
  166. package/src/modes/types.ts +2 -1
  167. package/src/modes/utils/ui-helpers.ts +38 -21
  168. package/src/prompts/agents/security-reviewer.md +75 -0
  169. package/src/prompts/security/scan-coordinator.md +7 -0
  170. package/src/prompts/security/scan-request.md +21 -0
  171. package/src/prompts/security/validate-request.md +8 -0
  172. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  173. package/src/prompts/system/plan-mode-active.md +2 -2
  174. package/src/prompts/system/system-prompt.md +6 -0
  175. package/src/prompts/system/xdev-mount-notice.md +1 -1
  176. package/src/prompts/tools/bash.md +15 -17
  177. package/src/prompts/tools/checkpoint.md +1 -1
  178. package/src/prompts/tools/glob.md +7 -6
  179. package/src/prompts/tools/grep.md +7 -6
  180. package/src/prompts/tools/security-publish.md +1 -0
  181. package/src/prompts/tools/security-scan.md +1 -0
  182. package/src/sdk.ts +212 -21
  183. package/src/security/auth.ts +98 -0
  184. package/src/security/cloud.ts +686 -0
  185. package/src/security/comparison.ts +255 -0
  186. package/src/security/contracts/ids.ts +111 -0
  187. package/src/security/contracts/index.ts +4 -0
  188. package/src/security/contracts/schemas.ts +201 -0
  189. package/src/security/contracts/types.ts +254 -0
  190. package/src/security/contracts/validation.ts +65 -0
  191. package/src/security/coordinator.ts +708 -0
  192. package/src/security/importers/codex-security.ts +387 -0
  193. package/src/security/importers/index.ts +2 -0
  194. package/src/security/importers/sarif.ts +357 -0
  195. package/src/security/index.ts +13 -0
  196. package/src/security/preflight.ts +405 -0
  197. package/src/security/provenance.ts +106 -0
  198. package/src/security/publication.ts +326 -0
  199. package/src/security/remediation.ts +93 -0
  200. package/src/security/resource-output.ts +50 -0
  201. package/src/security/sarif.ts +78 -0
  202. package/src/security/store.ts +430 -0
  203. package/src/session/agent-session-events.ts +1 -0
  204. package/src/session/agent-session-types.ts +40 -1
  205. package/src/session/agent-session.ts +302 -175
  206. package/src/session/claude-session-store.ts +426 -0
  207. package/src/session/codex-session-store.ts +673 -0
  208. package/src/session/credential-pin.ts +93 -0
  209. package/src/session/foreign-session-import.ts +52 -0
  210. package/src/session/foreign-session-jsonl.ts +29 -0
  211. package/src/session/foreign-session-store.ts +26 -0
  212. package/src/session/model-controls.ts +11 -3
  213. package/src/session/session-advisors.ts +140 -16
  214. package/src/session/session-entries.ts +21 -1
  215. package/src/session/session-handoff.ts +20 -3
  216. package/src/session/session-loader.ts +25 -9
  217. package/src/session/session-maintenance.ts +59 -11
  218. package/src/session/session-manager.ts +61 -0
  219. package/src/session/session-tools.ts +107 -5
  220. package/src/session/settings-stream-fn.ts +1 -0
  221. package/src/session/turn-recovery.ts +97 -51
  222. package/src/slash-commands/builtin-registry.ts +152 -3
  223. package/src/slash-commands/helpers/security.ts +451 -0
  224. package/src/system-prompt.ts +7 -2
  225. package/src/task/agents.ts +2 -0
  226. package/src/task/executor.ts +20 -4
  227. package/src/task/render.ts +20 -4
  228. package/src/task/structured-subagent.ts +6 -4
  229. package/src/thinking.ts +29 -10
  230. package/src/tools/acp-bridge.ts +52 -8
  231. package/src/tools/browser.ts +5 -0
  232. package/src/tools/builtin-names.ts +1 -0
  233. package/src/tools/checkpoint.ts +0 -13
  234. package/src/tools/grep.ts +64 -8
  235. package/src/tools/hub/launch.ts +10 -15
  236. package/src/tools/index.ts +37 -5
  237. package/src/tools/path-utils.ts +91 -0
  238. package/src/tools/read.ts +3 -0
  239. package/src/tools/security-scan.ts +287 -0
  240. package/src/tools/write.ts +9 -3
  241. package/src/tools/xdev.ts +39 -14
  242. package/src/utils/changelog.ts +144 -4
  243. package/src/utils/git.ts +61 -14
  244. package/src/utils/jj.ts +19 -13
  245. package/src/web/search/providers/codex.ts +34 -0
  246. package/src/web/search/types.ts +1 -1
@@ -6,16 +6,26 @@ import type { SessionInfo } from "../session/session-listing";
6
6
  import { SessionManager } from "../session/session-manager";
7
7
  import { FileSessionStorage } from "../session/session-storage";
8
8
 
9
+ /** Presentation and capability controls for the standalone session picker. */
10
+ export interface SessionPickerOptions {
11
+ allSessions?: SessionInfo[];
12
+ title?: string;
13
+ scopeLabel?: string | false;
14
+ showCwd?: boolean;
15
+ allowDelete?: boolean;
16
+ allowGlobalScope?: boolean;
17
+ historySearch?: boolean;
18
+ }
19
+
9
20
  /**
10
21
  * Show the TUI session selector and return the selected session, or null if
11
- * cancelled. Rendered as a fullscreen overlay on the terminal's alternate
12
- * screen, so the list scrolls and rows are clickable with the mouse. Tab
13
- * toggles between current-folder and all-projects scope; the all-projects list
14
- * is loaded lazily via `SessionManager.listAll`.
22
+ * cancelled. The default OMP picker supports deletion, transcript-history
23
+ * search, and an all-projects scope; foreign import pickers disable those
24
+ * source-owned capabilities.
15
25
  */
16
26
  export async function selectSession(
17
27
  sessions: SessionInfo[],
18
- options?: { allSessions?: SessionInfo[] },
28
+ options: SessionPickerOptions = {},
19
29
  ): Promise<SessionInfo | null> {
20
30
  const { promise, resolve } = Promise.withResolvers<SessionInfo | null>();
21
31
  const ui = new TUI(new ProcessTerminal());
@@ -26,11 +36,13 @@ export async function selectSession(
26
36
  // session-list prefix never sees. Best-effort: a missing/locked history.db
27
37
  // must not break the picker.
28
38
  let historyMatcher: ((query: string) => string[]) | undefined;
29
- try {
30
- const history = HistoryStorage.open();
31
- historyMatcher = (query: string) => history.matchingSessionIds(query);
32
- } catch (error) {
33
- logger.warn("History storage unavailable for session ranking", { error: String(error) });
39
+ if (options.historySearch !== false) {
40
+ try {
41
+ const history = HistoryStorage.open();
42
+ historyMatcher = (query: string) => history.matchingSessionIds(query);
43
+ } catch (error) {
44
+ logger.warn("History storage unavailable for session ranking", { error: String(error) });
45
+ }
34
46
  }
35
47
 
36
48
  const showSelector = () => {
@@ -58,16 +70,21 @@ export async function selectSession(
58
70
  }
59
71
  },
60
72
  {
61
- onDelete: async (session: SessionInfo) => {
62
- // Delete handler - SessionList will show confirmation internally
63
- await storage.deleteSessionWithArtifacts(session.path);
64
- return true;
65
- },
73
+ onDelete:
74
+ options.allowDelete === false
75
+ ? undefined
76
+ : async (session: SessionInfo) => {
77
+ await storage.deleteSessionWithArtifacts(session.path);
78
+ return true;
79
+ },
66
80
  historyMatcher,
67
- loadAllSessions: () => SessionManager.listAll(storage),
68
- allSessions: options?.allSessions,
81
+ loadAllSessions: options.allowGlobalScope === false ? undefined : () => SessionManager.listAll(storage),
82
+ allSessions: options.allSessions,
69
83
  getTerminalRows: () => ui.terminal.rows,
70
84
  fillHeight: true,
85
+ title: options.title,
86
+ scopeLabel: options.scopeLabel,
87
+ showCwd: options.showCwd,
71
88
  },
72
89
  );
73
90
  return selector;
@@ -94,12 +94,18 @@ interface TestReport {
94
94
  evaluated: number;
95
95
  triggered: RuleMatchDetail[];
96
96
  notTriggered: RuleMatchDetail[];
97
+ /**
98
+ * Set when a file path was supplied but the match source was inferred as
99
+ * `text` (extension absent from {@link SOURCE_FILE_EXT}), so callers can
100
+ * surface why a source file was evaluated against a prose context.
101
+ */
102
+ inferenceNote?: string;
97
103
  }
98
104
 
99
105
  const STDIN_MARKER = "-";
100
106
  /** Extensions treated as source files for default tool-context inference. */
101
107
  const SOURCE_FILE_EXT =
102
- /^\.(ts|tsx|js|jsx|mjs|cjs|rs|py|go|java|kt|swift|c|cc|cpp|h|hpp|rb|php|lua|css|scss|html|json|ya?ml|toml|md|mdc)$/i;
108
+ /^\.(ts|tsx|js|jsx|mjs|cjs|rs|py|go|java|kt|swift|c|cc|cpp|h|hpp|rb|php|lua|css|scss|html|json|ya?ml|toml|md|mdc|cs|razor|cshtml|fs|fsx|vb|sh|bash|sql|zig|dart|scala|ex|exs|proto|tf)$/i;
103
109
 
104
110
  const BINARY_PROBE_BYTES = 8192;
105
111
  const DEFAULT_MAX_SCAN_BYTES = 5 * 1024 * 1024;
@@ -342,6 +348,14 @@ async function runTest(args: TtsrTestArgs, json: boolean, cwd: string): Promise<
342
348
  args.source ?? (filePath && SOURCE_FILE_EXT.test(path.extname(filePath)) ? "tool" : "text");
343
349
  const tool = args.tool ?? (source === "tool" ? "edit" : undefined);
344
350
 
351
+ // A supplied source file whose extension is unknown falls through to the
352
+ // text (prose) context, where tool-scoped rules can never match. Surface
353
+ // that so a false negative reads as a context mismatch, not a bad regex.
354
+ const inferenceNote =
355
+ !args.source && filePath && source === "text"
356
+ ? `inferred --source text from '${path.extname(filePath) || filePath}' (not in the source-file extension set); pass --source tool --tool edit to evaluate tool-scoped rules`
357
+ : undefined;
358
+
345
359
  const context: TtsrMatchContext = {
346
360
  source,
347
361
  toolName: tool,
@@ -373,6 +387,7 @@ async function runTest(args: TtsrTestArgs, json: boolean, cwd: string): Promise<
373
387
  evaluated: rules.length,
374
388
  triggered,
375
389
  notTriggered,
390
+ inferenceNote,
376
391
  };
377
392
 
378
393
  if (json) {
@@ -390,6 +405,9 @@ function renderTestReport(report: TestReport, verbose: boolean, isolated: boolea
390
405
  `${chalk.bold("TTSR test")} — source=${chalk.cyan(ctxLabel)}${pathLabel} snippet=${chalk.dim(`${report.snippetBytes}b`)}\n`,
391
406
  );
392
407
  process.stdout.write(`${chalk.dim(` "${report.snippetPreview}"`)}\n\n`);
408
+ if (report.inferenceNote) {
409
+ process.stdout.write(`${chalk.yellow(`note: ${report.inferenceNote}`)}\n\n`);
410
+ }
393
411
 
394
412
  if (report.triggered.length === 0) {
395
413
  process.stdout.write(`${chalk.red("No rules triggered.")} (evaluated ${report.evaluated})\n`);
package/src/cli.ts CHANGED
@@ -32,6 +32,7 @@ import { extractProfileFlags } from "./cli/profile-bootstrap";
32
32
  import { startJsEvalProcess } from "./eval/js/process-entry";
33
33
  import type { WorkerInbound as JsWorkerInbound, WorkerOutbound as JsWorkerOutbound } from "./eval/js/worker-protocol";
34
34
  import { DAEMON_BROKER_WORKER_ARG } from "./launch/protocol";
35
+ import { TERMINAL_OUTPUT_WORKER_ARG } from "./launch/terminal-output-worker-protocol";
35
36
  import { COMPUTER_WORKER_ARG } from "./tools/computer/protocol";
36
37
  import { smokeTestComputerWorker } from "./tools/computer/supervisor";
37
38
  import { startComputerWorker } from "./tools/computer/worker-entry";
@@ -88,6 +89,7 @@ async function runSmokeTest(): Promise<void> {
88
89
  const { smokeTestJsEvalWorker } = await import("./eval/js/context-manager");
89
90
  // Other smoke dependencies stay lazy so normal CLI startup does not load their worker clients.
90
91
  const { smokeTestDaemonBroker } = await import("./launch/client");
92
+ const { smokeTestTerminalOutputWorker } = await import("./launch/terminal-output-worker-client");
91
93
  await smokeTestSyncWorker();
92
94
 
93
95
  const statsServer = await startServer(0);
@@ -109,6 +111,7 @@ async function runSmokeTest(): Promise<void> {
109
111
  await smokeTestTtsWorker();
110
112
  await smokeTestMnemopiEmbedWorker();
111
113
  await smokeTestDaemonBroker();
114
+ await smokeTestTerminalOutputWorker();
112
115
  process.stdout.write("smoke-test: ok\n");
113
116
  }
114
117
 
@@ -195,6 +198,12 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
195
198
  await runIpcSubprocessWorker(startMnemopiEmbedWorker);
196
199
  return true;
197
200
  }
201
+ if (arg === TERMINAL_OUTPUT_WORKER_ARG) {
202
+ if (parentPort) installWorkerInbox(parentPort);
203
+ // This selector is the isolation boundary; a static import would evaluate xterm in normal CLI startup.
204
+ await import("./launch/terminal-output-worker");
205
+ return true;
206
+ }
198
207
  if (arg === DAEMON_BROKER_WORKER_ARG) {
199
208
  // Worker selectors must dispatch before the normal command graph loads.
200
209
  const { startDaemonBrokerFromEnvironment } = await import("./launch/broker");
@@ -85,14 +85,15 @@ interface PendingSnapshot {
85
85
  /** Minimal context surface the idle-state reconciler mutates. */
86
86
  export interface GuestIdleReconcilerCtx {
87
87
  statusLine: { markActivityEnd: () => void };
88
+ statusContainer: Pick<InteractiveModeContext["statusContainer"], "disposeChildren">;
88
89
  loadingAnimation: { stop: () => void } | undefined;
89
90
  }
90
91
 
91
92
  /**
92
93
  * Close the guest UI state held open by an earlier `agent_start` whose
93
94
  * matching `agent_end` never reached us — most often because a reconnect
94
- * dropped the event mid-stream. Triggered from {@link CollabGuestLink}'s
95
- * `state` reconciler when the host reports `isStreaming === false`:
95
+ * dropped the event mid-stream. Reached via {@link reconcileGuestSnapshotHostState}
96
+ * (the live `state`-frame and welcome/resync reconciler) when the host reports `isStreaming === false`:
96
97
  * folds the in-flight active-time window into the per-session meter (so
97
98
  * `time_spent` stops ticking) and stops the `Working…` loader if one is
98
99
  * still animating. No-op when the host is still streaming.
@@ -106,17 +107,49 @@ export function reconcileGuestIdleHostState(ctx: GuestIdleReconcilerCtx, isStrea
106
107
  if (ctx.loadingAnimation) {
107
108
  ctx.loadingAnimation.stop();
108
109
  ctx.loadingAnimation = undefined;
110
+ ctx.statusContainer.disposeChildren();
109
111
  }
110
112
  }
111
113
 
112
114
  /** Reconcile a welcome/resync snapshot's host activity state into the guest meter. */
113
115
  export interface GuestSnapshotActivityReconcilerCtx extends GuestIdleReconcilerCtx {
114
116
  statusLine: GuestIdleReconcilerCtx["statusLine"] & { markActivityStart: () => void };
117
+ /**
118
+ * Start (or re-attach) the live "Working…" loader. Mirrors
119
+ * `InteractiveModeContext.ensureLoadingAnimation`, which is what
120
+ * `EventController` calls on `agent_start`. Required so a guest that
121
+ * missed an earlier `agent_start` (a reconnect dropped it mid-stream)
122
+ * starts its spinner when the host later reports it is streaming.
123
+ */
124
+ ensureLoadingAnimation: InteractiveModeContext["ensureLoadingAnimation"];
125
+ autoCompactionLoader: InteractiveModeContext["autoCompactionLoader"];
126
+ retryLoader: InteractiveModeContext["retryLoader"];
127
+ }
128
+
129
+ /** Status-area state which cannot outlive removal of its child components. */
130
+ export interface GuestTransientStatusCtx {
131
+ statusContainer: Pick<InteractiveModeContext["statusContainer"], "clear">;
132
+ autoCompactionLoader: InteractiveModeContext["autoCompactionLoader"];
133
+ retryLoader: InteractiveModeContext["retryLoader"];
134
+ }
135
+
136
+ /** Stop and forget status-area loaders before detaching their components. */
137
+ export function clearGuestTransientStatus(ctx: GuestTransientStatusCtx): void {
138
+ if (ctx.autoCompactionLoader) {
139
+ ctx.autoCompactionLoader.stop();
140
+ ctx.autoCompactionLoader = undefined;
141
+ }
142
+ if (ctx.retryLoader) {
143
+ ctx.retryLoader.stop();
144
+ ctx.retryLoader = undefined;
145
+ }
146
+ ctx.statusContainer.clear();
115
147
  }
116
148
 
117
149
  export function reconcileGuestSnapshotHostState(ctx: GuestSnapshotActivityReconcilerCtx, isStreaming: boolean): void {
118
150
  if (isStreaming) {
119
151
  ctx.statusLine.markActivityStart();
152
+ if (!ctx.autoCompactionLoader && !ctx.retryLoader) ctx.ensureLoadingAnimation();
120
153
  return;
121
154
  }
122
155
  reconcileGuestIdleHostState(ctx, false);
@@ -482,7 +515,7 @@ export class CollabGuestLink {
482
515
  this.#applyHostState(frame.state);
483
516
  setSessionTerminalTitle(frame.state.sessionName, frame.state.cwd);
484
517
  this.#updateStatusSegment();
485
- reconcileGuestIdleHostState(this.#ctx, frame.state.isStreaming);
518
+ reconcileGuestSnapshotHostState(this.#ctx, frame.state.isStreaming);
486
519
  this.#ctx.statusLine.invalidate();
487
520
  this.#ctx.ui.requestRender();
488
521
  break;
@@ -681,7 +714,7 @@ export class CollabGuestLink {
681
714
 
682
715
  #clearTransientUi(): void {
683
716
  this.#clearUiRequests();
684
- this.#ctx.statusContainer.clear();
717
+ clearGuestTransientStatus(this.#ctx);
685
718
  this.#ctx.pendingMessagesContainer.clear();
686
719
  this.#ctx.compactionQueuedMessages = [];
687
720
  this.#ctx.streamingComponent = undefined;
@@ -53,6 +53,7 @@ const STATE_TRIGGER_EVENTS: Record<string, true> = {
53
53
  message_end: true,
54
54
  tool_execution_end: true,
55
55
  thinking_level_changed: true,
56
+ model_changed: true,
56
57
  auto_compaction_end: true,
57
58
  };
58
59
 
@@ -99,6 +99,12 @@ export default class Index extends Command {
99
99
  char: "r",
100
100
  description: "Resume a session (by ID prefix, path, or picker if omitted)",
101
101
  }),
102
+ "from-claude": Flags.boolean({
103
+ description: "Import a Claude Code session into OMP",
104
+ }),
105
+ "from-codex": Flags.boolean({
106
+ description: "Import a Codex session into OMP",
107
+ }),
102
108
  "session-dir": Flags.string({
103
109
  description: "Directory for session storage and lookup",
104
110
  }),
@@ -62,6 +62,33 @@ async function withTimeoutSignal<T>(timeoutMs: number, fn: (signal: AbortSignal)
62
62
  }
63
63
  }
64
64
 
65
+ /** Generous discovery budget for a non-loopback (remote / LAN) inference host. */
66
+ const REMOTE_DISCOVERY_TIMEOUT_MS = 10_000;
67
+
68
+ /**
69
+ * Pick a discovery-probe timeout for a local-engine base URL.
70
+ *
71
+ * The implicit `127.0.0.1` default probe keeps a tight `loopbackMs` cap so a
72
+ * busy or foreign service on the default port never stalls startup. But that
73
+ * cap is far too short for a host reached over the network: a user who points
74
+ * `LLAMA_CPP_BASE_URL` / `OLLAMA_BASE_URL` / `OLLAMA_HOST` at a remote or LAN
75
+ * machine has real round-trip latency, and a 250ms cap made that server look
76
+ * empty (issue #7087). Anything that is not strictly loopback therefore gets
77
+ * {@link REMOTE_DISCOVERY_TIMEOUT_MS}.
78
+ */
79
+ export function discoveryProbeTimeoutMs(baseUrl: string, loopbackMs: number): number {
80
+ let hostname: string;
81
+ try {
82
+ hostname = new URL(baseUrl).hostname;
83
+ } catch {
84
+ return loopbackMs;
85
+ }
86
+ hostname = hostname.replace(/^\[/, "").replace(/\]$/, "");
87
+ const isLoopback =
88
+ hostname === "localhost" || hostname === "0.0.0.0" || hostname === "::1" || /^127\./.test(hostname);
89
+ return isLoopback ? loopbackMs : REMOTE_DISCOVERY_TIMEOUT_MS;
90
+ }
91
+
65
92
  const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434";
66
93
  const OLLAMA_HOST_DEFAULT_PORT = "11434";
67
94
 
@@ -391,7 +418,7 @@ async function discoverOllamaModelMetadata(
391
418
  ): Promise<OllamaDiscoveredModelMetadata | null> {
392
419
  const showUrl = `${endpoint}/api/show`;
393
420
  try {
394
- const payload = await withTimeoutSignal(150, async signal => {
421
+ const payload = await withTimeoutSignal(discoveryProbeTimeoutMs(endpoint, 150), async signal => {
395
422
  const response = await ctx.fetch(showUrl, {
396
423
  method: "POST",
397
424
  headers: { ...(headers ?? {}), "Content-Type": "application/json" },
@@ -444,7 +471,7 @@ export async function discoverOllamaModels(
444
471
  const endpoint = normalizeOllamaBaseUrl(providerConfig.baseUrl);
445
472
  const tagsUrl = `${endpoint}/api/tags`;
446
473
  const headers = { ...(providerConfig.headers ?? {}) };
447
- const payload = await withTimeoutSignal(250, async signal => {
474
+ const payload = await withTimeoutSignal(discoveryProbeTimeoutMs(endpoint, 250), async signal => {
448
475
  const response = await ctx.fetch(tagsUrl, {
449
476
  headers,
450
477
  signal,
@@ -491,7 +518,7 @@ async function discoverLlamaCppServerMetadata(
491
518
  ): Promise<LlamaCppDiscoveredServerMetadata | null> {
492
519
  const propsUrl = `${toLlamaCppNativeBaseUrl(baseUrl)}/props`;
493
520
  try {
494
- const payload = await withTimeoutSignal(150, async signal => {
521
+ const payload = await withTimeoutSignal(discoveryProbeTimeoutMs(baseUrl, 150), async signal => {
495
522
  const response = await ctx.fetch(propsUrl, {
496
523
  headers,
497
524
  signal,
@@ -567,7 +594,7 @@ export async function discoverLlamaCppModels(
567
594
  let headers = baseHeaders;
568
595
  const attempt = async (h: Record<string, string>) => {
569
596
  const [payload, metadata] = await Promise.all([
570
- withTimeoutSignal(250, async signal => {
597
+ withTimeoutSignal(discoveryProbeTimeoutMs(baseUrl, 250), async signal => {
571
598
  const response = await ctx.fetch(modelsUrl, {
572
599
  headers: h,
573
600
  signal,
@@ -641,7 +668,7 @@ export async function discoverLlamaCppModelRuntimeMetadata(
641
668
  const baseHeaders: Record<string, string> = { ...(model.headers ?? {}) };
642
669
  const attempt = async (headers: Record<string, string>) => {
643
670
  const [entries, serverMetadata] = await Promise.all([
644
- withTimeoutSignal(250, async signal => {
671
+ withTimeoutSignal(discoveryProbeTimeoutMs(nativeBaseUrl, 250), async signal => {
645
672
  const response = await ctx.fetch(modelsUrl, {
646
673
  headers,
647
674
  signal,
@@ -27,6 +27,7 @@ import {
27
27
  openaiCodexModelManagerOptions,
28
28
  PROVIDER_DESCRIPTORS,
29
29
  resolveModelCacheProviderId,
30
+ resolveOllamaModelCacheProviderId,
30
31
  } from "@oh-my-pi/pi-catalog/provider-models";
31
32
  import {
32
33
  collapseBuiltModelVariants,
@@ -539,6 +540,12 @@ interface ModelPatch {
539
540
  */
540
541
  type ModelTransportPolicy = "merge" | "replace";
541
542
 
543
+ /**
544
+ * Credential-aware model projection supplied by an extension provider. Receives
545
+ * the fully composed catalog and returns the list the host should serve.
546
+ */
547
+ type ModifyModelsHook = (models: Model<Api>[], credentials: OAuthCredentials) => Model<Api>[];
548
+
542
549
  function applyModelPatch(base: Model<Api>, patch: ModelPatch, transport: ModelTransportPolicy): Model<Api> {
543
550
  const result = { ...base };
544
551
  if (patch.name !== undefined) result.name = patch.name;
@@ -779,6 +786,7 @@ export type ResolvedRequestAuth =
779
786
  */
780
787
  export class ModelRegistry {
781
788
  #models: Model<Api>[] = [];
789
+ #unprojectedModels: Model<Api>[] = [];
782
790
  #hasFullSnapshot = false;
783
791
  #cachedStandardModels: Model<Api>[] = [];
784
792
  #cachedDiscoverableModels: Model<Api>[] = [];
@@ -805,6 +813,13 @@ export class ModelRegistry {
805
813
  #runtimeModelOverlays: CustomModelOverlay[] = [];
806
814
  #runtimeProviderApiKeys: Map<string, string> = new Map();
807
815
  #runtimeProviderOverrides: Map<string, ProviderOverride> = new Map();
816
+ // Credential-aware model projections registered via
817
+ // `registerProvider({ oauth: { modifyModels } })`. Persisted for the same
818
+ // reason as #runtimeModelOverlays: the overlays hold the *pre-projection*
819
+ // definitions, so without re-applying the projection every static reload
820
+ // would silently revert the provider to its unprojected catalog.
821
+ #runtimeModelModifiers: Map<string, ModifyModelsHook> = new Map();
822
+ #lastModelModifierWarnings: Map<string, string> = new Map();
808
823
  #runtimeProvidersBySource: Map<string, Set<string>> = new Map();
809
824
  #runtimeProviderSourceByName: Map<string, string> = new Map();
810
825
  // Runtime model managers registered by extensions via fetchDynamicModels.
@@ -1002,6 +1017,15 @@ export class ModelRegistry {
1002
1017
  if (patch.contextWindow === undefined && patch.maxTokens === undefined && patch.input === undefined) {
1003
1018
  return current;
1004
1019
  }
1020
+ const unprojected = resolveProviderModelReference(current.provider, current.id, this.#unprojectedModels);
1021
+ if (unprojected) {
1022
+ const patchedBase = applyModelPatch(unprojected, patch, "merge");
1023
+ this.#unprojectedModels = this.#unprojectedModels.map(candidate =>
1024
+ candidate.provider === unprojected.provider && candidate.id === unprojected.id ? patchedBase : candidate,
1025
+ );
1026
+ this.#models = this.#applyRuntimeModelModifiers(this.#unprojectedModels);
1027
+ return resolveProviderModelReference(current.provider, current.id, this.#models) ?? patchedBase;
1028
+ }
1005
1029
  const patched = applyModelPatch(current, patch, "merge");
1006
1030
  this.#models = this.#models.map(candidate =>
1007
1031
  candidate.provider === current.provider && candidate.id === current.id ? patched : candidate,
@@ -1102,6 +1126,7 @@ export class ModelRegistry {
1102
1126
 
1103
1127
  #resetStaticComposition(): void {
1104
1128
  this.#models = [];
1129
+ this.#unprojectedModels = [];
1105
1130
  this.#hasFullSnapshot = false;
1106
1131
  this.#internedStaticModels.clear();
1107
1132
  this.#providerLookupSnapshots.clear();
@@ -1126,7 +1151,46 @@ export class ModelRegistry {
1126
1151
  });
1127
1152
  }
1128
1153
 
1129
- #composeStaticModels(providerFilter?: ReadonlySet<string>): Model<Api>[] {
1154
+ /**
1155
+ * Re-apply the credential-aware projections registered by extension providers.
1156
+ *
1157
+ * Runtime overlays hold the pre-projection definitions, so the registry keeps
1158
+ * those definitions separate from `#models` and reruns the ordered hooks after
1159
+ * every catalog rebuild. Otherwise an offline refresh silently restores the
1160
+ * provider's placeholder catalog.
1161
+ *
1162
+ * A throwing hook falls back to the catalog produced by earlier hooks instead
1163
+ * of failing the whole composition; one bad extension must not empty the
1164
+ * registry. The failure is logged (deduped per provider) so it is not silent.
1165
+ * Each hook receives a deep clone because the public contract permits
1166
+ * mutation of both the array and its model records before returning.
1167
+ */
1168
+ #applyRuntimeModelModifiers(models: Model<Api>[]): Model<Api>[] {
1169
+ if (this.#runtimeModelModifiers.size === 0) return models;
1170
+ let projected = models;
1171
+ for (const [providerName, modifyModels] of this.#runtimeModelModifiers) {
1172
+ const credential = this.authStorage.getOAuthCredential(providerName);
1173
+ if (!credential) continue;
1174
+ try {
1175
+ projected = modifyModels(structuredClone(projected), credential);
1176
+ } catch (error) {
1177
+ this.#warnModelModifierFailure(providerName, error instanceof Error ? error.message : String(error));
1178
+ }
1179
+ }
1180
+ return projected;
1181
+ }
1182
+
1183
+ /**
1184
+ * Dedup key is separate from `#lastDiscoveryWarnings` so a repeated modifier
1185
+ * failure cannot mask a subsequent discovery failure for the same provider.
1186
+ */
1187
+ #warnModelModifierFailure(provider: string, error: string): void {
1188
+ if (this.#lastModelModifierWarnings.get(provider) === error) return;
1189
+ this.#lastModelModifierWarnings.set(provider, error);
1190
+ logger.warn("extension model projection failed; serving unprojected catalog", { provider, error });
1191
+ }
1192
+
1193
+ #composeUnprojectedStaticModels(providerFilter?: ReadonlySet<string>): Model<Api>[] {
1130
1194
  const select = <T extends { provider: string }>(models: readonly T[]): T[] =>
1131
1195
  providerFilter ? models.filter(model => providerFilter.has(model.provider)) : [...models];
1132
1196
  let builtInModels = this.#applyHardcodedModelPolicies(
@@ -1141,17 +1205,24 @@ export class ModelRegistry {
1141
1205
  );
1142
1206
  const withConfigModels = this.#mergeCustomModels(resolvedDefaults, select(this.#customModelOverlays));
1143
1207
  const combined = this.#mergeCustomModels(withConfigModels, select(this.#runtimeModelOverlays));
1144
- // Custom/config providers bypass the model-manager merge point —
1145
- // collapse effort-tier variants here so X/X-thinking twins fold.
1146
1208
  const withModelOverrides = this.#applyModelOverrides(collapseBuiltModelVariants(combined), this.#modelOverrides);
1147
- return this.#internStaticModels(
1148
- this.#applyLlamaCppQwenThinkingToModels(this.#applyRuntimeProviderOverrides(withModelOverrides)),
1149
- );
1209
+ return this.#applyLlamaCppQwenThinkingToModels(this.#applyRuntimeProviderOverrides(withModelOverrides));
1210
+ }
1211
+
1212
+ #composeStaticModels(providerFilter?: ReadonlySet<string>): Model<Api>[] {
1213
+ // A modifier is a whole-catalog transform. Build and project the full catalog
1214
+ // before narrowing a lazy lookup, matching getAll() followed by filtering.
1215
+ const projectFullCatalog = providerFilter !== undefined && this.#runtimeModelModifiers.size > 0;
1216
+ const unprojected = this.#composeUnprojectedStaticModels(projectFullCatalog ? undefined : providerFilter);
1217
+ const projected = this.#applyRuntimeModelModifiers(unprojected);
1218
+ const selected = projectFullCatalog ? projected.filter(model => providerFilter.has(model.provider)) : projected;
1219
+ return this.#internStaticModels(selected);
1150
1220
  }
1151
1221
 
1152
1222
  #ensureFullSnapshot(): Model<Api>[] {
1153
1223
  if (!this.#hasFullSnapshot) {
1154
- this.#models = this.#composeStaticModels();
1224
+ this.#unprojectedModels = this.#composeUnprojectedStaticModels();
1225
+ this.#models = this.#internStaticModels(this.#applyRuntimeModelModifiers(this.#unprojectedModels));
1155
1226
  this.#hasFullSnapshot = true;
1156
1227
  this.#providerLookupSnapshots.clear();
1157
1228
  }
@@ -1605,7 +1676,7 @@ export class ModelRegistry {
1605
1676
  discovered.map(model =>
1606
1677
  mergeDiscoveredModel(
1607
1678
  model,
1608
- this.find(model.provider, model.id),
1679
+ resolveProviderModelReference(model.provider, model.id, this.#unprojectedModels),
1609
1680
  this.#providerOverrides.get(model.provider),
1610
1681
  ),
1611
1682
  ),
@@ -1615,16 +1686,23 @@ export class ModelRegistry {
1615
1686
  authoritativeProviders.add(provider);
1616
1687
  }
1617
1688
  const baseModels =
1618
- authoritativeProviders.size > 0 ? dropProviderModels(this.#models, authoritativeProviders) : this.#models;
1689
+ authoritativeProviders.size > 0
1690
+ ? dropProviderModels(this.#unprojectedModels, authoritativeProviders)
1691
+ : this.#unprojectedModels;
1619
1692
  const resolved = this.#mergeResolvedModels(baseModels, discoveredModels);
1620
1693
  const withConfigModels = this.#mergeCustomModels(resolved, this.#customModelOverlays);
1621
- // Merge runtime extension models so they survive online discovery completion
1622
1694
  const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
1623
1695
  const withModelOverrides = this.#applyModelOverrides(collapseBuiltModelVariants(combined), this.#modelOverrides);
1624
- this.#models = this.#applyLlamaCppQwenThinkingToModels(this.#applyRuntimeProviderOverrides(withModelOverrides));
1696
+ this.#unprojectedModels = this.#applyLlamaCppQwenThinkingToModels(
1697
+ this.#applyRuntimeProviderOverrides(withModelOverrides),
1698
+ );
1699
+ this.#models = this.#applyRuntimeModelModifiers(this.#unprojectedModels);
1625
1700
  }
1626
1701
 
1627
1702
  #configuredDiscoveryCacheProviderId(providerConfig: DiscoveryProviderConfig): string {
1703
+ if (providerConfig.discovery.type === "ollama") {
1704
+ return resolveOllamaModelCacheProviderId(providerConfig.provider, providerConfig.baseUrl);
1705
+ }
1628
1706
  if (providerConfig.discovery.type === "openai-models-list") {
1629
1707
  return `${providerConfig.provider}:openai-models-list-context-v2`;
1630
1708
  }
@@ -2393,6 +2471,8 @@ export class ModelRegistry {
2393
2471
  this.#runtimeProviderOverrides.delete(providerName);
2394
2472
  this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
2395
2473
  this.#runtimeModelManagers.delete(providerName);
2474
+ this.#runtimeModelModifiers.delete(providerName);
2475
+ this.#lastModelModifierWarnings.delete(providerName);
2396
2476
  this.authStorage.removeConfigApiKey(providerName);
2397
2477
  }
2398
2478
 
@@ -2529,13 +2609,15 @@ export class ModelRegistry {
2529
2609
  this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(m => m.provider !== providerName);
2530
2610
  this.#runtimeModelOverlays.push(...newOverlays);
2531
2611
 
2532
- // Also update #models immediately for the current cycle
2533
- const nextModels = this.#models.filter(m => m.provider !== providerName);
2612
+ // Update the unprojected snapshot, then rerun every whole-catalog
2613
+ // projection exactly once. Incremental projection is not safe because one
2614
+ // provider's hook may inspect or suppress another provider's models.
2615
+ const nextModels = this.#unprojectedModels.filter(model => model.provider !== providerName);
2534
2616
  for (const overlay of newOverlays) {
2535
2617
  nextModels.push(finalizeCustomModel(overlay, { useDefaults: true }));
2536
2618
  }
2537
2619
  const runtimeTransportOverride = this.#runtimeProviderOverrides.get(providerName);
2538
- const withRuntimeTransportOverride = runtimeTransportOverride
2620
+ this.#unprojectedModels = runtimeTransportOverride
2539
2621
  ? nextModels.map(model => {
2540
2622
  if (model.provider !== providerName) return model;
2541
2623
  return this.#applyProviderTransportOverrideToModel(model, runtimeTransportOverride);
@@ -2543,14 +2625,12 @@ export class ModelRegistry {
2543
2625
  : nextModels;
2544
2626
 
2545
2627
  if (config.oauth?.modifyModels) {
2546
- const credential = this.authStorage.getOAuthCredential(providerName);
2547
- if (credential) {
2548
- this.#models = config.oauth.modifyModels(withRuntimeTransportOverride, credential);
2549
- return;
2550
- }
2628
+ this.#runtimeModelModifiers.set(providerName, config.oauth.modifyModels);
2629
+ } else {
2630
+ this.#runtimeModelModifiers.delete(providerName);
2551
2631
  }
2552
-
2553
- this.#models = withRuntimeTransportOverride;
2632
+ this.#models = this.#applyRuntimeModelModifiers(this.#unprojectedModels);
2633
+ this.#providerLookupSnapshots.clear();
2554
2634
  return;
2555
2635
  }
2556
2636
 
@@ -2619,12 +2699,14 @@ export class ModelRegistry {
2619
2699
  transportOverride,
2620
2700
  );
2621
2701
  this.#runtimeProviderOverrides.set(providerName, nextRuntimeOverride);
2622
- this.#models = this.#applyLlamaCppQwenThinkingToModels(
2623
- this.#models.map(m => {
2624
- if (m.provider !== providerName) return m;
2625
- return this.#applyProviderTransportOverrideToModel(m, transportOverride);
2702
+ this.#unprojectedModels = this.#applyLlamaCppQwenThinkingToModels(
2703
+ this.#unprojectedModels.map(model => {
2704
+ if (model.provider !== providerName) return model;
2705
+ return this.#applyProviderTransportOverrideToModel(model, transportOverride);
2626
2706
  }),
2627
2707
  );
2708
+ this.#models = this.#applyRuntimeModelModifiers(this.#unprojectedModels);
2709
+ this.#providerLookupSnapshots.clear();
2628
2710
  }
2629
2711
  }
2630
2712