@earendil-works/pi-coding-agent 1.0.3 → 1.1.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 (237) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/dist/bundle/chunks/{anthropic-messages-Y22YDKSW.js → anthropic-messages-KT2ID7DZ.js} +1 -1
  3. package/dist/bundle/chunks/anthropic.js +1 -1
  4. package/dist/bundle/chunks/{azure-openai-responses-YJDJIIDQ.js → azure-openai-responses-6A4PNZTH.js} +1 -1
  5. package/dist/bundle/chunks/bedrock-converse-stream.js +2 -2
  6. package/dist/bundle/chunks/chunk-33CRHCAK.js +2 -0
  7. package/dist/bundle/chunks/{chunk-UBHCV62G.js → chunk-BK6FEHYX.js} +3 -3
  8. package/dist/bundle/chunks/chunk-CH4ZBH45.js +11 -0
  9. package/dist/bundle/chunks/{chunk-6DBOMCBS.js → chunk-GXLAGZ3Z.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-J52J6TFP.js → chunk-HKYL362W.js} +1 -1
  11. package/dist/bundle/chunks/chunk-LPXNMA2W.js +2 -0
  12. package/dist/bundle/chunks/{chunk-TAOL3JFA.js → chunk-LTSARG5Q.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-ZSQEXT6X.js → chunk-MXTZIDYW.js} +4 -4
  14. package/dist/bundle/chunks/chunk-O7YIWAPD.js +2 -0
  15. package/dist/bundle/chunks/{chunk-BFNE7BHG.js → chunk-OIM2DMFI.js} +83 -72
  16. package/dist/bundle/chunks/{chunk-AXOERKKW.js → chunk-OTL6MGLJ.js} +6 -4
  17. package/dist/bundle/chunks/{chunk-LD2P6SFZ.js → chunk-PMRZO46Y.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-7TP2J6F2.js → chunk-SYCGMWFH.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-2VBQLN7Y.js → chunk-UH5J7I7X.js} +9 -6
  20. package/dist/bundle/chunks/{chunk-WE4MXUQS.js → chunk-VLRCF7MR.js} +1 -1
  21. package/dist/bundle/chunks/chunk-W5MPVSHA.js +82 -0
  22. package/dist/bundle/chunks/chunk-Y4NYF362.js +2 -0
  23. package/dist/bundle/chunks/{cli-HTJP6F6Z.js → cli-OQYLWLA3.js} +2 -2
  24. package/dist/bundle/chunks/cloudflare-workers-ai-system-one-U3AH4ODQ.js +2 -0
  25. package/dist/bundle/chunks/codemode-worker.js +101 -5
  26. package/dist/bundle/chunks/{easter-egg-3d-6YCNESG3.js → easter-egg-3d-TQFQS342.js} +1 -1
  27. package/dist/bundle/chunks/{execute-JRUOLYDA.js → execute-7IRB5TUX.js} +110 -8
  28. package/dist/bundle/chunks/github-copilot.js +1 -1
  29. package/dist/bundle/chunks/{google-generative-ai-XMFHGQC5.js → google-generative-ai-X2VFFS4I.js} +1 -1
  30. package/dist/bundle/chunks/{google-vertex-4XRRNP74.js → google-vertex-KMGHFTSP.js} +1 -1
  31. package/dist/bundle/chunks/image-resize-worker.js +1 -1
  32. package/dist/bundle/chunks/{llama-cpp-classify-XT44PI3C.js → llama-cpp-classify-FYN623RH.js} +2 -2
  33. package/dist/bundle/chunks/{mistral-conversations-Q22M3IBG.js → mistral-conversations-PRZLM26U.js} +2 -2
  34. package/dist/bundle/chunks/openai-chatgpt.js +1 -1
  35. package/dist/bundle/chunks/{openai-codex-responses-4MYQSO5E.js → openai-codex-responses-ZSLJZYOE.js} +2 -2
  36. package/dist/bundle/chunks/openai-codex.js +1 -1
  37. package/dist/bundle/chunks/{openai-completions-OSRCWAAA.js → openai-completions-WWQW7S3V.js} +1 -1
  38. package/dist/bundle/chunks/openai-decisions-QLZKZLAK.js +5 -0
  39. package/dist/bundle/chunks/{openai-responses-UHF3DHNG.js → openai-responses-KV7QHJY6.js} +1 -1
  40. package/dist/bundle/chunks/{openrouter-images-ZVXWCSIM.js → openrouter-images-DUPG5DZO.js} +1 -1
  41. package/dist/bundle/chunks/{pi-messages-TM5SOOIW.js → pi-messages-YHKPQIHC.js} +1 -1
  42. package/dist/bundle/chunks/{runtime-NW7GPWKS.js → runtime-ROGZ2EXI.js} +1 -1
  43. package/dist/bundle/chunks/typesafe-system-one-B4NNEG67.js +2 -0
  44. package/dist/bundle/chunks/{virtual-modules-CFHJO5SY.js → virtual-modules-4YN2GNVW.js} +1 -1
  45. package/dist/bundle/cli-runtime.js +1 -1
  46. package/dist/bundle/index.js +1 -1
  47. package/dist/bundle/rpc-entry.js +1 -1
  48. package/dist/cli/args.d.ts +2 -1
  49. package/dist/cli/args.d.ts.map +1 -1
  50. package/dist/cli/args.js +24 -5
  51. package/dist/cli/args.js.map +1 -1
  52. package/dist/core/agent-session.d.ts +33 -4
  53. package/dist/core/agent-session.d.ts.map +1 -1
  54. package/dist/core/agent-session.js +59 -21
  55. package/dist/core/agent-session.js.map +1 -1
  56. package/dist/core/bash-executor.d.ts.map +1 -1
  57. package/dist/core/bash-executor.js +40 -45
  58. package/dist/core/bash-executor.js.map +1 -1
  59. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  60. package/dist/core/export-html/tool-renderer.js +2 -0
  61. package/dist/core/export-html/tool-renderer.js.map +1 -1
  62. package/dist/core/extensions/types.d.ts +13 -0
  63. package/dist/core/extensions/types.d.ts.map +1 -1
  64. package/dist/core/extensions/types.js.map +1 -1
  65. package/dist/core/mcp-servers.d.ts +8 -0
  66. package/dist/core/mcp-servers.d.ts.map +1 -1
  67. package/dist/core/mcp-servers.js +30 -7
  68. package/dist/core/mcp-servers.js.map +1 -1
  69. package/dist/core/model-runtime.d.ts.map +1 -1
  70. package/dist/core/model-runtime.js +2 -1
  71. package/dist/core/model-runtime.js.map +1 -1
  72. package/dist/core/nested-tool-calls.d.ts +1 -0
  73. package/dist/core/nested-tool-calls.d.ts.map +1 -1
  74. package/dist/core/nested-tool-calls.js +1 -0
  75. package/dist/core/nested-tool-calls.js.map +1 -1
  76. package/dist/core/resource-loader.d.ts +3 -0
  77. package/dist/core/resource-loader.d.ts.map +1 -1
  78. package/dist/core/resource-loader.js +4 -3
  79. package/dist/core/resource-loader.js.map +1 -1
  80. package/dist/core/sdk.d.ts +13 -4
  81. package/dist/core/sdk.d.ts.map +1 -1
  82. package/dist/core/sdk.js +19 -6
  83. package/dist/core/sdk.js.map +1 -1
  84. package/dist/core/settings-manager.d.ts +13 -0
  85. package/dist/core/settings-manager.d.ts.map +1 -1
  86. package/dist/core/settings-manager.js +37 -13
  87. package/dist/core/settings-manager.js.map +1 -1
  88. package/dist/core/skills.d.ts +4 -1
  89. package/dist/core/skills.d.ts.map +1 -1
  90. package/dist/core/skills.js +6 -1
  91. package/dist/core/skills.js.map +1 -1
  92. package/dist/core/system-prompt.d.ts +6 -0
  93. package/dist/core/system-prompt.d.ts.map +1 -1
  94. package/dist/core/system-prompt.js +9 -4
  95. package/dist/core/system-prompt.js.map +1 -1
  96. package/dist/core/tools/read.d.ts +13 -0
  97. package/dist/core/tools/read.d.ts.map +1 -1
  98. package/dist/core/tools/read.js +18 -1
  99. package/dist/core/tools/read.js.map +1 -1
  100. package/dist/core/tools/renderers/bash.d.ts.map +1 -1
  101. package/dist/core/tools/renderers/bash.js +8 -3
  102. package/dist/core/tools/renderers/bash.js.map +1 -1
  103. package/dist/core/tools/renderers/edit.d.ts.map +1 -1
  104. package/dist/core/tools/renderers/edit.js +5 -4
  105. package/dist/core/tools/renderers/edit.js.map +1 -1
  106. package/dist/extensions/codemode/execute.d.ts.map +1 -1
  107. package/dist/extensions/codemode/execute.js +65 -8
  108. package/dist/extensions/codemode/execute.js.map +1 -1
  109. package/dist/extensions/codemode/index.d.ts.map +1 -1
  110. package/dist/extensions/codemode/index.js +1 -0
  111. package/dist/extensions/codemode/index.js.map +1 -1
  112. package/dist/extensions/codemode/tool.d.ts +9 -2
  113. package/dist/extensions/codemode/tool.d.ts.map +1 -1
  114. package/dist/extensions/codemode/tool.js +14 -6
  115. package/dist/extensions/codemode/tool.js.map +1 -1
  116. package/dist/extensions/llama/provider.d.ts.map +1 -1
  117. package/dist/extensions/llama/provider.js +34 -11
  118. package/dist/extensions/llama/provider.js.map +1 -1
  119. package/dist/extensions/mcp/cli.js +17 -23
  120. package/dist/extensions/mcp/cli.js.map +1 -1
  121. package/dist/extensions/mcp/index.d.ts.map +1 -1
  122. package/dist/extensions/mcp/index.js +165 -64
  123. package/dist/extensions/mcp/index.js.map +1 -1
  124. package/dist/extensions/mcp/oauth.d.ts +5 -3
  125. package/dist/extensions/mcp/oauth.d.ts.map +1 -1
  126. package/dist/extensions/mcp/oauth.js +36 -10
  127. package/dist/extensions/mcp/oauth.js.map +1 -1
  128. package/dist/extensions/mcp/resources.d.ts +3 -4
  129. package/dist/extensions/mcp/resources.d.ts.map +1 -1
  130. package/dist/extensions/mcp/resources.js +3 -4
  131. package/dist/extensions/mcp/resources.js.map +1 -1
  132. package/dist/extensions/mcp/runtime.d.ts +4 -1
  133. package/dist/extensions/mcp/runtime.d.ts.map +1 -1
  134. package/dist/extensions/mcp/runtime.js +22 -5
  135. package/dist/extensions/mcp/runtime.js.map +1 -1
  136. package/dist/extensions/mcp/tools.d.ts +2 -1
  137. package/dist/extensions/mcp/tools.d.ts.map +1 -1
  138. package/dist/extensions/mcp/tools.js +2 -1
  139. package/dist/extensions/mcp/tools.js.map +1 -1
  140. package/dist/extensions/mcp/ui.d.ts +3 -3
  141. package/dist/extensions/mcp/ui.d.ts.map +1 -1
  142. package/dist/extensions/mcp/ui.js +10 -2
  143. package/dist/extensions/mcp/ui.js.map +1 -1
  144. package/dist/main.d.ts.map +1 -1
  145. package/dist/main.js +1 -0
  146. package/dist/main.js.map +1 -1
  147. package/dist/modes/interactive/components/bash-execution.d.ts +5 -1
  148. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  149. package/dist/modes/interactive/components/bash-execution.js +17 -14
  150. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  151. package/dist/modes/interactive/components/branch-summary-message.d.ts +2 -1
  152. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  153. package/dist/modes/interactive/components/branch-summary-message.js +5 -2
  154. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  155. package/dist/modes/interactive/components/compaction-summary-message.d.ts +2 -1
  156. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  157. package/dist/modes/interactive/components/compaction-summary-message.js +5 -2
  158. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  159. package/dist/modes/interactive/components/custom-entry.d.ts +3 -1
  160. package/dist/modes/interactive/components/custom-entry.d.ts.map +1 -1
  161. package/dist/modes/interactive/components/custom-entry.js +8 -2
  162. package/dist/modes/interactive/components/custom-entry.js.map +1 -1
  163. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  164. package/dist/modes/interactive/components/custom-message.js +1 -0
  165. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  166. package/dist/modes/interactive/components/settings-selector.js +1 -1
  167. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  168. package/dist/modes/interactive/components/skill-invocation-message.d.ts +2 -1
  169. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  170. package/dist/modes/interactive/components/skill-invocation-message.js +5 -2
  171. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  172. package/dist/modes/interactive/components/tool-execution.d.ts +5 -0
  173. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  174. package/dist/modes/interactive/components/tool-execution.js +10 -0
  175. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  176. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  177. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  178. package/dist/modes/interactive/interactive-mode.js +50 -29
  179. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  180. package/dist/modes/interactive/program-status-reporter.d.ts +34 -0
  181. package/dist/modes/interactive/program-status-reporter.d.ts.map +1 -0
  182. package/dist/modes/interactive/program-status-reporter.js +110 -0
  183. package/dist/modes/interactive/program-status-reporter.js.map +1 -0
  184. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  185. package/dist/modes/interactive/theme/theme.js +1 -0
  186. package/dist/modes/interactive/theme/theme.js.map +1 -1
  187. package/dist/package-manager-cli.d.ts.map +1 -1
  188. package/dist/package-manager-cli.js +24 -0
  189. package/dist/package-manager-cli.js.map +1 -1
  190. package/dist/utils/ansi.d.ts +8 -0
  191. package/dist/utils/ansi.d.ts.map +1 -1
  192. package/dist/utils/ansi.js +30 -11
  193. package/dist/utils/ansi.js.map +1 -1
  194. package/dist/utils/clipboard.d.ts.map +1 -1
  195. package/dist/utils/clipboard.js +12 -11
  196. package/dist/utils/clipboard.js.map +1 -1
  197. package/dist/utils/image-resize-core.d.ts +12 -0
  198. package/dist/utils/image-resize-core.d.ts.map +1 -1
  199. package/dist/utils/image-resize-core.js +5 -0
  200. package/dist/utils/image-resize-core.js.map +1 -1
  201. package/dist/utils/image-resize-worker.js +3 -2
  202. package/dist/utils/image-resize-worker.js.map +1 -1
  203. package/dist/utils/image-resize.d.ts.map +1 -1
  204. package/dist/utils/image-resize.js +9 -10
  205. package/dist/utils/image-resize.js.map +1 -1
  206. package/dist/utils/syntax-highlight.d.ts.map +1 -1
  207. package/dist/utils/syntax-highlight.js +6 -1
  208. package/dist/utils/syntax-highlight.js.map +1 -1
  209. package/docs/cli.md +18 -6
  210. package/docs/codemode.md +26 -5
  211. package/docs/environment-variables.md +1 -0
  212. package/docs/extensions.md +1 -1
  213. package/docs/json.md +3 -3
  214. package/docs/llama-cpp.md +8 -1
  215. package/docs/mcp.md +3 -1
  216. package/docs/message-types.md +22 -4
  217. package/docs/models.md +4 -1
  218. package/docs/providers.md +27 -0
  219. package/docs/settings.md +3 -3
  220. package/docs/terminal-setup.md +14 -0
  221. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  222. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  223. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  224. package/examples/extensions/gondolin/package-lock.json +2 -2
  225. package/examples/extensions/gondolin/package.json +1 -1
  226. package/examples/extensions/sandbox/package-lock.json +2 -2
  227. package/examples/extensions/sandbox/package.json +1 -1
  228. package/examples/extensions/with-deps/package-lock.json +2 -2
  229. package/examples/extensions/with-deps/package.json +1 -1
  230. package/package.json +12 -12
  231. package/dist/bundle/chunks/chunk-A3JYRWB6.js +0 -2
  232. package/dist/bundle/chunks/chunk-C47CLQZK.js +0 -2
  233. package/dist/bundle/chunks/chunk-HQHD6PZI.js +0 -2
  234. package/dist/bundle/chunks/chunk-KJUDFE6Y.js +0 -11
  235. package/dist/bundle/chunks/chunk-PSQJVYXD.js +0 -82
  236. package/dist/bundle/chunks/cloudflare-workers-ai-system-one-NM3JRRWQ.js +0 -2
  237. package/dist/bundle/chunks/typesafe-system-one-PRR5K6CI.js +0 -2
