@oh-my-pi/pi-coding-agent 17.2.9 → 17.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/{CHANGELOG-8yy8h3bc.md → CHANGELOG-d9xenpn9.md} +83 -0
  3. package/dist/cli.js +10208 -12596
  4. package/dist/types/capability/mcp.d.ts +11 -0
  5. package/dist/types/capability/skill.d.ts +6 -0
  6. package/dist/types/cli/args.d.ts +1 -0
  7. package/dist/types/cli/command-help.d.ts +3 -0
  8. package/dist/types/commands/share.d.ts +25 -0
  9. package/dist/types/commit/agentic/index.d.ts +3 -1
  10. package/dist/types/commit/execute.d.ts +32 -0
  11. package/dist/types/commit/index.d.ts +2 -1
  12. package/dist/types/commit/pipeline.d.ts +7 -1
  13. package/dist/types/config/model-registry.d.ts +4 -0
  14. package/dist/types/config/model-resolver.d.ts +13 -0
  15. package/dist/types/config/settings-schema.d.ts +1 -34
  16. package/dist/types/discovery/agent-plugin-format.d.ts +143 -0
  17. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  18. package/dist/types/discovery/contained-path.d.ts +34 -0
  19. package/dist/types/discovery/index.d.ts +1 -0
  20. package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
  21. package/dist/types/extensibility/custom-tools/types.d.ts +3 -3
  22. package/dist/types/extensibility/extensions/loader.d.ts +9 -2
  23. package/dist/types/extensibility/extensions/types.d.ts +45 -8
  24. package/dist/types/extensibility/hooks/types.d.ts +5 -5
  25. package/dist/types/extensibility/shared-events.d.ts +3 -2
  26. package/dist/types/extensibility/skills.d.ts +5 -0
  27. package/dist/types/index.d.ts +2 -2
  28. package/dist/types/main.d.ts +1 -1
  29. package/dist/types/mcp/transports/header-policy.d.ts +45 -0
  30. package/dist/types/mcp/types.d.ts +15 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  32. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  33. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
  34. package/dist/types/modes/acp/acp-mode.d.ts +1 -1
  35. package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
  36. package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
  37. package/dist/types/modes/components/agent-hub.d.ts +14 -5
  38. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  39. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  40. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  41. package/dist/types/modes/components/model-browser.d.ts +9 -1
  42. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  43. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  44. package/dist/types/modes/interactive-mode.d.ts +8 -1
  45. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  46. package/dist/types/registry/agent-registry.d.ts +47 -0
  47. package/dist/types/registry/persisted-agents.d.ts +3 -1
  48. package/dist/types/secrets/index.d.ts +23 -1
  49. package/dist/types/session/agent-session-events.d.ts +2 -2
  50. package/dist/types/session/agent-session.d.ts +4 -2
  51. package/dist/types/session/messages.d.ts +1 -0
  52. package/dist/types/session/prewalk.d.ts +3 -1
  53. package/dist/types/session/session-entries.d.ts +10 -0
  54. package/dist/types/session/session-loader.d.ts +11 -1
  55. package/dist/types/session/session-manager.d.ts +9 -1
  56. package/dist/types/session/session-tools.d.ts +18 -1
  57. package/dist/types/session/turn-recovery.d.ts +7 -2
  58. package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
  59. package/dist/types/slash-commands/available-commands.d.ts +1 -1
  60. package/dist/types/task/executor.d.ts +6 -0
  61. package/dist/types/task/index.d.ts +4 -3
  62. package/dist/types/task/read-only-policy.d.ts +3 -0
  63. package/dist/types/task/structured-subagent.d.ts +2 -0
  64. package/dist/types/task/types.d.ts +4 -0
  65. package/dist/types/tools/bash.d.ts +3 -3
  66. package/dist/types/tools/browser/launch.d.ts +2 -1
  67. package/dist/types/tools/browser/tab-worker.d.ts +2 -2
  68. package/dist/types/tools/path-utils.d.ts +1 -0
  69. package/dist/types/tools/read.d.ts +1 -5
  70. package/dist/types/tools/run-scope.d.ts +27 -1
  71. package/dist/types/tools/shell-tokenize.d.ts +16 -0
  72. package/dist/types/tools/terminal-output.d.ts +1 -1
  73. package/dist/types/utils/turndown.d.ts +1 -1
  74. package/dist/types/vibe/runtime.d.ts +2 -3
  75. package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
  76. package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
  77. package/examples/custom-tools/README.md +1 -1
  78. package/examples/extensions/README.md +1 -1
  79. package/examples/extensions/api-demo.ts +5 -6
  80. package/examples/extensions/chalk-logger.ts +1 -1
  81. package/examples/extensions/hello.ts +2 -2
  82. package/examples/extensions/reload-runtime.ts +1 -1
  83. package/examples/extensions/tools.ts +2 -2
  84. package/examples/extensions/with-deps/index.ts +1 -1
  85. package/examples/sdk/06-extensions.ts +1 -1
  86. package/package.json +15 -31
  87. package/scripts/bundle-dist.ts +1 -11
  88. package/scripts/legacy-pi-virtual-module.ts +4 -1
  89. package/src/advisor/runtime.ts +54 -0
  90. package/src/capability/mcp.ts +11 -0
  91. package/src/capability/skill.ts +6 -0
  92. package/src/cli/agents-cli.ts +1 -1
  93. package/src/cli/args.ts +24 -2
  94. package/src/cli/auth-broker-cli.ts +1 -1
  95. package/src/cli/auth-gateway-cli.ts +1 -1
  96. package/src/cli/bench-cli.ts +1 -1
  97. package/src/cli/claude-trace-cli.ts +1 -1
  98. package/src/cli/command-help.ts +4 -0
  99. package/src/cli/config-cli.ts +1 -1
  100. package/src/cli/dry-balance-cli.ts +1 -1
  101. package/src/cli/file-processor.ts +1 -1
  102. package/src/cli/flag-tables.ts +4 -0
  103. package/src/cli/gallery-cli.ts +1 -1
  104. package/src/cli/grep-cli.ts +1 -1
  105. package/src/cli/grievances-cli.ts +1 -1
  106. package/src/cli/help-extra.ts +1 -1
  107. package/src/cli/models-cli.ts +1 -1
  108. package/src/cli/plugin-cli.ts +1 -1
  109. package/src/cli/read-cli.ts +1 -1
  110. package/src/cli/setup-cli.ts +1 -1
  111. package/src/cli/shell-cli.ts +1 -1
  112. package/src/cli/ssh-cli.ts +1 -1
  113. package/src/cli/stats-cli.ts +1 -1
  114. package/src/cli/tiny-models-cli.ts +1 -1
  115. package/src/cli/ttsr-cli.ts +1 -1
  116. package/src/cli/update-cli.ts +1 -1
  117. package/src/cli/usage-cli.ts +1 -1
  118. package/src/cli/web-search-cli.ts +1 -1
  119. package/src/cli/worktree-cli.ts +1 -1
  120. package/src/cli-commands.ts +5 -0
  121. package/src/commands/commit.ts +16 -3
  122. package/src/commands/say.ts +1 -1
  123. package/src/commands/share.ts +71 -0
  124. package/src/commands/token.ts +1 -1
  125. package/src/commit/agentic/agent.ts +1 -1
  126. package/src/commit/agentic/index.ts +39 -21
  127. package/src/commit/cli.ts +1 -1
  128. package/src/commit/execute.ts +56 -0
  129. package/src/commit/index.ts +2 -1
  130. package/src/commit/pipeline.ts +20 -7
  131. package/src/config/model-discovery.ts +1 -1
  132. package/src/config/model-registry.ts +21 -1
  133. package/src/config/model-resolver.ts +54 -7
  134. package/src/config/settings-schema.ts +2 -33
  135. package/src/config/settings.ts +46 -0
  136. package/src/discovery/agent-plugin-format.ts +551 -0
  137. package/src/discovery/agent-plugins.ts +341 -0
  138. package/src/discovery/agents.ts +4 -2
  139. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
  140. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
  141. package/src/discovery/claude-plugins.ts +21 -5
  142. package/src/discovery/contained-path.ts +78 -0
  143. package/src/discovery/helpers.ts +23 -9
  144. package/src/discovery/index.ts +1 -0
  145. package/src/discovery/omp-plugins.ts +20 -7
  146. package/src/eval/py/runner.py +38 -1
  147. package/src/exec/non-interactive-env.ts +1 -0
  148. package/src/extensibility/custom-commands/loader.ts +4 -4
  149. package/src/extensibility/custom-commands/types.ts +5 -5
  150. package/src/extensibility/custom-tools/loader.ts +4 -4
  151. package/src/extensibility/custom-tools/types.ts +3 -3
  152. package/src/extensibility/extensions/loader.ts +85 -20
  153. package/src/extensibility/extensions/runner.ts +6 -0
  154. package/src/extensibility/extensions/types.ts +51 -8
  155. package/src/extensibility/hooks/loader.ts +4 -5
  156. package/src/extensibility/hooks/types.ts +5 -5
  157. package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
  158. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  159. package/src/extensibility/shared-events.ts +3 -2
  160. package/src/extensibility/skills.ts +9 -0
  161. package/src/index.ts +2 -2
  162. package/src/internal-urls/memory-protocol.ts +5 -1
  163. package/src/internal-urls/skill-protocol.ts +14 -0
  164. package/src/internal-urls/vault-protocol.ts +2 -2
  165. package/src/launch/client.ts +11 -1
  166. package/src/launch/protocol.ts +7 -2
  167. package/src/launch/terminal-output.ts +1 -1
  168. package/src/main.ts +88 -23
  169. package/src/markit/converters/docx.ts +1 -1
  170. package/src/markit/converters/epub.ts +1 -1
  171. package/src/markit/converters/pptx.ts +1 -1
  172. package/src/markit/converters/xlsx.ts +1 -1
  173. package/src/mcp/config.ts +3 -0
  174. package/src/mcp/manager.ts +12 -9
  175. package/src/mcp/transports/header-policy.ts +95 -0
  176. package/src/mcp/transports/http.ts +35 -52
  177. package/src/mcp/transports/sse.ts +20 -27
  178. package/src/mcp/types.ts +15 -0
  179. package/src/modes/acp/acp-agent.ts +20 -9
  180. package/src/modes/acp/acp-client-bridge.ts +1 -1
  181. package/src/modes/acp/acp-event-mapper.ts +1 -1
  182. package/src/modes/acp/acp-mode.ts +1 -1
  183. package/src/modes/components/agent-dashboard.ts +2 -0
  184. package/src/modes/components/agent-hub-projection.ts +248 -0
  185. package/src/modes/components/agent-hub-renderer.ts +194 -0
  186. package/src/modes/components/agent-hub.ts +674 -207
  187. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  188. package/src/modes/components/assistant-message.ts +1 -1
  189. package/src/modes/components/chat-transcript-builder.ts +3 -0
  190. package/src/modes/components/custom-editor.ts +0 -9
  191. package/src/modes/components/extensions/inspector-panel.ts +11 -6
  192. package/src/modes/components/footer.ts +1 -3
  193. package/src/modes/components/model-browser.ts +11 -3
  194. package/src/modes/components/model-hub.ts +4 -1
  195. package/src/modes/components/status-line/component.ts +1 -0
  196. package/src/modes/components/status-line/segments.ts +10 -7
  197. package/src/modes/components/status-line/types.ts +2 -0
  198. package/src/modes/components/tool-execution.ts +13 -16
  199. package/src/modes/components/tree-selector.ts +62 -3
  200. package/src/modes/controllers/command-controller.ts +4 -1
  201. package/src/modes/controllers/event-controller.ts +44 -20
  202. package/src/modes/controllers/extension-ui-controller.ts +2 -2
  203. package/src/modes/controllers/input-controller.ts +20 -2
  204. package/src/modes/controllers/mcp-command-controller.ts +81 -20
  205. package/src/modes/controllers/selector-controller.ts +41 -49
  206. package/src/modes/controllers/streaming-reveal.ts +1 -1
  207. package/src/modes/interactive-mode.ts +13 -2
  208. package/src/modes/rpc/rpc-mode.ts +2 -2
  209. package/src/modes/runtime-init.ts +1 -1
  210. package/src/modes/theme/theme.ts +2 -2
  211. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  212. package/src/modes/utils/ui-helpers.ts +1 -0
  213. package/src/prompts/tools/computer.md +1 -1
  214. package/src/registry/agent-lifecycle.ts +14 -7
  215. package/src/registry/agent-registry.ts +65 -2
  216. package/src/registry/persisted-agents.ts +341 -16
  217. package/src/sdk.ts +6 -49
  218. package/src/secrets/index.ts +52 -1
  219. package/src/session/agent-session-events.ts +2 -2
  220. package/src/session/agent-session.ts +54 -16
  221. package/src/session/messages.ts +1 -0
  222. package/src/session/prewalk.ts +57 -17
  223. package/src/session/session-context.ts +3 -5
  224. package/src/session/session-entries.ts +10 -0
  225. package/src/session/session-handoff.ts +5 -1
  226. package/src/session/session-listing.ts +1 -1
  227. package/src/session/session-loader.ts +74 -8
  228. package/src/session/session-manager.ts +22 -2
  229. package/src/session/session-paths.ts +57 -9
  230. package/src/session/session-tools.ts +65 -4
  231. package/src/session/todo-tracker.ts +11 -1
  232. package/src/session/turn-recovery.ts +96 -74
  233. package/src/slash-commands/acp-builtins.ts +1 -1
  234. package/src/slash-commands/available-commands.ts +1 -1
  235. package/src/slash-commands/builtin-registry.ts +12 -8
  236. package/src/task/executor.ts +70 -20
  237. package/src/task/index.ts +30 -34
  238. package/src/task/isolation-runner.ts +24 -11
  239. package/src/task/persisted-revive.ts +12 -5
  240. package/src/task/read-only-policy.ts +27 -0
  241. package/src/task/structured-subagent.ts +14 -4
  242. package/src/task/types.ts +4 -0
  243. package/src/tools/auto-generated-guard.ts +1 -1
  244. package/src/tools/bash-interactive.ts +4 -4
  245. package/src/tools/bash-skill-urls.ts +15 -0
  246. package/src/tools/bash.ts +16 -17
  247. package/src/tools/browser/cmux/cmux-tab.ts +66 -18
  248. package/src/tools/browser/launch.ts +41 -12
  249. package/src/tools/browser/readable.ts +9 -9
  250. package/src/tools/browser/tab-supervisor.ts +12 -3
  251. package/src/tools/browser/tab-worker-entry.ts +1 -1
  252. package/src/tools/browser/tab-worker.ts +101 -12
  253. package/src/tools/debug.ts +1 -1
  254. package/src/tools/fetch.ts +1 -1
  255. package/src/tools/jtd-to-json-schema.ts +123 -21
  256. package/src/tools/path-utils.ts +9 -2
  257. package/src/tools/read.ts +21 -9
  258. package/src/tools/run-scope.ts +290 -4
  259. package/src/tools/shell-tokenize.ts +98 -0
  260. package/src/tools/terminal-output.ts +1 -1
  261. package/src/tools/todo.ts +1 -1
  262. package/src/utils/clipboard.ts +10 -2
  263. package/src/utils/external-editor.ts +4 -2
  264. package/src/utils/jj.ts +1 -1
  265. package/src/utils/turndown.ts +1 -2
  266. package/src/vibe/runtime.ts +5 -5
  267. package/src/web/scrapers/arxiv.ts +1 -1
  268. package/src/web/scrapers/go-pkg.ts +1 -1
  269. package/src/web/scrapers/iacr.ts +1 -1
  270. package/src/web/scrapers/readthedocs.ts +2 -1
  271. package/src/web/scrapers/twitter.ts +2 -2
  272. package/src/web/scrapers/types.ts +1 -1
  273. package/src/web/scrapers/wikipedia.ts +1 -1
  274. package/src/web/search/providers/browser-headers.ts +12 -39
  275. package/src/web/search/providers/codex.ts +3 -26
  276. package/src/web/search/providers/ecosia.ts +1 -1
  277. package/src/web/search/providers/exa.ts +1 -1
  278. package/src/web/search/providers/google.ts +1 -1
  279. package/src/web/search/providers/mojeek.ts +1 -1
  280. package/src/web/search/providers/startpage.ts +1 -1
  281. package/src/markit/converters/mammoth.d.ts +0 -24
