@oh-my-pi/pi-coding-agent 17.1.7 → 17.2.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 (246) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/dist/{CHANGELOG-5k4dq4g6.md → CHANGELOG-be1f2t8h.md} +127 -0
  3. package/dist/cli.js +9554 -12379
  4. package/dist/template-8vdv6xb4.js +1671 -0
  5. package/dist/template-f8wx9vfn.css +1355 -0
  6. package/dist/template-qat058wr.html +55 -0
  7. package/dist/tool-views.generated-jdfmzwmn.js +35 -0
  8. package/dist/types/advisor/advise-tool.d.ts +0 -7
  9. package/dist/types/advisor/runtime.d.ts +8 -11
  10. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  11. package/dist/types/async/job-manager.d.ts +11 -0
  12. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  13. package/dist/types/cleanse/agent.d.ts +19 -0
  14. package/dist/types/cleanse/balance.d.ts +7 -0
  15. package/dist/types/cleanse/checkers.d.ts +13 -0
  16. package/dist/types/cleanse/index.d.ts +16 -0
  17. package/dist/types/cleanse/loop.d.ts +16 -0
  18. package/dist/types/cleanse/parsers.d.ts +13 -0
  19. package/dist/types/cleanse/progress.d.ts +14 -0
  20. package/dist/types/cleanse/types.d.ts +64 -0
  21. package/dist/types/cli/args.d.ts +2 -0
  22. package/dist/types/cli/session-picker.d.ts +14 -7
  23. package/dist/types/collab/guest.d.ts +21 -2
  24. package/dist/types/commands/cleanse.d.ts +23 -0
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/config/settings-schema.d.ts +63 -8
  27. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  28. package/dist/types/cursor.d.ts +193 -8
  29. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  30. package/dist/types/edit/hashline/diff.d.ts +9 -1
  31. package/dist/types/edit/index.d.ts +8 -1
  32. package/dist/types/edit/streaming.d.ts +8 -1
  33. package/dist/types/export/html/index.d.ts +2 -0
  34. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  35. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
  38. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  39. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  40. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  41. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  42. package/dist/types/lsp/client.d.ts +2 -0
  43. package/dist/types/main.d.ts +2 -0
  44. package/dist/types/mcp/manager.d.ts +33 -4
  45. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  46. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  47. package/dist/types/modes/components/session-selector.d.ts +6 -0
  48. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  49. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  50. package/dist/types/modes/components/usage-row.d.ts +2 -0
  51. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  52. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  53. package/dist/types/modes/interactive-mode.d.ts +8 -5
  54. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  55. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  56. package/dist/types/modes/theme/theme.d.ts +12 -1
  57. package/dist/types/modes/types.d.ts +2 -1
  58. package/dist/types/sdk.d.ts +1 -0
  59. package/dist/types/session/agent-session-events.d.ts +2 -0
  60. package/dist/types/session/agent-session-types.d.ts +36 -1
  61. package/dist/types/session/agent-session.d.ts +4 -2
  62. package/dist/types/session/async-job-delivery.d.ts +8 -0
  63. package/dist/types/session/claude-session-store.d.ts +13 -0
  64. package/dist/types/session/codex-session-store.d.ts +14 -0
  65. package/dist/types/session/credential-pin.d.ts +58 -0
  66. package/dist/types/session/foreign-session-import.d.ts +15 -0
  67. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  68. package/dist/types/session/foreign-session-store.d.ts +23 -0
  69. package/dist/types/session/model-controls.d.ts +3 -3
  70. package/dist/types/session/session-advisors.d.ts +47 -2
  71. package/dist/types/session/session-entries.d.ts +19 -1
  72. package/dist/types/session/session-handoff.d.ts +4 -1
  73. package/dist/types/session/session-loader.d.ts +2 -0
  74. package/dist/types/session/session-maintenance.d.ts +1 -1
  75. package/dist/types/session/session-manager.d.ts +21 -0
  76. package/dist/types/session/session-tools.d.ts +25 -1
  77. package/dist/types/session/turn-recovery.d.ts +10 -4
  78. package/dist/types/system-prompt.d.ts +2 -1
  79. package/dist/types/task/executor.d.ts +11 -2
  80. package/dist/types/task/isolation-ownership.d.ts +34 -0
  81. package/dist/types/task/render.d.ts +7 -0
  82. package/dist/types/thinking.d.ts +18 -6
  83. package/dist/types/tools/acp-bridge.d.ts +28 -5
  84. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
  85. package/dist/types/tools/browser/tab-worker.d.ts +1 -4
  86. package/dist/types/tools/grep.d.ts +17 -1
  87. package/dist/types/tools/hub/launch.d.ts +5 -1
  88. package/dist/types/tools/index.d.ts +4 -1
  89. package/dist/types/tools/path-utils.d.ts +19 -0
  90. package/dist/types/tools/read.d.ts +2 -0
  91. package/dist/types/tools/xdev.d.ts +2 -1
  92. package/dist/types/tui/output-block.d.ts +5 -5
  93. package/dist/types/utils/changelog.d.ts +19 -0
  94. package/dist/types/utils/git.d.ts +10 -0
  95. package/dist/types/utils/jj.d.ts +11 -4
  96. package/dist/types/web/search/types.d.ts +2 -2
  97. package/package.json +16 -12
  98. package/scripts/bundle-dist.ts +3 -1
  99. package/src/advisor/advise-tool.ts +0 -11
  100. package/src/advisor/runtime.ts +138 -24
  101. package/src/advisor/transcript-recorder.ts +56 -0
  102. package/src/async/job-manager.ts +30 -7
  103. package/src/auto-thinking/classifier.ts +48 -10
  104. package/src/cleanse/agent.ts +226 -0
  105. package/src/cleanse/balance.ts +79 -0
  106. package/src/cleanse/checkers.ts +996 -0
  107. package/src/cleanse/index.ts +190 -0
  108. package/src/cleanse/loop.ts +51 -0
  109. package/src/cleanse/parsers.ts +726 -0
  110. package/src/cleanse/progress.ts +50 -0
  111. package/src/cleanse/prompts/assignment.md +47 -0
  112. package/src/cleanse/types.ts +72 -0
  113. package/src/cli/args.ts +6 -0
  114. package/src/cli/flag-tables.ts +2 -0
  115. package/src/cli/gallery-fixtures/fs.ts +25 -0
  116. package/src/cli/session-picker.ts +34 -17
  117. package/src/cli/ttsr-cli.ts +19 -1
  118. package/src/cli/update-cli.ts +3 -0
  119. package/src/cli/usage-cli.ts +29 -4
  120. package/src/cli/worktree-cli.ts +28 -11
  121. package/src/cli-commands.ts +1 -0
  122. package/src/cli.ts +11 -1
  123. package/src/collab/guest.ts +37 -4
  124. package/src/collab/host.ts +1 -0
  125. package/src/commands/cleanse.ts +45 -0
  126. package/src/commands/launch.ts +6 -0
  127. package/src/config/model-registry.ts +103 -25
  128. package/src/config/settings-schema.ts +65 -6
  129. package/src/config/settings.ts +25 -0
  130. package/src/cursor-bridge-tools.ts +81 -0
  131. package/src/cursor.ts +463 -12
  132. package/src/discovery/claude-plugins.ts +144 -34
  133. package/src/edit/edit-clipboard.ts +23 -0
  134. package/src/edit/hashline/diff.ts +49 -10
  135. package/src/edit/hashline/execute.ts +38 -6
  136. package/src/edit/hashline/filesystem.ts +27 -3
  137. package/src/edit/index.ts +12 -2
  138. package/src/edit/renderer.ts +4 -4
  139. package/src/edit/streaming.ts +17 -2
  140. package/src/export/html/index.ts +17 -10
  141. package/src/export/html/template.js +1 -1
  142. package/src/extensibility/extensions/runner.ts +70 -0
  143. package/src/extensibility/extensions/types.ts +30 -0
  144. package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
  145. package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
  146. package/src/internal-urls/local-protocol.ts +3 -2
  147. package/src/internal-urls/mcp-protocol.ts +4 -1
  148. package/src/launch/broker.ts +14 -4
  149. package/src/launch/terminal-output-worker-client.ts +53 -0
  150. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  151. package/src/launch/terminal-output-worker.ts +23 -0
  152. package/src/lsp/client.ts +18 -3
  153. package/src/lsp/index.ts +4 -0
  154. package/src/lsp/render.ts +1 -1
  155. package/src/main.ts +127 -56
  156. package/src/mcp/manager.ts +120 -18
  157. package/src/modes/acp/acp-agent.ts +27 -18
  158. package/src/modes/acp/acp-event-mapper.ts +38 -4
  159. package/src/modes/components/chat-transcript-builder.ts +30 -11
  160. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  161. package/src/modes/components/read-tool-group.ts +159 -17
  162. package/src/modes/components/session-selector.ts +19 -3
  163. package/src/modes/components/settings-selector.ts +9 -1
  164. package/src/modes/components/status-line/component.ts +381 -54
  165. package/src/modes/components/tool-execution.ts +78 -31
  166. package/src/modes/components/usage-row.ts +10 -5
  167. package/src/modes/controllers/event-controller.ts +26 -4
  168. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  169. package/src/modes/controllers/selector-controller.ts +94 -36
  170. package/src/modes/controllers/tan-command-controller.ts +14 -0
  171. package/src/modes/interactive-mode.ts +67 -64
  172. package/src/modes/prompt-action-autocomplete.ts +5 -3
  173. package/src/modes/rpc/rpc-client.ts +18 -0
  174. package/src/modes/rpc/rpc-mode.ts +16 -1
  175. package/src/modes/rpc/rpc-types.ts +11 -0
  176. package/src/modes/theme/theme.ts +151 -6
  177. package/src/modes/types.ts +2 -1
  178. package/src/modes/utils/ui-helpers.ts +38 -21
  179. package/src/prompts/goals/guided-goal-interview.md +41 -6
  180. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  181. package/src/prompts/system/plan-mode-active.md +2 -2
  182. package/src/prompts/system/system-prompt.md +3 -0
  183. package/src/prompts/system/vibe-mode-active.md +4 -1
  184. package/src/prompts/system/xdev-mount-notice.md +1 -1
  185. package/src/prompts/tools/bash.md +15 -17
  186. package/src/prompts/tools/browser.md +1 -0
  187. package/src/prompts/tools/checkpoint.md +1 -1
  188. package/src/prompts/tools/glob.md +7 -6
  189. package/src/prompts/tools/goal.md +1 -1
  190. package/src/prompts/tools/grep.md +7 -6
  191. package/src/sdk.ts +178 -15
  192. package/src/session/agent-session-events.ts +1 -0
  193. package/src/session/agent-session-types.ts +40 -1
  194. package/src/session/agent-session.ts +330 -179
  195. package/src/session/async-job-delivery.ts +8 -0
  196. package/src/session/claude-session-store.ts +426 -0
  197. package/src/session/codex-session-store.ts +673 -0
  198. package/src/session/credential-pin.ts +93 -0
  199. package/src/session/foreign-session-import.ts +52 -0
  200. package/src/session/foreign-session-jsonl.ts +29 -0
  201. package/src/session/foreign-session-store.ts +26 -0
  202. package/src/session/model-controls.ts +15 -7
  203. package/src/session/session-advisors.ts +142 -23
  204. package/src/session/session-entries.ts +21 -1
  205. package/src/session/session-handoff.ts +20 -3
  206. package/src/session/session-history-format.ts +31 -6
  207. package/src/session/session-loader.ts +25 -9
  208. package/src/session/session-maintenance.ts +59 -11
  209. package/src/session/session-manager.ts +61 -0
  210. package/src/session/session-tools.ts +107 -5
  211. package/src/session/settings-stream-fn.ts +1 -0
  212. package/src/session/turn-recovery.ts +97 -51
  213. package/src/slash-commands/builtin-registry.ts +136 -5
  214. package/src/system-prompt.ts +3 -2
  215. package/src/task/executor.ts +17 -4
  216. package/src/task/isolation-ownership.ts +106 -0
  217. package/src/task/render.ts +20 -4
  218. package/src/task/worktree.ts +8 -0
  219. package/src/thinking.ts +29 -10
  220. package/src/tools/acp-bridge.ts +52 -8
  221. package/src/tools/ask.ts +3 -3
  222. package/src/tools/ast-edit.ts +9 -2
  223. package/src/tools/browser/cmux/cmux-tab.ts +9 -14
  224. package/src/tools/browser/tab-worker.ts +12 -35
  225. package/src/tools/browser.ts +7 -2
  226. package/src/tools/checkpoint.ts +0 -13
  227. package/src/tools/gh-renderer.ts +3 -3
  228. package/src/tools/grep.ts +64 -8
  229. package/src/tools/hub/launch.ts +10 -15
  230. package/src/tools/index.ts +38 -5
  231. package/src/tools/path-utils.ts +88 -0
  232. package/src/tools/read.ts +3 -0
  233. package/src/tools/write.ts +44 -3
  234. package/src/tools/xdev.ts +39 -14
  235. package/src/tui/code-cell.ts +4 -4
  236. package/src/tui/output-block.ts +25 -8
  237. package/src/utils/changelog.ts +144 -4
  238. package/src/utils/git.ts +61 -14
  239. package/src/utils/jj.ts +19 -13
  240. package/src/utils/shell-snapshot-fn-env.sh +5 -2
  241. package/src/web/search/providers/codex.ts +34 -0
  242. package/src/web/search/render.ts +2 -2
  243. package/src/web/search/types.ts +1 -1
  244. package/dist/types/goals/guided-setup.d.ts +0 -30
  245. package/src/goals/guided-setup.ts +0 -171
  246. package/src/prompts/goals/guided-goal-system.md +0 -33