@@ -197,8 +197,6 @@ export function createMcpExtension(options = {}) {
197
197
  let projectConfig;
198
198
  /** Registered servers that `mcp.json` overrides, shown in `/mcp`. */
199
199
  let overridden = [];
200
- /** Between session_start and session_shutdown. Registrations before that are read on session_start. */
201
- let sessionActive = false;
202
200
  let autoEnableCodemode = true;
203
201
  /** Whether the "codemode tools unreachable" warning was shown since the session started. */
204
202
  let warnedUnreachable = false;
@@ -206,8 +204,6 @@ export function createMcpExtension(options = {}) {
206
204
  /** Whether a prompt already waited for the startup connections since the session started. */
207
205
  let waitedForStartup = false;
208
206
  const startupWaitMs = options.startupWaitMs ?? DEFAULT_STARTUP_WAIT_MS;
209
- /** Bumped on every session start and shutdown so a runtime load that resolves late is dropped. */
210
- let generation = 0;
211
207
  /** Working directory of the session, for stdio servers. */
212
208
  let sessionCwd = process.cwd();
213
209
  let credentials = options.credentials;
@@ -219,6 +215,23 @@ export function createMcpExtension(options = {}) {
219
215
  ((entry, patch) => updateMcpServerConfig(entry.override ?? entry.source, entry.name, patch, {
220
216
  override: entry.override !== undefined,
221
217
  }));
218
+ /**
219
+ * Lifetime of the current session: aborted on session_shutdown, and before the first session_start
220
+ * (registrations before that are read on session_start). Work captures `session.signal` when it
221
+ * starts and drops late results once it aborted, for example a runtime load that resolves after
222
+ * shutdown. Work that outlives the command that started it also stops on the signal and is
223
+ * registered with `track`, so shutdown can wait for its cleanup.
224
+ */
225
+ let session = new AbortController();
226
+ session.abort();
227
+ /** Work registered with `track`: manager actions opening or closing connections, and sign-ins. */
228
+ const backgroundActions = new Set();
229
+ const track = (work) => {
230
+ const task = work.then(() => undefined, () => undefined);
231
+ backgroundActions.add(task);
232
+ void task.finally(() => backgroundActions.delete(task));
233
+ return work;
234
+ };
222
235
  const listeners = new Set();
223
236
  const emitChange = () => {
224
237
  for (const listener of listeners)
@@ -301,8 +314,22 @@ export function createMcpExtension(options = {}) {
301
314
  exposure: getMcpToolExposure(entry.config, tool.name),
302
315
  namespace,
303
316
  timeoutMs: connection.timeoutMs,
304
- getClient: async () => connection,
305
- readableResources: () => resourceServers().includes(connection),
317
+ getClient: async () => {
318
+ // A prepared call can outlive this definition's connection while the readiness
319
+ // hook waits for a disable/re-enable. Resolve the replacement at execution time.
320
+ const current = findServer(server);
321
+ if (!current || !isEnabled(current))
322
+ throw new Error(`MCP server "${server}" is disabled.`);
323
+ const client = current.connection;
324
+ if (!client)
325
+ throw new Error(`MCP server "${server}" is still starting.`);
326
+ if (getMcpToolExposure(current.entry.config, tool.name) === "hidden" ||
327
+ (client.state === "connected" && !client.tools.some((offered) => offered.name === tool.name))) {
328
+ throw new Error(`MCP tool "${server}/${tool.name}" is no longer available.`);
329
+ }
330
+ return client;
331
+ },
332
+ readableResources: () => resourceServers().some((current) => current.name === server),
306
333
  });
307
334
  definitions.set(definition.name, definition);
308
335
  pi.registerTool(definition);
@@ -423,8 +450,10 @@ export function createMcpExtension(options = {}) {
423
450
  ensureDiscoveryActive(ctx);
424
451
  };
425
452
  /** Create the server's connection, loading the MCP runtime on first use. */
426
- const createConnection = async (server) => {
453
+ const createConnection = async (server, isCurrent) => {
427
454
  const runtime = await loadMcpRuntime();
455
+ if (!isCurrent())
456
+ return undefined;
428
457
  const connection = new runtime.McpServerConnection({
429
458
  entry: server.entry,
430
459
  cwd: sessionCwd,
@@ -432,8 +461,16 @@ export function createMcpExtension(options = {}) {
432
461
  credentials: getCredentials(runtime),
433
462
  providerToken: async (provider) => modelRegistry?.getApiKeyForProvider(provider),
434
463
  log: getServerLog(runtime),
435
- onTools: registerTools,
436
- onChange: onConnectionChange,
464
+ onTools: (connection) => {
465
+ if (isCurrent())
466
+ registerTools(connection);
467
+ },
468
+ onChange: (connection) => {
469
+ if (isCurrent())
470
+ onConnectionChange(connection);
471
+ else
472
+ tokensAtSignIn.delete(connection);
473
+ },
437
474
  });
438
475
  server.connection = connection;
439
476
  emitChange();
@@ -441,37 +478,48 @@ export function createMcpExtension(options = {}) {
441
478
  };
442
479
  /**
443
480
  * Connect the server in the background. `server.ready` settles when it connected or failed;
444
- * failures show in its state. It starts after `after`; `isCurrent` stops it when the session ended
481
+ * failures show in its state. It starts after `after`, unless disabled, replaced, or shut down
445
482
  * meanwhile. The returned promise rejects when the MCP runtime cannot be loaded.
446
483
  */
447
- const startConnection = (server, isCurrent, after) => {
484
+ const startConnection = (server, after) => {
485
+ const { signal } = session;
486
+ const attempt = Symbol();
487
+ server.attempt = attempt;
488
+ const isCurrent = () => !signal.aborted && server.attempt === attempt && isEnabled(server) && servers.includes(server);
448
489
  const ready = (async () => {
449
490
  await after;
450
491
  if (!isCurrent())
451
492
  return;
452
- const connection = await createConnection(server);
453
- if (!isCurrent())
493
+ const connection = await createConnection(server, isCurrent);
494
+ if (!connection || !isCurrent())
454
495
  return;
455
496
  await connection.getClient().catch(() => undefined);
456
497
  })();
457
498
  server.ready = ready.catch(() => undefined);
458
499
  return ready;
459
500
  };
460
- /** Wait for servers still connecting, until they settle or `signal` aborts. */
501
+ /** Wait for the latest connection attempts, including ones queued while waiting, or an abort. */
461
502
  const waitForServers = async (waiting, signal) => {
462
- const ready = waiting.flatMap((server) => (server.ready ? [server.ready] : []));
463
- if (ready.length === 0 || signal?.aborted)
464
- return;
465
- let onAbort;
466
- await Promise.race([
467
- Promise.all(ready),
468
- new Promise((resolve) => {
469
- onAbort = () => resolve();
470
- signal?.addEventListener("abort", onAbort, { once: true });
471
- }),
472
- ]);
473
- if (onAbort)
474
- signal?.removeEventListener("abort", onAbort);
503
+ for (;;) {
504
+ const ready = waiting.flatMap((server) => isEnabled(server) && servers.includes(server) && server.ready ? [{ server, ready: server.ready }] : []);
505
+ if (ready.length === 0 || signal?.aborted)
506
+ return;
507
+ let onAbort;
508
+ await Promise.race([
509
+ Promise.all(ready.map((attempt) => attempt.ready)),
510
+ new Promise((resolve) => {
511
+ onAbort = () => resolve();
512
+ signal?.addEventListener("abort", onAbort, { once: true });
513
+ }),
514
+ ]);
515
+ if (onAbort)
516
+ signal?.removeEventListener("abort", onAbort);
517
+ if (signal?.aborted ||
518
+ ready.every((attempt) => !isEnabled(attempt.server) ||
519
+ !servers.includes(attempt.server) ||
520
+ attempt.ready === attempt.server.ready))
521
+ return;
522
+ }
475
523
  };
476
524
  /**
477
525
  * One message for everything that needs the user after startup, or only for `only`, servers
@@ -506,20 +554,23 @@ export function createMcpExtension(options = {}) {
506
554
  server.entry = { ...entry, config: { ...entry.config, ...patch } };
507
555
  return undefined;
508
556
  };
509
- const signIn = async (server, prompt) => {
557
+ /** Sign in, then reconnect. Returns an error message on failure. `cancel` or the session's end stops the sign-in. */
558
+ const signIn = async (server, prompt, cancel) => {
510
559
  const connection = server.connection;
511
560
  const url = connection?.oauthUrl;
512
561
  if (!connection || !url)
513
562
  return `MCP server "${server.entry.name}" does not use OAuth.`;
514
563
  const runtime = await loadMcpRuntime();
564
+ const signal = cancel ? AbortSignal.any([session.signal, cancel]) : session.signal;
515
565
  try {
516
- await runtime.signInMcpServer({
566
+ await track(runtime.signInMcpServer({
517
567
  serverUrl: url,
518
568
  store: getCredentials(runtime).forServer(server.entry.name, url),
519
569
  settings: connection.oauthSettings(),
520
570
  challenge: connection.challenge,
521
571
  prompt,
522
- });
572
+ signal,
573
+ }));
523
574
  }
524
575
  catch (error) {
525
576
  if (error instanceof runtime.McpSignInCancelledError)
@@ -549,8 +600,14 @@ export function createMcpExtension(options = {}) {
549
600
  const connection = server.connection;
550
601
  if (!connection)
551
602
  return `MCP server "${server.entry.name}" is disabled.`;
552
- try {
603
+ const previous = server.ready;
604
+ const ready = (async () => {
605
+ await previous;
553
606
  await connection.reconnect();
607
+ })();
608
+ server.ready = ready.catch(() => undefined);
609
+ try {
610
+ await ready;
554
611
  return undefined;
555
612
  }
556
613
  catch (error) {
@@ -564,13 +621,26 @@ export function createMcpExtension(options = {}) {
564
621
  return failed;
565
622
  if (!enabled) {
566
623
  const connection = server.connection;
624
+ server.attempt = undefined;
567
625
  server.connection = undefined;
626
+ if (connection) {
627
+ // A reconnect may already have detached its old client while awaiting transport
628
+ // shutdown. Closing the connection alone does not wait for that reconnect.
629
+ server.closing = Promise.all([connection.close(), server.ready]).then(() => undefined);
630
+ }
631
+ const closing = server.closing;
568
632
  hideTools(server.entry.name);
569
633
  emitChange();
570
- await connection?.close();
634
+ try {
635
+ await closing;
636
+ }
637
+ finally {
638
+ if (server.closing === closing)
639
+ server.closing = undefined;
640
+ }
571
641
  return undefined;
572
642
  }
573
- await startConnection(server, () => true);
643
+ await startConnection(server, server.closing);
574
644
  return undefined;
575
645
  };
576
646
  const setExposure = (server, exposure) => {
@@ -718,11 +788,13 @@ export function createMcpExtension(options = {}) {
718
788
  return undefined;
719
789
  return setExposure(server, choice);
720
790
  };
721
- /** Sign in with the manager view's sign-in screen, which shows the URL with a copy key. */
791
+ /** Sign in with the manager view's sign-in screen, which shows the URL with a copy key and cancels with Esc. */
722
792
  const signInWithUi = (ui, server) => {
723
793
  const title = `Sign in to ${server.entry.name}`;
794
+ const cancel = new AbortController();
795
+ const status = (message) => ui.status(title, message, () => cancel.abort());
724
796
  let authorizationUrl = "";
725
- ui.status(title, "Contacting the authorization server…");
797
+ status("Contacting the authorization server…");
726
798
  return signIn(server, {
727
799
  showAuthorizationUrl: (url) => {
728
800
  authorizationUrl = url.href;
@@ -730,22 +802,38 @@ export function createMcpExtension(options = {}) {
730
802
  },
731
803
  promptForRedirectUrl: async (signal) => {
732
804
  const value = await ui.redirectUrl(title, authorizationUrl, signal);
733
- ui.status(title, "Connecting…");
805
+ status("Connecting…");
734
806
  return value;
735
807
  },
736
- });
808
+ }, cancel.signal);
809
+ };
810
+ /** Keep the subscribed menu usable while a connection opens or closes. */
811
+ const runInBackground = (ctx, server, operation) => {
812
+ const ready = operation();
813
+ const attempt = server.attempt;
814
+ const { signal } = session;
815
+ const finish = (message) => {
816
+ if (signal.aborted || server.attempt !== attempt || !servers.includes(server))
817
+ return;
818
+ server.message = message;
819
+ ensureDiscoveryActive(ctx);
820
+ emitChange();
821
+ };
822
+ void track(ready.then(finish, (error) => finish(errorMessage(error))));
737
823
  };
738
824
  const runAction = async (ui, ctx, server, action) => {
739
- const { name } = server.entry;
825
+ const { signal } = session;
740
826
  let message;
741
827
  switch (action) {
742
828
  case "signin":
743
829
  message = await signInWithUi(ui, server);
744
830
  break;
745
831
  case "reconnect":
746
- // A failure shows as the connection's state and error.
747
- ui.status(`MCP server ${name}`, "Reconnecting…");
748
- await reconnect(server);
832
+ // Connection state and error already report failures, including required sign-ins.
833
+ runInBackground(ctx, server, async () => {
834
+ await reconnect(server);
835
+ return undefined;
836
+ });
749
837
  break;
750
838
  case "signout":
751
839
  await signOut(server);
@@ -761,11 +849,13 @@ export function createMcpExtension(options = {}) {
761
849
  case "enable-project":
762
850
  case "disable-project": {
763
851
  const enable = action.startsWith("enable");
764
- ui.status(`MCP server ${name}`, enable ? "Connecting…" : "Disconnecting…");
765
- message = await setEnabled(server, enable, action.endsWith("-project"));
852
+ runInBackground(ctx, server, () => setEnabled(server, enable, action.endsWith("-project")));
766
853
  break;
767
854
  }
768
855
  }
856
+ // The session ended meanwhile (for example during a sign-in), which made ctx stale.
857
+ if (signal.aborted)
858
+ return;
769
859
  server.message = message;
770
860
  ensureDiscoveryActive(ctx);
771
861
  emitChange();
@@ -849,6 +939,7 @@ export function createMcpExtension(options = {}) {
849
939
  ctx.ui.notify(`Signing in to MCP server "${name}" requires interactive mode.`, "error");
850
940
  return;
851
941
  }
942
+ const { signal } = session;
852
943
  let failure;
853
944
  if (ctx.mode === "tui") {
854
945
  await showMcpManager(ctx, async (ui) => {
@@ -864,6 +955,9 @@ export function createMcpExtension(options = {}) {
864
955
  promptForRedirectUrl: (signal) => ctx.ui.input(`Waiting for sign-in to "${name}". If the browser cannot reach this machine, paste the URL it was redirected to.`, "http://127.0.0.1:.../callback?code=...", { signal }),
865
956
  });
866
957
  }
958
+ // The session ended meanwhile, which cancelled the sign-in and made ctx stale.
959
+ if (signal.aborted)
960
+ return;
867
961
  if (failure) {
868
962
  ctx.ui.notify(failure, failure === "Sign-in cancelled." ? "info" : "error");
869
963
  return;
@@ -880,8 +974,8 @@ export function createMcpExtension(options = {}) {
880
974
  waitedForStartup = false;
881
975
  sessionCwd = ctx.cwd;
882
976
  modelRegistry = ctx.modelRegistry;
883
- const current = ++generation;
884
- sessionActive = true;
977
+ session = new AbortController();
978
+ const { signal } = session;
885
979
  configuredEntries = loaded.servers;
886
980
  const registered = registeredServers();
887
981
  overridden = registered.overridden;
@@ -898,10 +992,9 @@ export function createMcpExtension(options = {}) {
898
992
  // The MCP client loads only now, so sessions without servers never pay for it. Waiting one
899
993
  // event loop turn lets the first render happen before loading and connecting.
900
994
  const runtime = new Promise((resolve) => setImmediate(resolve)).then(() => loadMcpRuntime());
901
- const isCurrent = () => current === generation;
902
- pending = Promise.all(enabled.map((server) => startConnection(server, isCurrent, runtime)))
995
+ pending = Promise.all(enabled.map((server) => startConnection(server, runtime)))
903
996
  .then(() => {
904
- if (isCurrent())
997
+ if (!signal.aborted)
905
998
  reportProblems(ctx);
906
999
  })
907
1000
  .catch((error) => {
@@ -947,22 +1040,29 @@ export function createMcpExtension(options = {}) {
947
1040
  });
948
1041
  // A codemode script waits for the servers it names, or for every server when it searches or
949
1042
  // enumerates tools, so their tools are registered before the script runs. tool_search and the
950
- // resource tools reach every server, so they wait for all of them.
1043
+ // resource tools reach every server, so they wait for all of them. Direct MCP calls wait only
1044
+ // for their own server, including a reconnect still closing its old transport.
951
1045
  pi.on("tool_call", async (event, ctx) => {
952
1046
  const tool = pi.getAllTools().find((candidate) => candidate.name === event.toolName);
953
1047
  if (!tool)
954
1048
  return;
955
- const pendingServers = servers.filter((server) => isEnabled(server) && server.connection?.state !== "connected" && server.ready);
956
- if (pendingServers.length === 0)
1049
+ // Readiness can be pending while the display state is still connected: reconnect first
1050
+ // closes the old transport, then opens a new one. Settled promises return immediately.
1051
+ const readyServers = servers.filter((server) => isEnabled(server) && server.ready);
1052
+ if (readyServers.length === 0)
957
1053
  return;
958
1054
  let waiting = [];
959
1055
  if (isCodemodeTool(tool)) {
960
1056
  const { code } = event.input;
961
1057
  const source = typeof code === "string" ? code : "";
962
- waiting = pendingServers.filter((server) => scriptNeedsServer(source, server.entry.name));
1058
+ waiting = readyServers.filter((server) => scriptNeedsServer(source, server.entry.name));
963
1059
  }
964
1060
  else if (isToolSearchTool(tool) || RESOURCE_TOOL_NAMES.has(tool.name)) {
965
- waiting = pendingServers;
1061
+ waiting = readyServers;
1062
+ }
1063
+ else {
1064
+ const owner = toolOwners.get(event.toolName)?.split("\0", 1)[0];
1065
+ waiting = readyServers.filter((server) => server.entry.name === owner);
966
1066
  }
967
1067
  await waitForServers(waiting, ctx.signal);
968
1068
  });
@@ -973,9 +1073,9 @@ export function createMcpExtension(options = {}) {
973
1073
  });
974
1074
  // Servers registered or unregistered during the session connect or disconnect right away.
975
1075
  pi.on("mcp_servers_change", async (_event, ctx) => {
976
- if (!sessionActive)
1076
+ const { signal } = session;
1077
+ if (signal.aborted)
977
1078
  return;
978
- const current = generation;
979
1079
  const registered = registeredServers();
980
1080
  overridden = registered.overridden;
981
1081
  const next = new Map(registered.servers.map((server) => [server.entry.name, server]));
@@ -991,11 +1091,11 @@ export function createMcpExtension(options = {}) {
991
1091
  ensureDiscoveryActive(ctx);
992
1092
  await Promise.all(removed.map((server) => server.connection?.close()));
993
1093
  const connecting = added.filter(isEnabled);
994
- if (current !== generation || connecting.length === 0)
1094
+ if (signal.aborted || connecting.length === 0)
995
1095
  return;
996
1096
  try {
997
- await Promise.all(connecting.map((server) => startConnection(server, () => current === generation)));
998
- if (current !== generation) {
1097
+ await Promise.all(connecting.map((server) => startConnection(server)));
1098
+ if (signal.aborted) {
999
1099
  await Promise.all(connecting.map((server) => server.connection?.close()));
1000
1100
  return;
1001
1101
  }
@@ -1004,17 +1104,16 @@ export function createMcpExtension(options = {}) {
1004
1104
  ctx.ui.notify(`MCP failed to load: ${errorMessage(error)}`, "error");
1005
1105
  return;
1006
1106
  }
1007
- if (current !== generation)
1107
+ if (signal.aborted)
1008
1108
  return;
1009
1109
  reportProblems(ctx, connecting);
1010
1110
  });
1011
1111
  pi.on("session_shutdown", async () => {
1012
- sessionActive = false;
1013
- generation++;
1112
+ session.abort();
1014
1113
  const closing = connections();
1015
1114
  servers = [];
1016
1115
  emitChange();
1017
- await Promise.all(closing.map((connection) => connection.close()));
1116
+ await Promise.all([...backgroundActions, ...closing.map((connection) => connection.close())]);
1018
1117
  });
1019
1118
  pi.registerCommand("mcp", {
1020
1119
  description: "Manage MCP servers: sign in, reconnect, enable or disable, and change exposure",
@@ -1040,19 +1139,21 @@ export function createMcpExtension(options = {}) {
1040
1139
  return items.length > 0 ? items : null;
1041
1140
  },
1042
1141
  handler: async (args, ctx) => {
1043
- await pending;
1044
1142
  const [action, name, ...extra] = args.trim().split(/\s+/).filter(Boolean);
1045
1143
  if (action === undefined) {
1046
1144
  if (ctx.mode === "tui")
1047
1145
  await showMcpManager(ctx, (ui) => manage(ui, ctx));
1048
- else
1146
+ else {
1147
+ await pending;
1049
1148
  ctx.ui.notify(formatStatus(), "info");
1149
+ }
1050
1150
  return;
1051
1151
  }
1052
1152
  if (extra.length > 0) {
1053
1153
  ctx.ui.notify(MCP_USAGE, "warning");
1054
1154
  return;
1055
1155
  }
1156
+ await pending;
1056
1157
  switch (action) {
1057
1158
  case "login": {
1058
1159
  const server = await pickServer(name, ctx, oauthPick);