@@ -89,6 +89,66 @@ function raceAbortSignal<T>(promise: Promise<T>, signal: AbortSignal, createErro
89
89
  });
90
90
  }
91
91
 
92
+ type ActiveMCPOAuthFlow = {
93
+ cancel: (reason: string) => void;
94
+ completion: Promise<void>;
95
+ complete: () => void;
96
+ };
97
+
98
+ type MCPOAuthFlowCoordinator = {
99
+ active?: ActiveMCPOAuthFlow;
100
+ transition: Promise<void>;
101
+ };
102
+
103
+ const mcpOAuthFlowCoordinators = new WeakMap<object, MCPOAuthFlowCoordinator>();
104
+ const MCP_OAUTH_SUPERSEDED_REASON = "MCP OAuth flow superseded by a new login";
105
+
106
+ /**
107
+ * Serialize MCP OAuth ownership across slash-command controller instances.
108
+ * Interactive mode creates a new controller for every command, while the
109
+ * manual-input manager remains stable for the session and is therefore the
110
+ * lifecycle key.
111
+ */
112
+ async function claimMCPOAuthFlow(owner: object, cancel: (reason: string) => void): Promise<{ release: () => void }> {
113
+ let coordinator = mcpOAuthFlowCoordinators.get(owner);
114
+ if (!coordinator) {
115
+ coordinator = { transition: Promise.resolve() };
116
+ mcpOAuthFlowCoordinators.set(owner, coordinator);
117
+ }
118
+
119
+ const precedingTransition = coordinator.transition;
120
+ const transition = Promise.withResolvers<void>();
121
+ coordinator.transition = transition.promise;
122
+ await precedingTransition;
123
+
124
+ try {
125
+ const active = coordinator.active;
126
+ if (active) {
127
+ active.cancel(MCP_OAUTH_SUPERSEDED_REASON);
128
+ await active.completion;
129
+ }
130
+
131
+ const completion = Promise.withResolvers<void>();
132
+ const flow: ActiveMCPOAuthFlow = {
133
+ cancel,
134
+ completion: completion.promise,
135
+ complete: () => completion.resolve(),
136
+ };
137
+ coordinator.active = flow;
138
+ let released = false;
139
+ return {
140
+ release: () => {
141
+ if (released) return;
142
+ released = true;
143
+ if (coordinator.active === flow) coordinator.active = undefined;
144
+ flow.complete();
145
+ },
146
+ };
147
+ } finally {
148
+ transition.resolve();
149
+ }
150
+ }
151
+
92
152
  /**
93
153
  * Minimum column budget for URL wrapping. Below this the terminal is
94
154
  * effectively unusable, but we still emit chunks so no character is silently
@@ -782,28 +842,23 @@ export class MCPCommandController {
782
842
  const resolvedClientSecret = clientSecret.trim() || undefined;
783
843
 
784
844
  const manualInput = this.ctx.oauthManualInput;
785
- if (manualInput.hasPending()) {
786
- const pendingProvider = manualInput.pendingProviderId ?? "another provider";
787
- throw new Error(
788
- `OAuth login already in progress for ${pendingProvider}. Complete or cancel it before starting MCP OAuth.`,
789
- );
790
- }
791
845
  let manualInputClaim: { promise: Promise<string>; clear: (reason?: string) => void } | undefined;
792
846
  const oauthTimeout = new AbortController();
793
- // User Esc and external aborts route through here; the timeout path sets
794
- // its own reason and leaves this flag false so the catch can distinguish
795
- // "user cancelled" (status) from "deadline elapsed" (error).
796
- let userCancelled = false;
797
- const requestUserCancel = (reason: string): void => {
798
- userCancelled = true;
847
+ // Esc, external aborts, and a replacement MCP flow route through here;
848
+ // the timeout path sets its own reason and leaves this flag false so the
849
+ // catch can distinguish cancellation (status) from deadline failure.
850
+ let cancellationRequested = false;
851
+ const requestCancellation = (reason: string): void => {
852
+ cancellationRequested = true;
799
853
  if (!oauthTimeout.signal.aborted) oauthTimeout.abort(reason);
800
854
  };
855
+ const flowClaim = await claimMCPOAuthFlow(manualInput, requestCancellation);
801
856
  const originalOnEscape = this.ctx.editor.onEscape;
802
- this.ctx.editor.onEscape = () => requestUserCancel(MCP_OAUTH_USER_CANCEL_REASON);
857
+ this.ctx.editor.onEscape = () => requestCancellation(MCP_OAUTH_USER_CANCEL_REASON);
803
858
  const externalSignal = opts?.abortSignal;
804
859
  const onExternalAbort = (): void => {
805
860
  const reason = externalSignal?.reason;
806
- requestUserCancel(typeof reason === "string" ? reason : MCP_OAUTH_USER_CANCEL_REASON);
861
+ requestCancellation(typeof reason === "string" ? reason : MCP_OAUTH_USER_CANCEL_REASON);
807
862
  };
808
863
  if (externalSignal?.aborted) {
809
864
  onExternalAbort();
@@ -811,6 +866,12 @@ export class MCPCommandController {
811
866
  externalSignal?.addEventListener("abort", onExternalAbort, { once: true });
812
867
  }
813
868
  try {
869
+ if (manualInput.hasPending()) {
870
+ const pendingProvider = manualInput.pendingProviderId ?? "another provider";
871
+ throw new Error(
872
+ `OAuth login already in progress for ${pendingProvider}. Complete or cancel it before starting MCP OAuth.`,
873
+ );
874
+ }
814
875
  // Create OAuth flow
815
876
  const flow = new MCPOAuthFlow(
816
877
  {
@@ -887,7 +948,7 @@ export class MCPCommandController {
887
948
 
888
949
  const createAbortError = (): Error => {
889
950
  const reason = String(oauthTimeout.signal.reason ?? "MCP OAuth flow aborted");
890
- return userCancelled ? new MCPOAuthCancelledError() : new Error(reason);
951
+ return cancellationRequested ? new MCPOAuthCancelledError() : new Error(reason);
891
952
  };
892
953
  if (oauthTimeout.signal.aborted) throw createAbortError();
893
954
 
@@ -934,11 +995,10 @@ export class MCPCommandController {
934
995
  resource: flow.resource,
935
996
  };
936
997
  } catch (error) {
937
- // User-initiated cancel (Esc or external signal) neutral status, not
938
- // a failure. Check the flag we set in `requestUserCancel`, not the
939
- // abort reason: the timeout path also aborts but with a different
940
- // reason, and we want it to surface as a timeout error below.
941
- if (userCancelled) {
998
+ // Esc, an external abort, or a newer MCP flow are neutral
999
+ // cancellations. The timeout path also aborts the controller but does
1000
+ // not set this flag, so it remains a surfaced error.
1001
+ if (cancellationRequested) {
942
1002
  throw new MCPOAuthCancelledError();
943
1003
  }
944
1004
 
@@ -960,6 +1020,7 @@ export class MCPCommandController {
960
1020
  this.ctx.editor.onEscape = originalOnEscape;
961
1021
  externalSignal?.removeEventListener("abort", onExternalAbort);
962
1022
  manualInputClaim?.clear("Manual MCP OAuth input cleared");
1023
+ flowClaim.release();
963
1024
  }
964
1025
  }
965
1026
 
@@ -110,6 +110,23 @@ const MANUAL_LOGIN_PROMPT = "Paste the authorization code (or full redirect URL)
110
110
 
111
111
  export class SelectorController {
112
112
  constructor(private ctx: InteractiveModeContext) {}
113
+ /**
114
+ * Mount a primary fullscreen menu through the one polished modal path shared
115
+ * by Settings, Model Hub, and Agent Hub.
116
+ */
117
+ #showFullscreenMenu(component: Component): OverlayHandle {
118
+ const handle = this.ctx.ui.showOverlay(component, {
119
+ anchor: "bottom-center",
120
+ width: "100%",
121
+ maxHeight: "100%",
122
+ margin: 0,
123
+ fullscreen: true,
124
+ });
125
+ this.ctx.ui.setFocus(component);
126
+ this.ctx.ui.requestRender();
127
+ return handle;
128
+ }
129
+
113
130
  #defaultRoleMutationTail = Promise.resolve();