@@ -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
 
@@ -0,0 +1,45 @@
1
+ import { postmortem } from "@oh-my-pi/pi-utils";
2
+ import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
3
+ import { runCleanseCommand } from "../cleanse";
4
+ import { CliUsageError } from "../cli/usage-error";
5
+
6
+ export default class Cleanse extends Command {
7
+ static description = "Detect and fix project diagnostics with weighted parallel subagents";
8
+
9
+ static flags = {
10
+ agents: Flags.integer({
11
+ char: "n",
12
+ description: "Maximum number of file-disjoint subagents",
13
+ default: 8,
14
+ }),
15
+ model: Flags.string({
16
+ char: "m",
17
+ description: "Subagent model selector",
18
+ default: "@smol",
19
+ }),
20
+ tests: Flags.boolean({
21
+ char: "t",
22
+ description: "Also run configured project test suites",
23
+ default: false,
24
+ }),
25
+ };
26
+
27
+ static examples = [
28
+ "omp cleanse",
29
+ "omp cleanse -n 4",
30
+ "omp cleanse -m opus",
31
+ "omp cleanse -t",
32
+ "omp cleanse --agents 12 --model anthropic/claude-opus-4-6",
33
+ ];
34
+
35
+ async run(): Promise<void> {
36
+ const { flags } = await this.parse(Cleanse);
37
+ if (flags.agents <= 0) throw new CliUsageError("--agents must be a positive integer");
38
+ const result = await runCleanseCommand({
39
+ maxAgents: flags.agents,
40
+ model: flags.model,
41
+ includeTests: flags.tests,
42
+ });
43
+ await postmortem.quit(result.exitCode);
44
+ }
45
+ }
@@ -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
  }),