114
131
 
115
132
  async #acquireDefaultRoleMutation(): Promise<() => void> {
@@ -242,15 +259,7 @@ export class SelectorController {
242
259
  },
243
260
  },
244
261
  );
245
- overlayHandle = this.ctx.ui.showOverlay(selector, {
246
- anchor: "bottom-center",
247
- width: "100%",
248
- maxHeight: "100%",
249
- margin: 0,
250
- fullscreen: true,
251
- });
252
- this.ctx.ui.setFocus(selector);
253
- this.ctx.ui.requestRender();
262
+ overlayHandle = this.#showFullscreenMenu(selector);
254
263
  });
255
264
  }
256
265
 
@@ -1023,15 +1032,7 @@ export class SelectorController {
1023
1032
  initialProviderId: hubOptions.initialProviderId,
1024
1033
  },
1025
1034
  );
1026
- overlayHandle = this.ctx.ui.showOverlay(hub, {
1027
- anchor: "bottom-center",
1028
- width: "100%",
1029
- maxHeight: "100%",
1030
- margin: 0,
1031
- fullscreen: true,
1032
- });
1033
- this.ctx.ui.setFocus(hub);
1034
- this.ctx.ui.requestRender();
1035
+ overlayHandle = this.#showFullscreenMenu(hub);
1035
1036
  }