@@ -539,6 +539,12 @@ interface ModelPatch {
539
539
  */
540
540
  type ModelTransportPolicy = "merge" | "replace";
541
541
 
542
+ /**
543
+ * Credential-aware model projection supplied by an extension provider. Receives
544
+ * the fully composed catalog and returns the list the host should serve.
545
+ */
546
+ type ModifyModelsHook = (models: Model<Api>[], credentials: OAuthCredentials) => Model<Api>[];
547
+
542
548
  function applyModelPatch(base: Model<Api>, patch: ModelPatch, transport: ModelTransportPolicy): Model<Api> {
543
549
  const result = { ...base };
544
550
  if (patch.name !== undefined) result.name = patch.name;
@@ -779,6 +785,7 @@ export type ResolvedRequestAuth =
779
785
  */
780
786
  export class ModelRegistry {
781
787
  #models: Model<Api>[] = [];
788
+ #unprojectedModels: Model<Api>[] = [];
782
789
  #hasFullSnapshot = false;
783
790
  #cachedStandardModels: Model<Api>[] = [];
784
791
  #cachedDiscoverableModels: Model<Api>[] = [];
@@ -805,6 +812,13 @@ export class ModelRegistry {
805
812
  #runtimeModelOverlays: CustomModelOverlay[] = [];
806
813
  #runtimeProviderApiKeys: Map<string, string> = new Map();
807
814
  #runtimeProviderOverrides: Map<string, ProviderOverride> = new Map();
815
+ // Credential-aware model projections registered via
816
+ // `registerProvider({ oauth: { modifyModels } })`. Persisted for the same
817
+ // reason as #runtimeModelOverlays: the overlays hold the *pre-projection*
818
+ // definitions, so without re-applying the projection every static reload
819
+ // would silently revert the provider to its unprojected catalog.
820
+ #runtimeModelModifiers: Map<string, ModifyModelsHook> = new Map();
821
+ #lastModelModifierWarnings: Map<string, string> = new Map();
808
822
  #runtimeProvidersBySource: Map<string, Set<string>> = new Map();
809
823
  #runtimeProviderSourceByName: Map<string, string> = new Map();
810
824
  // Runtime model managers registered by extensions via fetchDynamicModels.
@@ -1002,6 +1016,15 @@ export class ModelRegistry {
1002
1016
  if (patch.contextWindow === undefined && patch.maxTokens === undefined && patch.input === undefined) {
1003
1017
  return current;
1004
1018
  }
1019
+ const unprojected = resolveProviderModelReference(current.provider, current.id, this.#unprojectedModels);
1020
+ if (unprojected) {
1021
+ const patchedBase = applyModelPatch(unprojected, patch, "merge");
1022
+ this.#unprojectedModels = this.#unprojectedModels.map(candidate =>
1023
+ candidate.provider === unprojected.provider && candidate.id === unprojected.id ? patchedBase : candidate,
1024
+ );
1025
+ this.#models = this.#applyRuntimeModelModifiers(this.#unprojectedModels);
1026
+ return resolveProviderModelReference(current.provider, current.id, this.#models) ?? patchedBase;
1027
+ }
1005
1028
  const patched = applyModelPatch(current, patch, "merge");
1006
1029
  this.#models = this.#models.map(candidate =>
1007
1030
  candidate.provider === current.provider && candidate.id === current.id ? patched : candidate,
@@ -1102,6 +1125,7 @@ export class ModelRegistry {
1102
1125
 
1103
1126
  #resetStaticComposition(): void {
1104
1127
  this.#models = [];
1128
+ this.#unprojectedModels = [];
1105
1129
  this.#hasFullSnapshot = false;
1106
1130
  this.#internedStaticModels.clear();
1107
1131
  this.#providerLookupSnapshots.clear();
@@ -1126,7 +1150,46 @@ export class ModelRegistry {
1126
1150
  });
1127
1151
  }
1128
1152
 
1129
- #composeStaticModels(providerFilter?: ReadonlySet<string>): Model<Api>[] {
1153
+ /**
1154
+ * Re-apply the credential-aware projections registered by extension providers.
1155
+ *
1156
+ * Runtime overlays hold the pre-projection definitions, so the registry keeps
1157
+ * those definitions separate from `#models` and reruns the ordered hooks after
1158
+ * every catalog rebuild. Otherwise an offline refresh silently restores the
1159
+ * provider's placeholder catalog.
1160
+ *
1161
+ * A throwing hook falls back to the catalog produced by earlier hooks instead
1162
+ * of failing the whole composition; one bad extension must not empty the
1163
+ * registry. The failure is logged (deduped per provider) so it is not silent.
1164
+ * Each hook receives a deep clone because the public contract permits
1165
+ * mutation of both the array and its model records before returning.
1166
+ */
1167
+ #applyRuntimeModelModifiers(models: Model<Api>[]): Model<Api>[] {
1168
+ if (this.#runtimeModelModifiers.size === 0) return models;
1169
+ let projected = models;
1170
+ for (const [providerName, modifyModels] of this.#runtimeModelModifiers) {
1171
+ const credential = this.authStorage.getOAuthCredential(providerName);
1172
+ if (!credential) continue;
1173
+ try {
1174
+ projected = modifyModels(structuredClone(projected), credential);
1175
+ } catch (error) {
1176
+ this.#warnModelModifierFailure(providerName, error instanceof Error ? error.message : String(error));
1177
+ }
1178
+ }
1179
+ return projected;
1180
+ }
1181
+
1182
+ /**
1183
+ * Dedup key is separate from `#lastDiscoveryWarnings` so a repeated modifier
1184
+ * failure cannot mask a subsequent discovery failure for the same provider.
1185
+ */
1186
+ #warnModelModifierFailure(provider: string, error: string): void {
1187
+ if (this.#lastModelModifierWarnings.get(provider) === error) return;
1188
+ this.#lastModelModifierWarnings.set(provider, error);
1189
+ logger.warn("extension model projection failed; serving unprojected catalog", { provider, error });
1190
+ }
1191
+
1192
+ #composeUnprojectedStaticModels(providerFilter?: ReadonlySet<string>): Model<Api>[] {
1130
1193
  const select = <T extends { provider: string }>(models: readonly T[]): T[] =>
1131
1194
  providerFilter ? models.filter(model => providerFilter.has(model.provider)) : [...models];
1132
1195
  let builtInModels = this.#applyHardcodedModelPolicies(
@@ -1141,17 +1204,24 @@ export class ModelRegistry {
1141
1204
  );
1142
1205
  const withConfigModels = this.#mergeCustomModels(resolvedDefaults, select(this.#customModelOverlays));
1143
1206
  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
1207
  const withModelOverrides = this.#applyModelOverrides(collapseBuiltModelVariants(combined), this.#modelOverrides);
1147
- return this.#internStaticModels(
1148
- this.#applyLlamaCppQwenThinkingToModels(this.#applyRuntimeProviderOverrides(withModelOverrides)),
1149
- );
1208
+ return this.#applyLlamaCppQwenThinkingToModels(this.#applyRuntimeProviderOverrides(withModelOverrides));
1209
+ }
1210
+
1211
+ #composeStaticModels(providerFilter?: ReadonlySet<string>): Model<Api>[] {
1212
+ // A modifier is a whole-catalog transform. Build and project the full catalog
1213
+ // before narrowing a lazy lookup, matching getAll() followed by filtering.
1214
+ const projectFullCatalog = providerFilter !== undefined && this.#runtimeModelModifiers.size > 0;
1215
+ const unprojected = this.#composeUnprojectedStaticModels(projectFullCatalog ? undefined : providerFilter);
1216
+ const projected = this.#applyRuntimeModelModifiers(unprojected);
1217
+ const selected = projectFullCatalog ? projected.filter(model => providerFilter.has(model.provider)) : projected;
1218
+ return this.#internStaticModels(selected);
1150
1219
  }
1151
1220
 
1152
1221
  #ensureFullSnapshot(): Model<Api>[] {
1153
1222
  if (!this.#hasFullSnapshot) {
1154
- this.#models = this.#composeStaticModels();
1223
+ this.#unprojectedModels = this.#composeUnprojectedStaticModels();
1224
+ this.#models = this.#internStaticModels(this.#applyRuntimeModelModifiers(this.#unprojectedModels));
1155
1225
  this.#hasFullSnapshot = true;
1156
1226
  this.#providerLookupSnapshots.clear();
1157
1227
  }
@@ -1605,7 +1675,7 @@ export class ModelRegistry {
1605
1675
  discovered.map(model =>
1606
1676
  mergeDiscoveredModel(
1607
1677
  model,
1608
- this.find(model.provider, model.id),
1678
+ resolveProviderModelReference(model.provider, model.id, this.#unprojectedModels),
1609
1679
  this.#providerOverrides.get(model.provider),
1610
1680
  ),
1611
1681
  ),
@@ -1615,13 +1685,17 @@ export class ModelRegistry {
1615
1685
  authoritativeProviders.add(provider);
1616
1686
  }
1617
1687
  const baseModels =
1618
- authoritativeProviders.size > 0 ? dropProviderModels(this.#models, authoritativeProviders) : this.#models;
1688
+ authoritativeProviders.size > 0
1689
+ ? dropProviderModels(this.#unprojectedModels, authoritativeProviders)
1690
+ : this.#unprojectedModels;
1619
1691
  const resolved = this.#mergeResolvedModels(baseModels, discoveredModels);
1620
1692
  const withConfigModels = this.#mergeCustomModels(resolved, this.#customModelOverlays);
1621
- // Merge runtime extension models so they survive online discovery completion
1622
1693
  const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
1623
1694
  const withModelOverrides = this.#applyModelOverrides(collapseBuiltModelVariants(combined), this.#modelOverrides);
1624
- this.#models = this.#applyLlamaCppQwenThinkingToModels(this.#applyRuntimeProviderOverrides(withModelOverrides));
1695
+ this.#unprojectedModels = this.#applyLlamaCppQwenThinkingToModels(
1696
+ this.#applyRuntimeProviderOverrides(withModelOverrides),
1697
+ );
1698
+ this.#models = this.#applyRuntimeModelModifiers(this.#unprojectedModels);
1625
1699
  }
1626
1700
 
1627
1701
  #configuredDiscoveryCacheProviderId(providerConfig: DiscoveryProviderConfig): string {
@@ -2393,6 +2467,8 @@ export class ModelRegistry {
2393
2467
  this.#runtimeProviderOverrides.delete(providerName);
2394
2468
  this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(overlay => overlay.provider !== providerName);
2395
2469
  this.#runtimeModelManagers.delete(providerName);
2470
+ this.#runtimeModelModifiers.delete(providerName);
2471
+ this.#lastModelModifierWarnings.delete(providerName);
2396
2472
  this.authStorage.removeConfigApiKey(providerName);
2397
2473
  }
2398
2474
 
@@ -2529,13 +2605,15 @@ export class ModelRegistry {
2529
2605
  this.#runtimeModelOverlays = this.#runtimeModelOverlays.filter(m => m.provider !== providerName);
2530
2606
  this.#runtimeModelOverlays.push(...newOverlays);
2531
2607
 
2532
- // Also update #models immediately for the current cycle
2533
- const nextModels = this.#models.filter(m => m.provider !== providerName);
2608
+ // Update the unprojected snapshot, then rerun every whole-catalog
2609
+ // projection exactly once. Incremental projection is not safe because one
2610
+ // provider's hook may inspect or suppress another provider's models.
2611
+ const nextModels = this.#unprojectedModels.filter(model => model.provider !== providerName);
2534
2612
  for (const overlay of newOverlays) {
2535
2613
  nextModels.push(finalizeCustomModel(overlay, { useDefaults: true }));
2536
2614
  }
2537
2615
  const runtimeTransportOverride = this.#runtimeProviderOverrides.get(providerName);
2538
- const withRuntimeTransportOverride = runtimeTransportOverride
2616
+ this.#unprojectedModels = runtimeTransportOverride
2539
2617
  ? nextModels.map(model => {
2540
2618
  if (model.provider !== providerName) return model;
2541
2619
  return this.#applyProviderTransportOverrideToModel(model, runtimeTransportOverride);
@@ -2543,14 +2621,12 @@ export class ModelRegistry {
2543
2621
  : nextModels;
2544
2622
 
2545
2623
  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
- }
2624
+ this.#runtimeModelModifiers.set(providerName, config.oauth.modifyModels);
2625
+ } else {
2626
+ this.#runtimeModelModifiers.delete(providerName);
2551
2627
  }
2552
-
2553
- this.#models = withRuntimeTransportOverride;
2628
+ this.#models = this.#applyRuntimeModelModifiers(this.#unprojectedModels);
2629
+ this.#providerLookupSnapshots.clear();
2554
2630
  return;
2555
2631
  }
2556
2632
 
@@ -2619,12 +2695,14 @@ export class ModelRegistry {
2619
2695
  transportOverride,
2620
2696
  );
2621
2697
  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);
2698
+ this.#unprojectedModels = this.#applyLlamaCppQwenThinkingToModels(
2699
+ this.#unprojectedModels.map(model => {
2700
+ if (model.provider !== providerName) return model;
2701
+ return this.#applyProviderTransportOverrideToModel(model, transportOverride);
2626
2702
  }),
2627
2703
  );
2704
+ this.#models = this.#applyRuntimeModelModifiers(this.#unprojectedModels);
2705
+ this.#providerLookupSnapshots.clear();
2628
2706
  }
2629
2707
  }
2630
2708
 
@@ -927,6 +927,18 @@ export const SETTINGS_SCHEMA = {
927
927
  },
928
928
  },
929
929
 
930
+ "tui.codexResetFireworks": {
931
+ type: "boolean",
932
+ default: false,
933
+ ui: {
934
+ tab: "appearance",
935
+ group: "Display",
936
+ label: "Codex Reset Fireworks",
937
+ description:
938
+ "Celebrate unscheduled Codex weekly usage resets and newly banked saved resets with a top-third fireworks overlay that remains until Escape",
939
+ },
940
+ },
941
+
930
942
  "tui.titleState": {
931
943
  type: "boolean",
932
944
  default: true,
@@ -1811,14 +1823,32 @@ export const SETTINGS_SCHEMA = {
1811
1823
  },
1812
1824
  },
1813
1825
 
1814
- collapseChangelog: {
1815
- type: "boolean",
1816
- default: false,
1826
+ "startup.changelogMode": {
1827
+ type: "enum",
1828
+ values: ["summary", "expanded", "hidden"] as const,
1829
+ default: "summary",
1817
1830
  ui: {
1818
1831
  tab: "interaction",
1819
1832
  group: "Startup & Updates",
1820
- label: "Collapse Changelog",
1821
- description: "Show condensed changelog after updates",
1833
+ label: "Startup Changelog",
1834
+ description: "Choose whether update notes start as a summary, full details, or stay hidden",
1835
+ options: [
1836
+ {
1837
+ value: "summary",
1838
+ label: "Summary",
1839
+ description: "Show release and change counts with a /changelog hint",
1840
+ },
1841
+ {
1842
+ value: "expanded",
1843
+ label: "Expanded",
1844
+ description: "Show the recent release notes in full",
1845
+ },
1846
+ {
1847
+ value: "hidden",
1848
+ label: "Hidden",
1849
+ description: "Do not show release notes on startup",
1850
+ },
1851
+ ],
1822
1852
  },
1823
1853
  },
1824
1854
 
@@ -4054,6 +4084,18 @@ export const SETTINGS_SCHEMA = {
4054
4084
  },
4055
4085
  },