1036
1037
 
1037
1038
  /** /login round-trip for a locked provider; reopen the hub on that provider only after a successful login. */
@@ -2000,27 +2001,23 @@ export class SelectorController {
2000
2001
  ...this.ctx.keybindings.getKeys("app.agents.hub"),
2001
2002
  ...this.ctx.keybindings.getKeys("app.session.observe"),
2002
2003
  ];
2003
- let hub: AgentHubOverlayComponent | undefined;
2004
-
2005
- // Render the hub inline in the editor slot — the same anchored region
2006
- // every other selector (model, session, tree, the `ask` tool) uses —
2007
- // rather than a floating overlay. A non-fullscreen overlay composited over
2008
- // a live transcript strands a stale copy in native scrollback every time a
2009
- // running subagent's progress grows the frame and scrolls the window; the
2010
- // hub is opened mid-run, so those copies stacked into a wall of duplicate
2011
- // "Agent Hub" frames bleeding the task tree behind them. As an editor-slot
2012
- // component it rides the normal append-only commit path: the transcript
2013
- // commits above it exactly once and the hub repaints in place.
2004
+ let overlayHandle: OverlayHandle | undefined;
2005
+ let closed = false;
2006
+
2014
2007
  const done = () => {
2015
- hub?.dispose();
2016
- this.ctx.editorContainer.clear();
2017
- this.ctx.editorContainer.addChild(this.ctx.editor);
2018
- this.ctx.ui.setFocus(this.ctx.editor);
2008
+ if (closed) return;
2009
+ closed = true;
2010
+ hub.dispose();
2011
+ overlayHandle?.hide();
2012
+ // A gated empty Hub may never have been mounted. Restoring editor
2013
+ // focus in that case would steal focus from a menu opened meanwhile.
2014
+ if (overlayHandle) this.focusActiveEditorArea();
2019
2015
  this.ctx.ui.requestRender();
2020
2016
  };