4056
4086
 
4087
+ "browser.cdpUrl": {
4088
+ type: "string",
4089
+ default: undefined,
4090
+ ui: {
4091
+ tab: "tools",
4092
+ group: "Grep & Browser",
4093
+ label: "Browser CDP URL",
4094
+ description:
4095
+ "Default HTTP CDP discovery endpoint (for example http://127.0.0.1:9222) to attach to instead of launching a browser. Explicit app.cdp_url or app.path on the tool call take precedence.",
4096
+ },
4097
+ },
4098
+
4057
4099
  "browser.headless": {
4058
4100
  type: "boolean",
4059
4101
  default: true,
@@ -4592,7 +4634,7 @@ export const SETTINGS_SCHEMA = {
4592
4634
  group: "Subagents",
4593
4635
  label: "Soft Subagent Request Budget",
4594
4636
  description:
4595
- "Soft per-subagent request budget (assistant requests per run). Crossing it injects a wrap-up steering notice (see task.softRequestBudgetNotice); at 1.5x the budget the run is force-stopped and the agent must yield its partial findings. 0 disables the guard. Bundled scout/sonic agents use a lower built-in budget.",
4637
+ "Soft per-subagent request budget (assistant requests per run). Crossing it injects a wrap-up steering notice (see task.softRequestBudgetNotice); at 1.5x the budget the run is force-stopped and the agent must yield its partial findings. 0 disables the guard. Bundled scout/sonic agents cap out at a lower built-in budget, so a value below that cap still applies to them.",
4596
4638
  options: [
4597
4639
  { value: "0", label: "Disabled" },
4598
4640
  { value: "90", label: "90 requests" },
@@ -5064,6 +5106,23 @@ export const SETTINGS_SCHEMA = {
5064
5106
  options: AUTO_THINKING_MODEL_OPTIONS,
5065
5107
  },
5066
5108
  },
5109
+ "providers.autoThinkingMaxEffort": {
5110
+ type: "enum",
5111
+ values: ["xhigh", "max"] as const,
5112
+ default: "xhigh",
5113
+ ui: {
5114
+ tab: "model",
5115
+ group: "Thinking",
5116
+ label: "Auto Thinking Ceiling",
5117
+ description:
5118
+ "Highest effort the `auto` classifier may resolve. `xhigh` keeps the classifier one tier below the top, so only an explicit `ultrathink` reaches `max`; `max` lets a turn the classifier judges exceptional bill the top tier on models that expose it.",
5119
+ condition: "autoThinkingActive",
5120
+ options: [
5121
+ { value: "xhigh", label: "xhigh", description: "Classifier stops at xhigh (default)" },
5122
+ { value: "max", label: "max", description: "Classifier may resolve max where the model supports it" },
5123
+ ],
5124
+ },
5125
+ },
5067
5126
  "features.unexpectedStopDetection": {
5068
5127
  type: "boolean",
5069
5128
  default: false,
@@ -1346,6 +1346,31 @@ export class Settings {
1346
1346
  }
1347
1347
  delete raw.lastChangelogVersion;
1348
1348
 
1349
+ // collapseChangelog (boolean) -> startup.changelogMode (enum). Preserve
1350
+ // every explicit legacy choice while giving new installs the schema's
1351
+ // "summary" default: true -> summary, false -> expanded. A separately
1352
+ // configured new mode always wins.
1353
+ const startupObj = isRecord(raw.startup) ? (raw.startup as Record<string, unknown>) : undefined;
1354
+ const legacyCollapseChangelog = typeof raw.collapseChangelog === "boolean" ? raw.collapseChangelog : undefined;
1355
+ const flatChangelogMode = raw["startup.changelogMode"];
1356
+ const normalizedFlatChangelogMode =
1357
+ flatChangelogMode === "summary" || flatChangelogMode === "expanded" || flatChangelogMode === "hidden"
1358
+ ? flatChangelogMode
1359
+ : undefined;
1360
+ if (legacyCollapseChangelog !== undefined || normalizedFlatChangelogMode !== undefined) {
1361
+ if (!startupObj) {
1362
+ raw.startup = {};
1363
+ }
1364
+ const target = raw.startup as Record<string, unknown>;
1365
+ if (target.changelogMode === undefined) {
1366
+ target.changelogMode =
1367
+ normalizedFlatChangelogMode ??
1368
+ (legacyCollapseChangelog !== undefined ? (legacyCollapseChangelog ? "summary" : "expanded") : undefined);
1369
+ }
1370
+ }
1371
+ delete raw.collapseChangelog;
1372
+ delete raw["startup.changelogMode"];
1373
+
1349
1374
  // ask.timeout: ms -> seconds (if value > 1000, it's old ms format)
1350
1375
  if (raw.ask && typeof (raw.ask as Record<string, unknown>).timeout === "number") {
1351
1376
  const oldValue = (raw.ask as Record<string, unknown>).timeout as number;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Per-call tools the Cursor exec bridge needs but the model-facing registry
3
+ * cannot supply.
4
+ *
5
+ * Both bridge callsites — the primary session and the advisor roster — build
6
+ * the same instances, and both must apply the session's approval wrapper. A
7
+ * raw tool here silently escapes the gate every registry call goes through, so
8
+ * the construction lives in one place rather than being repeated per callsite.
9
+ */
10
+
11
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
12
+ import { EditTool } from "./edit";
13
+ import type { ExtensionRunner } from "./extensibility/extensions";
14
+ import { ExtensionToolWrapper } from "./extensibility/extensions";
15
+ import type { GrepToolOptions, Tool, ToolSession } from "./tools";
16
+ import { GrepTool } from "./tools";
17
+
18
+ /**
19
+ * Build the bridge's `createGrepTool` factory for one tool session.
20
+ *
21
+ * A `pi_grep` frame carries its own context width and total match cap. Neither
22
+ * is expressible in the model-facing `grep` schema — context comes from
23
+ * `grep.contextBefore`/`grep.contextAfter`, fixed when the shared instance is
24
+ * constructed — so honoring them needs a fresh tool per call.
25
+ *
26
+ * The result is wrapped exactly like a registry tool: the approval gate runs on
27
+ * every call site, and a per-call instance is no exception.
28
+ */
29
+ export function createBridgeGrepFactory(
30
+ session: ToolSession,
31
+ extensionRunner: ExtensionRunner,
32
+ ): (options: GrepToolOptions) => AgentTool {
33
+ return options => {
34
+ const grepTool: Tool = new GrepTool(session, options);
35
+ return new ExtensionToolWrapper(grepTool, extensionRunner);
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Build the `replace`-mode `edit` the bridge answers `pi_edit` with.
41
+ *
42
+ * `PiEditExecArgs` carries `old_text`/`new_text` pairs, which is exactly
43
+ * `replace`'s schema and nothing else's. The session's own instance follows the
44
+ * configured `edit.mode` — `hashline` by default, whose schema is a single
45
+ * `input` string — so a frame handed that instance fails validation instead of
46
+ * editing the file.
47
+ *
48
+ * Callers MUST gate this on the session having actually granted `edit`: the
49
+ * tool is constructed rather than looked up, so building one unconditionally
50
+ * hands a restricted agent a mutating tool it was denied (issue #5680).
51
+ */
52
+ export function createBridgeEditTool(session: ToolSession, extensionRunner: ExtensionRunner): AgentTool {
53
+ const editTool: Tool = new EditTool(session, "replace");
54
+ return new ExtensionToolWrapper(editTool, extensionRunner);
55
+ }
56
+
57
+ /**
58
+ * The tool map the exec bridge should run, given the map a caller granted.
59
+ *
60
+ * `pi_edit` needs a `replace`-mode instance, but only when `edit` was granted:
61
+ * the tool is constructed rather than looked up, so substituting
62
+ * unconditionally would hand a restricted roster a mutating tool it was denied
63
+ * (issue #5680). The granted map is never mutated: an unsubstituted result is a
64
+ * copy, so a caller without an `edit` grant cannot accidentally gain one.
65
+ *
66
+ * The advisor roster passes its granted map here; the primary session keeps its
67
+ * instance out of the registry entirely (Cursor does not advertise `edit`) and
68
+ * serves it through the bridge's `getEditReplaceTool` accessor instead — not
69
+ * the `getTool` fallback, which doubles as the agent loop's resolver for
70
+ * unadvertised calls and must stay device-only.
71
+ */
72
+ export function bridgeToolMap(
73
+ granted: ReadonlyMap<string, AgentTool>,
74
+ createEditTool: (() => AgentTool | undefined) | undefined,
75
+ ): Map<string, AgentTool> {
76
+ const bridged = new Map(granted);
77
+ if (!granted.has("edit") || !createEditTool) return bridged;
78
+ const bridgeEdit = createEditTool();
79
+ if (bridgeEdit) bridged.set("edit", bridgeEdit);
80
+ return bridged;
81
+ }