2021
2017
 
2022
- hub = new AgentHubOverlayComponent({
2018
+ const hub = new AgentHubOverlayComponent({
2023
2019
  observers,
2020
+ settings: this.ctx.settings,
2024
2021
  hubKeys,
2025
2022
  expandKeys: this.ctx.keybindings.getKeys("app.tools.expand"),
2026
2023
  onDone: done,
@@ -2029,6 +2026,7 @@ export class SelectorController {
2029
2026
  remote: this.ctx.collabGuest?.hubRemote,
2030
2027
  ui: this.ctx.ui,
2031
2028
  getTool: name => this.ctx.session.getToolByName(name),
2029
+ isBuiltInTool: name => this.ctx.session.hasBuiltInTool(name),
2032
2030
  getMessageRenderer: type => this.ctx.session.extensionRunner?.getMessageRenderer(type),
2033
2031
  cwd: this.ctx.sessionManager.getCwd(),
2034
2032
  hideThinkingBlock: () => this.ctx.effectiveHideThinkingBlock,
@@ -2038,30 +2036,24 @@ export class SelectorController {
2038
2036
  });
2039
2037
 
2040
2038
  const showReadyHub = () => {
2041
- // The double-← gesture passes requireContent so it stays inert when
2042
- // neither live nor persisted subagents are available. Persisted rows now
2043
- // load asynchronously, so defer the gate until that scan has refreshed the
2044
- // hub instead of treating the initial empty table as authoritative.
2039
+ if (closed) return;
2040
+ // The double-← gesture stays inert when neither live nor persisted
2041
+ // subagents are available, so wait for discovery before making the gate.
2045
2042
  if (options?.requireContent && hub.isEmpty) {
2046
- hub.dispose();
2043
+ done();
2047
2044
  return;
2048
2045
  }
2049
2046
 
2050
- this.ctx.editorContainer.clear();
2051
- this.ctx.editorContainer.addChild(hub);
2052
- this.ctx.ui.setFocus(hub);
2053
- // When the hub was raised by the editor's double-← gesture, prime its own
2054
- // close detector so the *next* single ← dismisses it — the two taps that
2055
- // opened it were consumed by the editor's detector (issue #4780).
2047
+ // Prime the detector before the first frame when the editor's double-←
2048
+ // gesture opened the hub, so the next single ← dismisses it.
2056
2049
  if (options?.armCloseTap) hub.armCloseTap();
2057
- this.ctx.ui.requestRender();
2050
+ overlayHandle = this.#showFullscreenMenu(hub);
2058
2051
  };
2059
2052
 
2060
2053
  if (options?.requireContent && hub.isEmpty) {
2061
2054
  void hub.persistedSubagentsReady.then(showReadyHub);
2062
- return;
2055
+ } else {
2056
+ showReadyHub();
2063
2057
  }
2064
-
2065
- showReadyHub();
2066
2058
  }
2067
2059
  }
@@ -1,6 +1,6 @@
1
1
  import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
2
  import { type Component, getSegmenter } from "@oh-my-pi/pi-tui";
3
- import { LRUCache } from "lru-cache/raw";
3
+ import { LRUCache } from "@oh-my-pi/pi-utils/lru";
4
4
  import { formatThinkingForDisplay, hasDisplayableThinking } from "../../utils/thinking-display";
5
5
  import type { AssistantMessageComponent } from "../components/assistant-message";
6
6
 
@@ -52,7 +52,7 @@ import {
52
52
  prompt,
53
53
  setProjectDir,
54
54
  } from "@oh-my-pi/pi-utils";
55
- import chalk from "chalk";
55
+ import chalk from "@oh-my-pi/pi-utils/chalk";
56
56
  import { reset as resetCapabilities } from "../capability";
57
57
  import type { CollabGuestLink } from "../collab/guest";
58
58
  import type { CollabHost } from "../collab/host";
@@ -4141,7 +4141,14 @@ export class InteractiveMode implements InteractiveModeContext {
4141
4141
  this.ui.requestRender();
4142
4142
  }
4143
4143
 
4144
- /** Defer transcript command panels until the active turn can no longer grow above them. */
4144
+ /**
4145
+ * Defer transcript command panels while the agent is streaming, then mount
4146
+ * them at the next settle, terminal or not. A non-terminal settle is only a
4147
+ * scheduling pause, so resumed streaming can still land below a panel
4148
+ * flushed there. That is preferred over leaving it queued behind a command
4149
+ * the user runs during the pause, which mounts immediately and would put the
4150
+ * older panel out of order.
4151
+ */
4145
4152
  presentCommandOutput(content: Component | readonly Component[]): void {
4146
4153
  if (!this.session.isStreaming) {
4147
4154
  this.present(content);
@@ -4154,6 +4161,10 @@ export class InteractiveMode implements InteractiveModeContext {
4154
4161
  this.#pendingCommandOutputSessionId = sessionId;
4155
4162
  const items = Array.isArray(content) ? content : [content as Component];
4156
4163
  this.#pendingCommandOutput.push(...items);
4164
+ // No feedback here on purpose: mounting anything into the transcript
4165
+ // mid-turn (even a status line) re-renders rows below the growing live
4166
+ // block and duplicates them in native scrollback — the exact regression
4167
+ // issues #4806/#6767 pin. The queue flushes at the next settle.
4157
4168
  }
4158
4169
 
4159
4170
  /** Mount every command panel queued for the current session while the agent was streaming. */
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { once } from "node:events";
14
14
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
15
- import { isZodSchema, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
15
+ import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
16
16
  import { $env, isRecord, readLines, Snowflake } from "@oh-my-pi/pi-utils";
17
17
  import { reset as resetCapabilities } from "../../capability";
18
18
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
@@ -1095,7 +1095,7 @@ export async function runRpcMode(
1095
1095
  dumpTools: session.agent.state.tools.map(tool => ({
1096
1096
  name: tool.name,
1097
1097
  description: tool.description,
1098
- parameters: isZodSchema(tool.parameters) ? zodToWireSchema(tool.parameters) : tool.parameters,
1098
+ parameters: toolWireSchema(tool),
1099
1099
  examples: tool.examples,
1100
1100
  })),
1101
1101
  contextUsage: session.getContextUsage(),
@@ -84,7 +84,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
84
84
  session.sessionManager.appendLabelChange(targetId, label);
85
85
  },
86
86
  getActiveTools: () => session.getEnabledToolNames(),
87
- getAllTools: () => session.getAllToolNames(),
87
+ getAllTools: () => session.getAllToolInfos(),
88
88
  setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
89
89
  getCommands: () => getSessionSlashCommands(session),
90
90
  setModel: model => runExtensionSetModel(session, model),
@@ -20,8 +20,8 @@ import type {
20
20
  TerminalAppearance,
21
21
  } from "@oh-my-pi/pi-tui";
22
22
  import { adjustHsv, colorLuma, getCustomThemesDir, isEnoent, logger, relativeLuminance } from "@oh-my-pi/pi-utils";
23
- import chalk from "chalk";
24
- import { LRUCache } from "lru-cache/raw";
23
+ import chalk from "@oh-my-pi/pi-utils/chalk";
24
+ import { LRUCache } from "@oh-my-pi/pi-utils/lru";
25
25
  // Embed theme JSON files at build time
26
26
  import darkThemeJson from "./dark.json" with { type: "json" };
27
27
  import { defaultThemes } from "./defaults";
@@ -211,13 +211,15 @@ function sanitizeRecoveredRetryNote(note: string): string {
211
211
 
212
212
  /**
213
213
  * Resolve the turn-ending assistant error presentation, if any.
214
- * Silent and user-interrupt aborts yield no label. Recovered auto-retry errors
215
- * collapse to a single non-error note; terminal errors keep the full red presentation.
214
+ * Silent and user-interrupt aborts yield no label. Recovered retry attempts
215
+ * render a compact note; attempts superseded by an exhausted budget are hidden
216
+ * while the final terminal error keeps its full presentation.
216
217
  */
217
218
  export function resolveAssistantErrorPresentation(
218
219
  message: AssistantAgentMessage,
219
220
  retryAttempt = 0,
220
221
  ): AssistantErrorPresentation {
222
+ if (message.retryRecovery?.status === "superseded") return { kind: "none" };
221
223
  if (message.retryRecovery?.status === "recovered") {
222
224
  return {
223
225
  kind: "compact-recovered",
@@ -500,6 +500,7 @@ export class UiHelpers {
500
500
  content.name,
501
501
  renderArgs,
502
502
  {
503
+ useBuiltInRenderer: this.ctx.viewSession.hasBuiltInTool(content.name),
503
504
  snapshots: getFileSnapshotStore(this.ctx.viewSession),
504
505
  clipboard: getEditClipboard(this.ctx.viewSession),
505
506
  showImages: settings.get("terminal.showImages"),
@@ -16,7 +16,7 @@ Controls the host desktop with a JS script: windows, screenshots, native input,
16
16
  - Pointer `x,y` are pixels in the MOST RECENT screenshot of the SAME target (window or desktop). No screenshot of that target yet → coordinate input throws. AX coordinates (`.bounds()`, `elementAt`) are global desktop coords — two spaces, both converted automatically; never mix them.
17
17
  - Each `.ax()` of a window starts a new ref generation; refs from the current and previous snapshot stay valid, older ones throw StaleRef — re-snapshot, don't guess.
18
18
  - Input defaults to `delivery: "background"` — delivered to the target window without touching the user's focus, pointer, or window order. On macOS, keyboard input to an app with multiple windows throws `BackgroundUnavailable` because the OS accepts only a process id and could send keys to a different window; retry with `delivery: "foreground"` (briefly activates the target, acts, restores focus) or act through AX instead. Targets whose input stack drops other background events also throw `BackgroundUnavailable` naming the window class and event kind. Never assume a background action landed because no error was displayed — errors are how this surface reports failure.
19
- - Wayland only: there is no per-window background input (compositor-focus-only); use AX actions, or `delivery: "foreground"`.
19
+ - Wayland only: per-window native input and `raise()` are unavailable; use AX actions, or desktop input after focusing the target yourself.
20
20
  - `read_only: true` for pure inspection — input and mutation throw, approval is lighter.
21
21
  - Screenshots auto-display to you and save full-res to a temp path; pass `{silent: true}` in loops.
22
22
 
@@ -20,6 +20,7 @@
20
20
  * a superseded revive) can never clobber a newer same-id ref.
21
21
  */
22
22
 
23
+ import * as fs from "node:fs/promises";
23
24
  import { logger, untilAborted } from "@oh-my-pi/pi-utils";
24
25
  import type { AgentSession } from "../session/agent-session";
25
26
  import { trackLateCleanup } from "../utils/late-cleanup";
@@ -27,6 +28,7 @@ import {
27
28
  type AgentRef,
28
29
  type AgentRefExpectation,
29
30
  AgentRegistry,
31
+ getAgentTombstonePath,
30
32
  MAIN_AGENT_ID,
31
33
  type RegistryEvent,
32
34
  } from "./agent-registry";
@@ -35,6 +37,14 @@ export type AgentReviver = (expected: AgentRef) => Promise<AgentSession>;
35
37
 
36
38
  const AGENT_RELEASE_GRACE_MS = 5000;
37
39
 
40
+ async function persistAgentTombstone(sessionFile: string): Promise<void> {
41
+ try {
42
+ await fs.writeFile(getAgentTombstonePath(sessionFile), "", { encoding: "utf8", flag: "wx", mode: 0o600 });
43
+ } catch (error) {
44
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
45
+ }
46
+ }
47
+
38
48
  /**
39
49
  * Builds a reviver for a `parked` ref restored from disk (Agent Hub scan,
40
50
  * collab mirror, resumed process) that carries a sessionFile but no in-memory
@@ -382,13 +392,10 @@ export class AgentLifecycleManager {
382
392
  }
383
393
 
384
394
  if (options?.tombstone) {
385
- // Explicit kill: mark the ref terminal `aborted` and detach the session
386
- // BEFORE disposing. aborted refs must satisfy the AgentRef invariant
387
- // (session === null) so ensureLive / hub focus can never route into a
388
- // disposed session; setting the terminal status first also makes
389
- // createAgentSession's dispose wrapper (unregisterUnlessParked) preserve
390
- // the ref instead of removing it, so a later persisted-subagent rescan
391
- // skips it. The transcript file is left intact (history://<id>).
395
+ // Persist the terminal decision before detaching the session. The
396
+ // sidecar prevents a later discovery pass from reviving this transcript
397
+ // as a fresh parked ref.
398
+ if (ref.sessionFile) await persistAgentTombstone(ref.sessionFile);
392
399
  this.#registry.setStatus(id, "aborted", ref);
393
400
  }
394
401
  const live = this.#registry.get(id) === ref ? ref.session : null;
@@ -14,6 +14,13 @@ import { oneLineLabel } from "../task/types";
14
14
 
15
15
  export const MAIN_AGENT_ID = "Main";
16
16
 
17
+ /** Sidecar marker retained beside a child transcript after an explicit kill. */
18
+ export const AGENT_TOMBSTONE_SUFFIX = ".tombstone";
19
+
20
+ export function getAgentTombstonePath(sessionFile: string): string {
21
+ return `${sessionFile}${AGENT_TOMBSTONE_SUFFIX}`;
22
+ }
23
+
17
24
  /**
18
25
  * - `running`: a turn is in flight.
19
26
  * - `idle`: live AgentSession in memory, awaiting work. Finished agents are
@@ -22,6 +29,8 @@ export const MAIN_AGENT_ID = "Main";
22
29
  * - `aborted`: hard-killed, terminal.
23
30
  */
24
31
  export type AgentStatus = "running" | "idle" | "parked" | "aborted";
32
+ /** Provenance of a displayed duration: active runtime, transcript span, or unavailable. */
33
+ export type AgentDurationKind = "active" | "span" | "unknown";
25
34
  /**
26
35
  * - `main`/`sub`: the user-facing agent tree (driving agent + task subagents).
27
36
  * - `advisor`: a passive review transcript persisted like a subagent for usage
@@ -30,6 +39,35 @@ export type AgentStatus = "running" | "idle" | "parked" | "aborted";
30
39
  */
31
40
  export type AgentKind = "main" | "sub" | "advisor";
32
41
 
42
+ /** Persisted per-agent totals reconstructed from the child session transcript. */
43
+ export interface AgentMetricsSummary {
44
+ tokens: number;
45
+ requests: number;
46
+ tools: number;
47
+ cost: number;
48
+ durationMs: number;
49
+ durationKind?: AgentDurationKind;
50
+ contextTokens?: number;
51
+ contextWindow?: number;
52
+ }
53
+
54
+ /** Historical identity and telemetry that remain available after the live session is disposed. */
55
+ export interface AgentHistorySummary {
56
+ agent?: string;
57
+ modelRole?: string;
58
+ resolvedModel?: string;
59
+ /** Whether the last resolved model was selected by retry fallback routing. */
60
+ resolvedModelIsFallback?: boolean;
61
+ metrics?: AgentMetricsSummary;
62
+ readOnly?: boolean;
63
+ /** Durable task output artifact, when the executor wrote one. */
64
+ outputPath?: string;
65
+ /** Captured isolated-worktree patch, when patch capture succeeded. */
66
+ patchPath?: string;
67
+ /** Isolated branch identity, when branch-mode capture succeeded. */
68
+ branchName?: string;
69
+ }
70
+
33
71
  export interface AgentRef {
34
72
  id: string;
35
73
  displayName: string;
@@ -43,6 +81,8 @@ export interface AgentRef {
43
81
  lastActivity: number;
44
82
  /** Short gist of what the agent is currently doing (latest intent or tool), for the work-aware roster. Display-only. */
45
83
  activity?: string;
84
+ /** Persisted identity and telemetry restored after the live observer is gone. */
85
+ history?: AgentHistorySummary;
46
86
  }
47
87
 
48
88
  export type AgentRefExpectation = AgentRef | AgentSession;
@@ -50,6 +90,7 @@ export type AgentRefExpectation = AgentRef | AgentSession;
50
90
  export type RegistryEvent =
51
91
  | { type: "registered"; ref: AgentRef }
52
92
  | { type: "status_changed"; ref: AgentRef }
93
+ | { type: "metadata_changed"; ref: AgentRef }
53
94
  | { type: "removed"; ref: AgentRef };
54
95
 
55
96
  type RegistryListener = (event: RegistryEvent) => void;
@@ -62,6 +103,14 @@ export interface RegisterInput {
62
103
  session: AgentSession | null;
63
104
  sessionFile?: string | null;
64
105
  status?: AgentStatus;
106
+ /** Last persisted task summary, when restoring a historical agent. */
107
+ activity?: string;
108
+ /** Original registration timestamp, when known from persisted history. */
109
+ createdAt?: number;
110
+ /** Last transcript activity timestamp, when known from persisted history. */
111
+ lastActivity?: number;
112
+ /** Persisted identity and telemetry restored after the live observer is gone. */
113
+ history?: AgentHistorySummary;
65
114
  }
66
115
 
67
116
  export class AgentRegistry {
@@ -96,8 +145,10 @@ export class AgentRegistry {
96
145
  status: input.status ?? "running",
97
146
  session: input.session,
98
147
  sessionFile: input.sessionFile ?? null,
99
- createdAt: now,
100
- lastActivity: now,
148
+ createdAt: input.createdAt ?? now,
149
+ lastActivity: input.lastActivity ?? now,
150
+ activity: input.activity,
151
+ history: input.history,
101
152
  };
102
153
  this.#refs.set(ref.id, ref);
103
154
  this.#emit({ type: "registered", ref });
@@ -116,6 +167,18 @@ export class AgentRegistry {
116
167
  return current === expected && current.status === "parked" && !current.session ? current : undefined;
117
168
  }
118
169
 
170
+ /** Attach transcript-derived identity and telemetry without changing lifecycle state. */
171
+ setHistory(id: string, history: AgentHistorySummary, expectedSessionFile?: string): boolean {
172
+ const ref = this.#refs.get(id);
173
+ if (!ref || (expectedSessionFile !== undefined && ref.sessionFile !== expectedSessionFile)) return false;
174
+ const definedHistory = Object.fromEntries(
175
+ Object.entries(history).filter(([, value]) => value !== undefined),
176
+ ) as AgentHistorySummary;
177
+ ref.history = { ...ref.history, ...definedHistory };
178
+ this.#emit({ type: "metadata_changed", ref });
179
+ return true;
180
+ }
181
+
119
182
  setStatus(id: string, status: AgentStatus, expected?: AgentRefExpectation): boolean {
120
183
  const ref = this.#refs.get(id);
121
184
  if (!ref || !this.#matchesExpected(ref, expected)) return false;