one-code-extension 0.4.1 → 0.6.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 (439) hide show
  1. package/README.md +16 -5
  2. package/agents/general-purpose.md +2 -2
  3. package/docs/guide/README.md +2 -1
  4. package/docs/guide/bring-your-claude-code-setup.md +60 -9
  5. package/docs/guide/configuration.md +48 -14
  6. package/docs/guide/differences-from-claude-code.md +40 -28
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +26 -4
  10. package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
  11. package/docs/guide/providers-and-models.md +137 -54
  12. package/docs/guide/reference.md +27 -15
  13. package/docs/guide/sessions-and-context.md +69 -16
  14. package/docs/guide/skills-plugins-and-mcp.md +57 -20
  15. package/docs/guide/subagents-and-workflows.md +95 -29
  16. package/docs/guide/tasks-and-background-work.md +101 -26
  17. package/docs/guide/terminal-interface.md +24 -12
  18. package/docs/guide/tools.md +46 -5
  19. package/docs/guide/troubleshooting.md +13 -9
  20. package/docs/guide/windows.md +58 -31
  21. package/extensions/artifacts/gallery.ts +227 -0
  22. package/extensions/artifacts/index.ts +288 -0
  23. package/extensions/artifacts/store.ts +570 -0
  24. package/extensions/artifacts/viewer.ts +199 -0
  25. package/extensions/ask-user/description.ts +67 -0
  26. package/extensions/ask-user/dialogs.ts +80 -0
  27. package/extensions/ask-user/index.ts +37 -33
  28. package/extensions/ask-user/questions.ts +32 -7
  29. package/extensions/ask-user/widget.ts +45 -25
  30. package/extensions/auto-mode/actions.ts +6 -0
  31. package/extensions/auto-mode/cache.ts +88 -0
  32. package/extensions/auto-mode/classifier-prompt.ts +5 -4
  33. package/extensions/auto-mode/classifier.ts +290 -65
  34. package/extensions/auto-mode/config.ts +72 -20
  35. package/extensions/auto-mode/decision-log.ts +2 -0
  36. package/extensions/auto-mode/defaults.ts +1 -1
  37. package/extensions/auto-mode/denials.ts +124 -0
  38. package/extensions/auto-mode/git-checkout-programs.ts +187 -0
  39. package/extensions/auto-mode/git-status-meta.ts +159 -0
  40. package/extensions/auto-mode/history.ts +189 -0
  41. package/extensions/auto-mode/instructions.ts +58 -52
  42. package/extensions/auto-mode/model-select.ts +159 -134
  43. package/extensions/auto-mode/paths.ts +57 -5
  44. package/extensions/auto-mode/pause.ts +57 -2
  45. package/extensions/auto-mode/prompt.ts +37 -21
  46. package/extensions/auto-mode/read-only-options.ts +724 -0
  47. package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
  48. package/extensions/auto-mode/rules.ts +18 -1
  49. package/extensions/auto-mode/safety-floor.ts +361 -18
  50. package/extensions/auto-mode/setup-run.ts +20 -14
  51. package/extensions/auto-mode/shell-analysis.ts +764 -490
  52. package/extensions/auto-mode/shell-parse.ts +885 -0
  53. package/extensions/auto-mode/system-note.ts +33 -0
  54. package/extensions/auto-mode/transcript.ts +80 -51
  55. package/extensions/background/cron.ts +642 -0
  56. package/extensions/background/index.ts +614 -248
  57. package/extensions/background/loop-fire.ts +173 -0
  58. package/extensions/background/loop-preamble.ts +59 -0
  59. package/extensions/background/loop-skill.ts +163 -0
  60. package/extensions/background/monitor-batch.ts +53 -0
  61. package/extensions/background/noop-fold.ts +128 -0
  62. package/extensions/background/switch-guard.ts +56 -0
  63. package/extensions/background/wakeup.ts +178 -55
  64. package/extensions/bash/background.ts +53 -17
  65. package/extensions/bash/child.ts +7 -0
  66. package/extensions/bash/description.ts +188 -0
  67. package/extensions/bash/guards.ts +15 -67
  68. package/extensions/bash/index.ts +66 -35
  69. package/extensions/branding/compaction-keep.ts +63 -0
  70. package/extensions/branding/declined.ts +22 -0
  71. package/extensions/branding/index.ts +133 -9
  72. package/extensions/branding/prompt-editor.ts +53 -6
  73. package/extensions/branding/queued-edit.ts +31 -0
  74. package/extensions/branding/replaced-builtins.ts +68 -0
  75. package/extensions/branding/startup.ts +33 -71
  76. package/extensions/btw/index.ts +250 -78
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/cache-marks/index.ts +42 -0
  80. package/extensions/claude-compat/index.ts +7 -11
  81. package/extensions/claude-context/index.ts +436 -63
  82. package/extensions/compaction/cut.ts +157 -0
  83. package/extensions/compaction/index.ts +219 -75
  84. package/extensions/compaction/kept-thinking.ts +62 -0
  85. package/extensions/compaction/prompt.ts +15 -12
  86. package/extensions/context-budget/budget.ts +16 -2
  87. package/extensions/context-budget/index.ts +35 -15
  88. package/extensions/context-management/index.ts +36 -28
  89. package/extensions/doctor/build.ts +59 -8
  90. package/extensions/doctor/builtins.ts +84 -0
  91. package/extensions/doctor/cli.ts +7 -1
  92. package/extensions/doctor/compat.ts +88 -23
  93. package/extensions/doctor/dependencies.ts +41 -5
  94. package/extensions/doctor/fix-prompt.ts +9 -2
  95. package/extensions/doctor/index.ts +102 -58
  96. package/extensions/doctor/models.ts +102 -63
  97. package/extensions/doctor/presets.ts +42 -27
  98. package/extensions/doctor/report.ts +19 -2
  99. package/extensions/doctor/viewer.ts +49 -27
  100. package/extensions/effort/index.ts +141 -25
  101. package/extensions/effort/settings.ts +21 -0
  102. package/extensions/effort/slider.ts +24 -20
  103. package/extensions/exit/index.ts +32 -1
  104. package/extensions/file-tracker/index.ts +420 -42
  105. package/extensions/file-tracker/replay.ts +137 -24
  106. package/extensions/file-tracker/restore.ts +66 -0
  107. package/extensions/file-tracker/results.ts +66 -0
  108. package/extensions/file-tracker/shell-reads.ts +211 -0
  109. package/extensions/file-tracker/tracker.ts +45 -15
  110. package/extensions/footer/footer-line.ts +45 -24
  111. package/extensions/footer/index.ts +47 -2
  112. package/extensions/footer/pr.ts +34 -5
  113. package/extensions/hooks/compaction.ts +7 -0
  114. package/extensions/hooks/executor.ts +32 -30
  115. package/extensions/hooks/index.ts +209 -58
  116. package/extensions/hooks/matcher.ts +3 -0
  117. package/extensions/hooks/prompt-context.ts +59 -0
  118. package/extensions/hooks/protocol.ts +41 -3
  119. package/extensions/hooks/settings.ts +50 -19
  120. package/extensions/hooks/subagent-bridge.ts +2 -0
  121. package/extensions/hooks/trust.ts +26 -12
  122. package/extensions/idle-compact/index.ts +119 -0
  123. package/extensions/idle-compact/policy.ts +143 -0
  124. package/extensions/init/index.ts +11 -10
  125. package/extensions/init/prompt.ts +25 -32
  126. package/extensions/interrupted/index.ts +13 -1
  127. package/extensions/kept-thinking/index.ts +27 -0
  128. package/extensions/ledger/index.ts +46 -0
  129. package/extensions/ledger/wire.ts +154 -0
  130. package/extensions/lib/agent-cron.ts +146 -0
  131. package/extensions/lib/agent-loader.ts +18 -3
  132. package/extensions/lib/agent-view.ts +85 -0
  133. package/extensions/lib/anthropic-auth.ts +98 -0
  134. package/extensions/lib/anthropic-payload.ts +82 -1
  135. package/extensions/lib/anthropic-server-call.ts +230 -0
  136. package/extensions/lib/anthropic-server-tools.ts +485 -0
  137. package/extensions/lib/app-launch.mjs +94 -0
  138. package/extensions/lib/argument-hints.ts +190 -0
  139. package/extensions/lib/atomic-write.ts +6 -2
  140. package/extensions/lib/auto-memory.ts +75 -0
  141. package/extensions/lib/bash-parser.ts +84 -0
  142. package/extensions/lib/btw-fork.ts +93 -0
  143. package/extensions/lib/child-extensions.ts +52 -0
  144. package/extensions/lib/child-writes.ts +45 -0
  145. package/extensions/lib/claude-context.ts +515 -100
  146. package/extensions/lib/claude-external-includes.ts +68 -0
  147. package/extensions/lib/claude-id.ts +22 -0
  148. package/extensions/lib/claude-rules.ts +323 -0
  149. package/extensions/lib/claude-settings.ts +76 -4
  150. package/extensions/lib/compaction-boundary.ts +36 -0
  151. package/extensions/lib/compaction-keep.mjs +83 -0
  152. package/extensions/lib/config-mode.ts +112 -0
  153. package/extensions/lib/consent-dialogs.ts +63 -0
  154. package/extensions/lib/consent-preview.ts +5 -1
  155. package/extensions/lib/consent-stores.ts +48 -0
  156. package/extensions/lib/context-facts.ts +97 -0
  157. package/extensions/lib/covered-messages.ts +23 -0
  158. package/extensions/lib/deferred.ts +359 -25
  159. package/extensions/lib/dock-rows.ts +60 -0
  160. package/extensions/lib/empty-tool-result.ts +27 -0
  161. package/extensions/lib/environment-block.ts +76 -0
  162. package/extensions/lib/fetch-timeout.ts +24 -6
  163. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  164. package/extensions/lib/git.ts +75 -13
  165. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  166. package/extensions/lib/grammars/README.md +2 -0
  167. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  168. package/extensions/lib/guide-docs.ts +45 -0
  169. package/extensions/lib/headless-output.ts +46 -0
  170. package/extensions/lib/hook-gate.ts +7 -7
  171. package/extensions/lib/interrupt.ts +20 -4
  172. package/extensions/lib/key-input.ts +455 -0
  173. package/extensions/lib/keys.ts +14 -1
  174. package/extensions/lib/ledger/fold-memo.ts +49 -0
  175. package/extensions/lib/ledger/fold.ts +265 -0
  176. package/extensions/lib/ledger/layout.ts +72 -0
  177. package/extensions/lib/ledger/plan.ts +102 -0
  178. package/extensions/lib/ledger/records.ts +276 -0
  179. package/extensions/lib/ledger/state.ts +47 -0
  180. package/extensions/lib/ledger/validate.ts +141 -0
  181. package/extensions/lib/ledger/wire.ts +632 -0
  182. package/extensions/lib/local-command.ts +71 -19
  183. package/extensions/lib/mcp-overrides.ts +1 -1
  184. package/extensions/lib/mcp-share.ts +46 -0
  185. package/extensions/lib/memory-content.ts +146 -0
  186. package/extensions/lib/memory-index.ts +18 -0
  187. package/extensions/lib/memory.ts +134 -123
  188. package/extensions/lib/model-catalog/huggingface.json +178 -0
  189. package/extensions/lib/model-catalog/models-dev.json +18533 -0
  190. package/extensions/lib/model-catalog/openrouter.json +5050 -0
  191. package/extensions/lib/model-catalog-data.ts +513 -0
  192. package/extensions/lib/model-catalog.ts +462 -0
  193. package/extensions/lib/model-choice-warnings.ts +137 -0
  194. package/extensions/lib/model-pairings.ts +152 -0
  195. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  196. package/extensions/lib/model-policy.ts +112 -15
  197. package/extensions/lib/model-tier.ts +283 -355
  198. package/extensions/lib/model-unusable.ts +1 -1
  199. package/extensions/lib/newer-model.ts +100 -0
  200. package/extensions/lib/notices.ts +87 -0
  201. package/extensions/lib/notifications.ts +357 -73
  202. package/extensions/lib/one-code-settings.ts +74 -8
  203. package/extensions/lib/open-browser.ts +63 -0
  204. package/extensions/lib/openrouter-generation.ts +94 -0
  205. package/extensions/lib/operation-deadline.ts +30 -0
  206. package/extensions/lib/original-command.ts +4 -4
  207. package/extensions/lib/package-version.ts +1 -1
  208. package/extensions/lib/panel-focus.ts +134 -0
  209. package/extensions/lib/paths.ts +17 -2
  210. package/extensions/lib/permission-gate.ts +72 -6
  211. package/extensions/lib/persisted-output.ts +60 -8
  212. package/extensions/lib/pi-ai-estimate.ts +19 -3
  213. package/extensions/lib/pi-version.ts +38 -2
  214. package/extensions/lib/plan-mode-channels.ts +42 -5
  215. package/extensions/lib/plugins.ts +26 -17
  216. package/extensions/lib/powershell-parse-script.ts +208 -0
  217. package/extensions/lib/powershell-parser.ts +344 -0
  218. package/extensions/lib/previous-mark.ts +101 -0
  219. package/extensions/lib/process-state.ts +48 -0
  220. package/extensions/lib/process-tree.ts +132 -8
  221. package/extensions/lib/prompt-options.ts +82 -0
  222. package/extensions/lib/queued-delivery.ts +44 -0
  223. package/extensions/lib/reminders.ts +370 -358
  224. package/extensions/lib/replaced-builtins.mjs +199 -0
  225. package/extensions/lib/replay-call.ts +70 -0
  226. package/extensions/lib/request-replay.ts +547 -0
  227. package/extensions/lib/scratchpad.ts +81 -14
  228. package/extensions/lib/send-now-channel.ts +7 -0
  229. package/extensions/lib/session-model-tier.ts +51 -0
  230. package/extensions/lib/session-work.ts +64 -0
  231. package/extensions/lib/settings-channels.ts +4 -4
  232. package/extensions/lib/shell-exit-semantics.ts +170 -0
  233. package/extensions/lib/shell-quote.ts +11 -0
  234. package/extensions/lib/shell-result.ts +70 -0
  235. package/extensions/lib/shell-spawn.ts +70 -6
  236. package/extensions/lib/shell-tool.ts +75 -25
  237. package/extensions/lib/side-call-cache.ts +124 -0
  238. package/extensions/lib/side-call-run.ts +96 -0
  239. package/extensions/lib/side-call-usage.ts +38 -0
  240. package/extensions/lib/side-call.ts +24 -1
  241. package/extensions/lib/single-flight.ts +18 -0
  242. package/extensions/lib/skill-body.ts +42 -0
  243. package/extensions/lib/skill-scan.ts +63 -24
  244. package/extensions/lib/spill-file.ts +140 -0
  245. package/extensions/lib/subagent-handback.ts +112 -0
  246. package/extensions/lib/system-role.ts +156 -0
  247. package/extensions/lib/terminal-text.ts +114 -0
  248. package/extensions/lib/text-width.ts +47 -16
  249. package/extensions/lib/timer-ops.ts +20 -0
  250. package/extensions/lib/tool-additions.ts +129 -0
  251. package/extensions/lib/tool-call-corruption.ts +134 -0
  252. package/extensions/lib/tool-path.ts +66 -0
  253. package/extensions/lib/tool-variants.ts +69 -0
  254. package/extensions/lib/tui-render.ts +255 -27
  255. package/extensions/lib/turn-budget-layout.ts +159 -0
  256. package/extensions/lib/update-check.mjs +15 -2
  257. package/extensions/lib/usage-bus.ts +1 -1
  258. package/extensions/lib/user-denial.ts +38 -0
  259. package/extensions/lib/workspace-channel.ts +16 -0
  260. package/extensions/lib/worktree-channel.ts +31 -3
  261. package/extensions/lib/worktree-isolation.ts +40 -10
  262. package/extensions/lsp/client.ts +8 -9
  263. package/extensions/lsp/index.ts +76 -12
  264. package/extensions/lsp/keep-alive.ts +1 -1
  265. package/extensions/lsp/trust.ts +119 -0
  266. package/extensions/lsp/watcher.ts +1 -1
  267. package/extensions/mcp/announce.ts +90 -43
  268. package/extensions/mcp/client.ts +161 -48
  269. package/extensions/mcp/config.ts +132 -8
  270. package/extensions/mcp/discovery.ts +19 -0
  271. package/extensions/mcp/index.ts +220 -121
  272. package/extensions/mcp/oauth/browser.ts +38 -17
  273. package/extensions/mcp/oauth/flow.ts +25 -17
  274. package/extensions/mcp/oauth/provider.ts +14 -8
  275. package/extensions/mcp/oauth/store.ts +34 -16
  276. package/extensions/mcp/panel/keys.ts +12 -11
  277. package/extensions/mcp/panel/render.ts +9 -6
  278. package/extensions/mcp/schema.ts +104 -5
  279. package/extensions/mcp/trust.ts +164 -73
  280. package/extensions/memory/entries.ts +23 -14
  281. package/extensions/memory/index.ts +134 -28
  282. package/extensions/memory/open-external.ts +33 -18
  283. package/extensions/memory/panel.ts +135 -26
  284. package/extensions/model-default/index.ts +86 -19
  285. package/extensions/newer-model/index.ts +31 -0
  286. package/extensions/notebook/description.ts +18 -0
  287. package/extensions/notebook/index.ts +6 -5
  288. package/extensions/notebook/notebook.ts +1 -1
  289. package/extensions/permissions/index.ts +1426 -398
  290. package/extensions/permissions/matcher.ts +425 -74
  291. package/extensions/permissions/modes.ts +11 -4
  292. package/extensions/permissions/panel/host.ts +130 -0
  293. package/extensions/permissions/panel/keys.ts +48 -0
  294. package/extensions/permissions/panel/render.ts +450 -0
  295. package/extensions/permissions/panel/state.ts +550 -0
  296. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  297. package/extensions/permissions/powershell-paths.ts +197 -0
  298. package/extensions/permissions/powershell-rules.ts +207 -280
  299. package/extensions/permissions/powershell-tree.ts +557 -0
  300. package/extensions/permissions/project-trust.ts +27 -11
  301. package/extensions/permissions/protected-paths.ts +14 -2
  302. package/extensions/permissions/session-grant.ts +6 -0
  303. package/extensions/permissions/settings.ts +166 -31
  304. package/extensions/permissions/subagent-gate.ts +25 -0
  305. package/extensions/permissions/workspace.ts +78 -0
  306. package/extensions/plan-mode/index.ts +276 -92
  307. package/extensions/plan-mode/reminder.ts +89 -21
  308. package/extensions/plan-mode/run-state.ts +26 -0
  309. package/extensions/plan-mode/texts.ts +177 -0
  310. package/extensions/plan-mode/viewer.ts +92 -27
  311. package/extensions/plugins/counts.ts +3 -3
  312. package/extensions/plugins/index.ts +64 -46
  313. package/extensions/plugins/install/install.ts +16 -0
  314. package/extensions/plugins/marketplace/sync.ts +9 -3
  315. package/extensions/plugins/marketplace/types.ts +3 -1
  316. package/extensions/plugins/panel/keys.ts +21 -24
  317. package/extensions/plugins/panel/render.ts +12 -6
  318. package/extensions/plugins/shell-expand.ts +107 -0
  319. package/extensions/plugins/template.ts +32 -15
  320. package/extensions/powershell/child.ts +7 -0
  321. package/extensions/powershell/description.ts +38 -6
  322. package/extensions/powershell/guards.ts +1 -1
  323. package/extensions/powershell/index.ts +14 -7
  324. package/extensions/recap/index.ts +73 -71
  325. package/extensions/recap/prompt.ts +2 -2
  326. package/extensions/recap/scheduler.ts +3 -5
  327. package/extensions/search-tools/grep-merge.ts +39 -0
  328. package/extensions/search-tools/index.ts +0 -0
  329. package/extensions/send-now/chord.ts +62 -0
  330. package/extensions/send-now/index.ts +133 -0
  331. package/extensions/session-title/index.ts +3 -0
  332. package/extensions/session-title/title.ts +4 -1
  333. package/extensions/skill/announce.ts +30 -0
  334. package/extensions/skill/code-review.ts +97 -0
  335. package/extensions/skill/index.ts +469 -132
  336. package/extensions/skill/invoke.ts +118 -75
  337. package/extensions/skill/listing.ts +133 -9
  338. package/extensions/skill/panel/keys.ts +15 -16
  339. package/extensions/skill/panel/render.ts +5 -2
  340. package/extensions/skill/panel/state.ts +12 -1
  341. package/extensions/subagents/agent-args.ts +39 -0
  342. package/extensions/subagents/agent-description.ts +132 -0
  343. package/extensions/subagents/agents.ts +146 -31
  344. package/extensions/subagents/default-model.ts +2 -3
  345. package/extensions/subagents/delegation-steer.ts +4 -4
  346. package/extensions/subagents/fork-cache.ts +82 -0
  347. package/extensions/subagents/guide-agent.ts +189 -0
  348. package/extensions/subagents/hand-back-review.ts +3 -0
  349. package/extensions/subagents/index.ts +1594 -852
  350. package/extensions/subagents/live-runs.ts +26 -6
  351. package/extensions/subagents/model-select.ts +38 -25
  352. package/extensions/subagents/outcome.ts +5 -2
  353. package/extensions/subagents/panel-keys.ts +73 -31
  354. package/extensions/subagents/panel-render.ts +120 -96
  355. package/extensions/subagents/panel-widget.ts +60 -89
  356. package/extensions/subagents/parent-mode.ts +50 -0
  357. package/extensions/subagents/pending-claim.ts +18 -1
  358. package/extensions/subagents/runner.ts +135 -80
  359. package/extensions/subagents/runs.ts +44 -5
  360. package/extensions/subagents/send-to-main-tool.ts +1 -1
  361. package/extensions/subagents/session-turns.ts +32 -8
  362. package/extensions/subagents/shell-panel.ts +36 -178
  363. package/extensions/subagents/tasks-dialog.ts +170 -0
  364. package/extensions/subagents/viewport-mirror.ts +95 -0
  365. package/extensions/subagents/worktree.ts +57 -13
  366. package/extensions/system-prompt/environment.ts +19 -19
  367. package/extensions/system-prompt/idle-turn.ts +65 -0
  368. package/extensions/system-prompt/index.ts +165 -47
  369. package/extensions/system-prompt/template.ts +34 -51
  370. package/extensions/system-prompt/tiers/common.ts +23 -64
  371. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  372. package/extensions/system-prompt/tiers/low.ts +67 -45
  373. package/extensions/system-prompt/tiers/mid.ts +96 -38
  374. package/extensions/system-reminder/index.ts +204 -70
  375. package/extensions/tasks/descriptions.ts +190 -0
  376. package/extensions/tasks/index.ts +120 -41
  377. package/extensions/tasks/store.ts +83 -16
  378. package/extensions/tool-call-corruption/child.ts +7 -0
  379. package/extensions/tool-call-corruption/index.ts +237 -0
  380. package/extensions/tool-search/index.ts +366 -144
  381. package/extensions/tool-style/index.ts +68 -10
  382. package/extensions/tool-style/write-description.ts +40 -0
  383. package/extensions/tool-style/write-render.ts +237 -0
  384. package/extensions/turn-duration/index.ts +7 -3
  385. package/extensions/turn-duration/line.ts +47 -4
  386. package/extensions/web/backends.ts +6 -9
  387. package/extensions/web/description.ts +50 -0
  388. package/extensions/web/index.ts +118 -53
  389. package/extensions/web/native-call.ts +15 -0
  390. package/extensions/web/native-results.ts +41 -0
  391. package/extensions/web-fetch/description.ts +42 -0
  392. package/extensions/web-fetch/extract.ts +53 -6
  393. package/extensions/web-fetch/index.ts +125 -76
  394. package/extensions/web-fetch/page-content.ts +19 -0
  395. package/extensions/web-fetch/response.ts +49 -0
  396. package/extensions/web-fetch/summarize.ts +9 -10
  397. package/extensions/workflow/agent-session.ts +108 -18
  398. package/extensions/workflow/args.ts +24 -0
  399. package/extensions/workflow/description.ts +67 -0
  400. package/extensions/workflow/globals.ts +17 -3
  401. package/extensions/workflow/index.ts +157 -162
  402. package/extensions/workflow/records.ts +1 -0
  403. package/extensions/workflow/run-manager.ts +33 -6
  404. package/extensions/workflow/saved-workflows.ts +3 -2
  405. package/extensions/workflow/types.ts +22 -1
  406. package/extensions/workflow/viewer.ts +54 -24
  407. package/extensions/workflow/widget.ts +20 -0
  408. package/extensions/worktree/descriptions.ts +91 -0
  409. package/extensions/worktree/guards.ts +316 -54
  410. package/extensions/worktree/index.ts +162 -44
  411. package/extensions/worktree/powershell-guards.ts +167 -0
  412. package/extensions/worktree/rewrite.ts +14 -16
  413. package/package.json +21 -7
  414. package/skills/artifact-design/SKILL.md +85 -0
  415. package/skills/code-review/SKILL.md +20 -20
  416. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  417. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  418. package/skills/code-review/cells/high-8-inline.md +122 -0
  419. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  420. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  421. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  422. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  423. package/skills/code-review/cells/minimal-prompt.md +32 -0
  424. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  425. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  426. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  427. package/skills/loop/SKILL.md +11 -0
  428. package/skills/security-review/SKILL.md +2 -5
  429. package/skills/simplify/SKILL.md +7 -5
  430. package/skills/workflow-authoring/SKILL.md +143 -0
  431. package/themes/onecode-light.json +1 -1
  432. package/themes/onecode.json +1 -1
  433. package/types/app.d.ts +58 -0
  434. package/types/community-packages.d.ts +26 -1
  435. package/extensions/auto-mode/recoverability.ts +0 -229
  436. package/extensions/lib/capability-index.ts +0 -466
  437. package/extensions/lib/model-facts.generated.json +0 -2675
  438. package/extensions/lib/model-facts.ts +0 -162
  439. package/extensions/tool-search/announce.ts +0 -42
@@ -16,11 +16,13 @@
16
16
  */
17
17
 
18
18
  import os from "node:os";
19
- import { getAgentDir, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
19
+ import { isDeepStrictEqual } from "node:util";
20
+ import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
20
21
  import { Type } from "typebox";
21
22
  import { DEFER_CHANNEL } from "../lib/deferred.ts";
22
23
  import { MCP_TOOLS_CHANNEL } from "../lib/mcp-share.ts";
23
24
  import { persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
25
+ import { emptyToolResultContent } from "../lib/empty-tool-result.ts";
24
26
  import { ccToolRenderers } from "../lib/tui-render.ts";
25
27
  import {
26
28
  callTool,
@@ -34,7 +36,7 @@ import {
34
36
  readResource,
35
37
  readResourceDir,
36
38
  } from "./client.ts";
37
- import { CONTEXT_ORDER, REMINDER_CHANNEL } from "../lib/reminders.ts";
39
+ import { CONTEXT_ORDER, REMINDER_CHANNEL, RENDER_CHANNEL, type RenderRequest } from "../lib/reminders.ts";
38
40
  import { join } from "node:path";
39
41
  import {
40
42
  MCP_STATUS_CHANNEL,
@@ -47,14 +49,14 @@ import { readDisabledMcpServers, setMcpServerDisabled } from "../lib/mcp-overrid
47
49
  import { boundedDockHeight, safeThemeBold, safeThemePaint, truncateLine } from "../lib/tui-render.ts";
48
50
  import { authenticate as runOAuthFlow, silentProvider } from "./oauth/flow.ts";
49
51
  import { hasStoredTokens } from "./oauth/store.ts";
50
- import { announcedFrom, applyMcpDelta, emptyAnnounced, mcpChangeNotices, mcpDelta, type McpSnapshot } from "./announce.ts";
52
+ import { mcpChangeNotices, mcpDelta, mcpFacts, type McpSnapshot, mcpStartupNotices } from "./announce.ts";
51
53
  import { decodeMcpKey } from "./panel/keys.ts";
52
54
  import { type McpEntry, type McpEntryStatus } from "./panel/model.ts";
53
55
  import { renderMcpPanel, type McpPaint } from "./panel/render.ts";
54
56
  import { applyMcpKey, initialMcpState, type McpEffect } from "./panel/state.ts";
55
57
  import { sessionAlive } from "../lib/session-lifecycle.ts";
56
- import { loadServers, type McpServer } from "./config.ts";
57
- import { approveMcpServers, persistApproval, projectRootOf } from "./trust.ts";
58
+ import { loadServers, type McpServer, noMcpServersMessage, piMcpConfigPaths, registeredMcpServers } from "./config.ts";
59
+ import { approveMcpServers, type McpTrustDeps, persistApproval, projectRootOf, reconnectRefusal } from "./trust.ts";
58
60
  import {
59
61
  capDescription,
60
62
  describeContent,
@@ -65,6 +67,11 @@ import {
65
67
  namespacedToolName,
66
68
  } from "./schema.ts";
67
69
  import { registerLocalCommand } from "../lib/local-command.ts";
70
+ import { canShowCustomUi, notifyRpcReadOnly } from "../lib/headless-output.ts";
71
+ import { claudeSourcesOn } from "../lib/config-mode.ts";
72
+ import { claudeJsonPath } from "../lib/paths.ts";
73
+ import { consentDialog, startupConsentReady } from "../lib/consent-dialogs.ts";
74
+ import { panelUi } from "../lib/panel-focus.ts";
68
75
 
69
76
  /** Anthropic's hard limit on a tool name; a longer one fails the whole request. */
70
77
  const MAX_TOOL_NAME_LENGTH = 128;
@@ -78,10 +85,13 @@ function unknownServerError(name: string) {
78
85
  };
79
86
  }
80
87
 
88
+ /** pi's own `mcp.json` files, read because our `/mcp` replaces pi 0.99's built-in MCP (config.ts). */
89
+ const piMcpConfigs = (cwd: string) => piMcpConfigPaths(getAgentDir(), cwd, CONFIG_DIR_NAME);
90
+
81
91
  export default function mcpExtension(pi: ExtensionAPI) {
82
92
  const connections = new Map<string, Connection>();
83
93
  let failures: FailedConnection[] = [];
84
- const registered = new Set<string>();
94
+ const registered = new Map<string, { server: string; tool: string }>();
85
95
  let servers: McpServer[] = [];
86
96
  // http servers that returned a 401 with no stored tokens — authNeeded via OAuth,
87
97
  // distinct from a missing-env authNeeded (which Authenticate cannot fix).
@@ -93,18 +103,17 @@ export default function mcpExtension(pi: ExtensionAPI) {
93
103
  const withheldNames = new Map<string, string>();
94
104
  // Config file paths contributed by plugins, so the panel can group them.
95
105
  let pluginConfigPaths = new Set<string>();
106
+ // pi 0.99's registry of servers other extensions register; absent on older pi.
107
+ // Registered after the session started: our servers are read once per session.
108
+ pi.on("mcp_servers_change", (_event, ctx) => {
109
+ const text = "An extension changed its MCP servers; run /reload to connect the change.";
110
+ if (ctx.hasUI) ctx.ui.notify(text, "info");
111
+ else process.stderr.write(`${text}\n`);
112
+ });
96
113
  const home = os.homedir();
97
114
  // Live tool definitions, shared with in-process subagents so they reach MCP
98
115
  // through these same open connections instead of connecting their own.
99
116
  const sharedTools: ToolDefinition[] = [];
100
- // Message 1 carries the MCP instructions and failed-servers reminders. Once a
101
- // request has gone out it is part of every later request's cached prefix, so
102
- // those blocks are frozen and later changes ride one-shot notices (./announce.ts).
103
- let requestSent = false;
104
- let announced = emptyAnnounced();
105
- pi.on("context", () => {
106
- requestSent = true;
107
- });
108
117
 
109
118
  /**
110
119
  * Where oversized results are persisted — the session dir, matching Claude
@@ -114,6 +123,8 @@ export default function mcpExtension(pi: ExtensionAPI) {
114
123
  const resultsDir = (ctx: ExtensionContext | undefined): string => sessionResultsDir(ctx);
115
124
 
116
125
  const registerToolsFor = (connection: Connection) => {
126
+ // Names other extensions own, read once per batch (getAllTools builds every definition).
127
+ let taken: Set<string> | undefined;
117
128
  for (const tool of connection.tools) {
118
129
  const name = namespacedToolName(connection.server.name, tool.name);
119
130
  // Anthropic caps a tool name at 128 characters and rejects the whole
@@ -128,8 +139,12 @@ export default function mcpExtension(pi: ExtensionAPI) {
128
139
  });
129
140
  continue;
130
141
  }
131
- if (registered.has(name)) {
132
- // Two tools sanitise to one name (`a-b` and `a_b`, or a server
142
+ const owner = registered.get(name);
143
+ // An unchanged tool on reconnect keeps its exact definition and loaded
144
+ // state. Only a different raw server/tool pair is a name collision.
145
+ if (owner?.server === connection.server.name && owner.tool === tool.name) continue;
146
+ if (owner || (taken ??= new Set(pi.getAllTools().map((existing) => existing.name))).has(name)) {
147
+ // Two tools sanitise to one name (`a.b` and `a_b`, or a server
133
148
  // reconnecting under a stale registration). Skipping silently would
134
149
  // make the second tool unreachable with no trace; record it where
135
150
  // the connection summary reports it.
@@ -139,7 +154,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
139
154
  });
140
155
  continue;
141
156
  }
142
- registered.add(name);
157
+ registered.set(name, { server: connection.server.name, tool: tool.name });
143
158
 
144
159
  const def: ToolDefinition = {
145
160
  name,
@@ -147,7 +162,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
147
162
  ...ccToolRenderers(`${connection.server.name}: ${tool.name}`),
148
163
  description: capDescription(tool.description) ?? `MCP tool "${tool.name}" from server "${connection.server.name}".`,
149
164
  parameters: jsonSchemaToTypeBox(tool.inputSchema),
150
- async execute(toolCallId, params, _signal, _onUpdate, ctx) {
165
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
151
166
  const live = connections.get(connection.server.name);
152
167
  if (!live) {
153
168
  return {
@@ -156,17 +171,27 @@ export default function mcpExtension(pi: ExtensionAPI) {
156
171
  isError: true,
157
172
  };
158
173
  }
174
+ const current = live.tools.find((candidate) => candidate.name === tool.name);
175
+ if (!current || !isDeepStrictEqual(current.inputSchema, tool.inputSchema)) {
176
+ return {
177
+ content: [{ type: "text", text: !current
178
+ ? `MCP server "${connection.server.name}" no longer exposes tool "${tool.name}".`
179
+ : `MCP tool "${name}" changed its input schema. Run /reload to load the new definition; this call was not sent.` }],
180
+ details: {} as Record<string, unknown>,
181
+ isError: true,
182
+ };
183
+ }
159
184
  try {
160
- const result = await callTool(live, tool.name, (params ?? {}) as Record<string, unknown>);
161
- const { text, images } = describeContent(result.content as McpContentBlock[] | undefined, connection.server.name);
185
+ const result = await callTool(live, tool.name, (params ?? {}) as Record<string, unknown>, signal);
186
+ const { text: contentText, images } = describeContent(result.content as McpContentBlock[] | undefined, connection.server.name);
187
+ const structuredText = result.structuredContent !== undefined ? JSON.stringify(result.structuredContent, null, 2) : "";
188
+ const text = [contentText, structuredText].filter(Boolean).join("\n");
189
+ const content = [
190
+ ...(text ? [{ type: "text" as const, text: persistIfLarge(text, { dir: resultsDir(ctx), id: toolCallId }) }] : []),
191
+ ...images.map((image) => ({ type: "image" as const, data: image.data, mimeType: image.mimeType })),
192
+ ];
162
193
  return {
163
- content: [
164
- {
165
- type: "text",
166
- text: text ? persistIfLarge(text, { dir: resultsDir(ctx), id: toolCallId }) : "(no output)",
167
- },
168
- ...images.map((image) => ({ type: "image" as const, data: image.data, mimeType: image.mimeType })),
169
- ],
194
+ content: emptyToolResultContent(content, name) ?? content,
170
195
  details: { server: connection.server.name, tool: tool.name } as Record<string, unknown>,
171
196
  isError: result.isError === true,
172
197
  };
@@ -232,15 +257,38 @@ export default function mcpExtension(pi: ExtensionAPI) {
232
257
  });
233
258
  emitInstructions();
234
259
  };
260
+ let previousTools = connection.tools;
261
+ connection.onToolsChanged = (error) => {
262
+ if (!alive() || connection.closing || connections.get(server.name) !== connection) return;
263
+ if (error) {
264
+ failures.push({ server, error: error.message });
265
+ pi.events.emit(REMINDER_CHANNEL, { text: `MCP server "${server.name}" could not refresh its tool list: ${error.message}` });
266
+ return;
267
+ }
268
+ const unavailable = previousTools.filter((old) => {
269
+ const current = connection.tools.find((tool) => tool.name === old.name);
270
+ return !current || !isDeepStrictEqual(current.inputSchema, old.inputSchema);
271
+ });
272
+ previousTools = connection.tools;
273
+ registerToolsFor(connection);
274
+ if (unavailable.length > 0) {
275
+ pi.events.emit(REMINDER_CHANNEL, {
276
+ text: `MCP server "${server.name}" removed or changed these tools: ${unavailable.map((tool) => namespacedToolName(server.name, tool.name)).join(", ")}. Their old definitions cannot be used. Run /reload to refresh changed schemas.`,
277
+ });
278
+ }
279
+ };
235
280
  registerToolsFor(connection);
236
281
  for (const warning of connection.warnings) failures.push({ server, error: warning });
237
- if (connection.resources.length > 0) registerResourceTools();
282
+ if (connection.resources.length > 0 || connection.resourceTemplates?.length) registerResourceTools();
238
283
  };
239
284
 
285
+ /** Servers with a connect in flight (startup, a reconnect, an authenticate): not dropped while it runs. */
286
+ const connectingNames = new Set<string>();
240
287
  const connectOne = async (server: McpServer): Promise<void> => {
241
288
  failures = failures.filter((f) => f.server.name !== server.name);
242
289
  oauthNeeded.delete(server.name);
243
- const provider = server.kind === "http" && hasStoredTokens(server.name) ? silentProvider(server.name) : undefined;
290
+ const provider = server.kind === "http" && hasStoredTokens(server) ? silentProvider(server) : undefined;
291
+ connectingNames.add(server.name);
244
292
  try {
245
293
  const connection = await connect(server, provider);
246
294
  if (!alive()) {
@@ -252,6 +300,8 @@ export default function mcpExtension(pi: ExtensionAPI) {
252
300
  if (!alive()) return;
253
301
  if (server.kind === "http" && isUnauthorized(error)) oauthNeeded.add(server.name);
254
302
  else failures.push({ server, error: (error as Error).message });
303
+ } finally {
304
+ connectingNames.delete(server.name);
255
305
  }
256
306
  };
257
307
 
@@ -261,73 +311,94 @@ export default function mcpExtension(pi: ExtensionAPI) {
261
311
  * which servers failed — otherwise it concludes the tools do not exist or the
262
312
  * user has no access instead of reporting a connection failure the user can
263
313
  * fix (review M4). Re-run on every connect, close, reconnect, authenticate,
264
- * enable and disable. Before the first request the two first-prepend blocks on
265
- * message 1 are (re)written; after it they are frozen and only the changes are
266
- * announced (./announce.ts).
314
+ * enable and disable. The two first-prepend blocks are live state: what an
315
+ * epoch's message 1 says. Inside an epoch the render step below announces
316
+ * only what changed since the epoch told it (./announce.ts).
267
317
  */
318
+ const liveSnapshot = (): McpSnapshot => ({
319
+ connected: [...connections.values()].map((c) => ({ name: c.server.name, instructions: c.instructions })),
320
+ failed: failures.filter((f) => !connections.has(f.server.name)).map((f) => ({ name: f.server.name, error: f.error })),
321
+ });
268
322
  const emitInstructions = () => {
269
323
  const live = [...connections.values()];
270
- const snapshot: McpSnapshot = {
271
- connected: live.map((c) => ({ name: c.server.name, instructions: c.instructions })),
272
- failed: failures.filter((f) => !connections.has(f.server.name)).map((f) => ({ name: f.server.name, error: f.error })),
273
- };
274
-
275
- if (!requestSent) {
276
- // Nothing is cached yet: (re)write the standing message-1 blocks.
277
- if (snapshot.failed.length === 0) {
278
- pi.events.emit(REMINDER_CHANNEL, { scope: "every-turn", key: "mcp-failures", text: "", remove: true });
279
- } else {
280
- pi.events.emit(REMINDER_CHANNEL, {
281
- scope: "every-turn",
282
- key: "mcp-failures",
283
- text: mcpFailuresReminder(snapshot.failed),
284
- placement: "first-prepend",
285
- order: CONTEXT_ORDER.mcp + 1,
286
- });
287
- }
288
- const instructions = mcpInstructionsReminder(live);
289
- if (instructions) {
290
- pi.events.emit(REMINDER_CHANNEL, {
291
- scope: "every-turn",
292
- key: "mcp-instructions",
293
- text: instructions,
294
- placement: "first-prepend",
295
- order: CONTEXT_ORDER.mcp,
296
- });
297
- } else {
298
- // No server has instructions anymore (e.g. the last one was disabled or
299
- // disconnected) — drop the stale every-turn reminder so the model stops
300
- // being told to use tools that are gone.
301
- pi.events.emit(REMINDER_CHANNEL, { scope: "every-turn", key: "mcp-instructions", text: "", remove: true });
302
- }
303
- announced = announcedFrom(snapshot);
304
- return;
324
+ const snapshot = liveSnapshot();
325
+ if (snapshot.failed.length === 0) {
326
+ pi.events.emit(REMINDER_CHANNEL, { key: "mcp-failures", text: "", remove: true });
327
+ } else {
328
+ pi.events.emit(REMINDER_CHANNEL, {
329
+ key: "mcp-failures",
330
+ text: mcpFailuresReminder(snapshot.failed),
331
+ placement: "first-prepend",
332
+ order: CONTEXT_ORDER.mcp + 1,
333
+ facts: mcpFacts({ connected: [], failed: snapshot.failed }),
334
+ });
335
+ }
336
+ const instructions = mcpInstructionsReminder(live);
337
+ if (instructions) {
338
+ pi.events.emit(REMINDER_CHANNEL, {
339
+ key: "mcp-instructions",
340
+ text: instructions,
341
+ placement: "first-prepend",
342
+ order: CONTEXT_ORDER.mcp,
343
+ facts: mcpFacts({ connected: snapshot.connected, failed: [] }),
344
+ });
345
+ } else {
346
+ // No server has instructions anymore (e.g. the last one was disabled or
347
+ // disconnected) — drop the stale block so an epoch does not tell the
348
+ // model to use tools that are gone.
349
+ pi.events.emit(REMINDER_CHANNEL, { key: "mcp-instructions", text: "", remove: true });
305
350
  }
306
-
307
- // Message 1 is frozen (rewriting it would re-cache the whole conversation):
308
- // tell the model only what changed, as one-shots where it reads next.
309
- const delta = mcpDelta(announced, snapshot);
310
- for (const text of mcpChangeNotices(delta)) pi.events.emit(REMINDER_CHANNEL, { text });
311
- applyMcpDelta(announced, delta);
312
351
  };
352
+ // Inside an epoch, what changed since message 1 (or the last notice) rides
353
+ // the request's tail, decided when the request goes out.
354
+ pi.events.on(RENDER_CHANNEL, (data) => {
355
+ const request = data as RenderRequest;
356
+ if (request.epoch) return;
357
+ const delta = mcpDelta(request.toldBlocks, liveSnapshot(), !connectSettled, connectingNames);
358
+ const notices = mcpChangeNotices(delta);
359
+ notices.forEach((text, i) => request.blocks.push({ text, ...(i === 0 ? { facts: delta.facts } : {}) }));
360
+ });
313
361
 
314
362
  /** Config files that exist but failed to parse (review M11) — shown at startup and in /mcp. */
315
363
  let configErrors: string[] = [];
316
364
 
365
+ /** How the consent check asks, tells and records a "No", for startup and for Reconnect alike. */
366
+ const consentDeps = (ctx: ExtensionContext): McpTrustDeps => ({
367
+ hasUI: ctx.hasUI,
368
+ select: (title, options) => consentDialog(pi.events, (signal) => ctx.ui.select(title, options, { signal })),
369
+ notify: (message) => {
370
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
371
+ else process.stderr.write(`${message}\n`);
372
+ },
373
+ disable: (server) => {
374
+ setMcpServerDisabled(server.name, true, "project", ctx.cwd, home);
375
+ disabledNames.add(server.name);
376
+ },
377
+ });
378
+
317
379
  const connectAll = async (ctx: ExtensionContext) => {
318
380
  const plugins = discoverPlugins(defaultDiscoverRoots(getAgentDir(), ctx.cwd));
319
381
  pluginConfigPaths = new Set(plugins.mcpConfigs);
320
382
  configErrors = [];
383
+ const configNotes: string[] = [];
321
384
  servers = loadServers(ctx.cwd, home, process.env, [...pluginConfigPaths], {
322
385
  pluginNames: plugins.mcpConfigPlugins,
323
386
  onError: (path, message) => configErrors.push(`${path}: ${message}`),
387
+ piConfigs: piMcpConfigs(ctx.cwd),
388
+ onNote: (message) => configNotes.push(message),
389
+ registered: registeredMcpServers(pi),
324
390
  });
325
- if (configErrors.length > 0) {
326
- const text = `MCP config could not be parsed (ignored): ${configErrors.join("; ")}`;
391
+ const configWarnings = [
392
+ ...(configErrors.length > 0 ? [`MCP config could not be parsed (ignored): ${configErrors.join("; ")}`] : []),
393
+ ...configNotes,
394
+ ];
395
+ for (const text of configWarnings) {
327
396
  if (ctx.hasUI) ctx.ui.notify(text, "warning");
328
397
  else process.stderr.write(`${text}\n`);
329
398
  }
330
399
  disabledNames = readDisabledMcpServers(ctx.cwd, home);
400
+ // With no server configured there is nothing to connect; a resumed epoch
401
+ // that told the model about one hears that it dropped at render time.
331
402
  if (servers.length === 0) return;
332
403
 
333
404
  // Disabled servers are listed but never connected; a server with an unset
@@ -336,19 +407,13 @@ export default function mcpExtension(pi: ExtensionAPI) {
336
407
  // server needs the user's consent first (trust.ts — a cloned repo must not
337
408
  // run commands at startup). Everything else connects.
338
409
  const candidates = servers.filter((server) => !disabledNames.has(server.name) && !server.missingEnv?.length);
339
- const consent = await approveMcpServers(candidates, pluginConfigPaths, ctx.cwd, home, {
340
- hasUI: ctx.hasUI,
341
- select: (title, options) => ctx.ui.select(title, options),
342
- notify: (message) => {
343
- if (ctx.hasUI) ctx.ui.notify(message, "warning");
344
- else process.stderr.write(`${message}\n`);
345
- },
346
- disable: (server) => {
347
- setMcpServerDisabled(server.name, true, "project", ctx.cwd, home);
348
- disabledNames.add(server.name);
349
- },
350
- });
351
- if (!alive()) return;
410
+ // Reserve the modal position before the asynchronous policy read, so the
411
+ // external-includes dialog cannot jump ahead of MCP's startup consent.
412
+ const consent = await consentDialog(pi.events, (signal) => approveMcpServers(candidates, pluginConfigPaths, ctx.cwd, home, {
413
+ ...consentDeps(ctx),
414
+ select: (title, options) => ctx.ui.select(title, options, { signal }),
415
+ }));
416
+ if (!consent || !alive()) return;
352
417
  withheldNames.clear();
353
418
  for (const { server, reason } of consent.withheld) {
354
419
  withheldNames.set(
@@ -364,16 +429,15 @@ export default function mcpExtension(pi: ExtensionAPI) {
364
429
 
365
430
  if (!alive()) return;
366
431
 
367
- if (failures.length > 0) {
368
- const text = failures.map((f) => `MCP server "${f.server.name}" failed: ${f.error}`).join("\n") + " (/mcp for status)";
369
- if (ctx.hasUI) ctx.ui.notify(text, "warning");
370
- else process.stderr.write(`${text}\n`);
371
- }
372
- if (oauthNeeded.size > 0 && ctx.hasUI) {
373
- ctx.ui.notify(
374
- `${oauthNeeded.size} MCP server${oauthNeeded.size === 1 ? "" : "s"} need authentication — run /mcp to authenticate.`,
375
- "warning",
376
- );
432
+ // Warnings from servers that did connect are shown in /mcp, not as failures.
433
+ const failed = failures.filter((f) => !connections.has(f.server.name));
434
+ if (ctx.hasUI) {
435
+ // Needs auth: an OAuth sign-in, or a credential variable that is not set (never attempted).
436
+ const needsAuth = new Set([...oauthNeeded, ...servers.filter((server) => !disabledNames.has(server.name) && server.missingEnv?.length).map((server) => server.name)]).size;
437
+ for (const text of mcpStartupNotices(new Set(failed.map((f) => f.server.name)).size, needsAuth)) ctx.ui.notify(text, "warning");
438
+ } else if (failed.length > 0) {
439
+ // Headless runs have no /mcp, so they keep each server's error on stderr.
440
+ process.stderr.write(`${failed.map((f) => `MCP server "${f.server.name}" failed: ${f.error}`).join("\n")}\n`);
377
441
  }
378
442
 
379
443
  emitInstructions();
@@ -384,12 +448,17 @@ export default function mcpExtension(pi: ExtensionAPI) {
384
448
  if (resourceToolsRegistered) return;
385
449
  resourceToolsRegistered = true;
386
450
 
387
- pi.registerTool({
451
+ const registerResourceTool: ExtensionAPI["registerTool"] = (definition) => {
452
+ pi.registerTool(definition);
453
+ sharedTools.push(definition as ToolDefinition);
454
+ };
455
+
456
+ registerResourceTool({
388
457
  name: "list_mcp_resources",
389
458
  label: "MCP Resources",
390
459
  ...ccToolRenderers("MCP Resources"),
391
460
  description:
392
- "List resources exposed by connected MCP servers. Resources are readable documents or data the server offers, addressed by uri.",
461
+ "List resources and URI templates exposed by connected MCP servers. Resources are readable documents or data the server offers, addressed by uri. Substitute template variables to construct a resource uri.",
393
462
  parameters: Type.Object({
394
463
  server: Type.Optional(Type.String({ description: "Limit to one server by name" })),
395
464
  }),
@@ -417,12 +486,15 @@ export default function mcpExtension(pi: ExtensionAPI) {
417
486
  `${connection.server.name} ${resource.uri}${resource.name ? ` — ${resource.name}` : ""}${resource.description ? `: ${resource.description}` : ""}`,
418
487
  );
419
488
  }
489
+ for (const template of connection.resourceTemplates ?? []) {
490
+ lines.push(`${connection.server.name} ${template.uriTemplate} (URI template)${template.name ? ` — ${template.name}` : ""}${template.description ? `: ${template.description}` : ""}`);
491
+ }
420
492
  }
421
493
  return {
422
494
  content: [
423
495
  {
424
496
  type: "text",
425
- text: lines.length ? persistIfLarge(lines.join("\n"), { dir: resultsDir(ctx), id: toolCallId }) : "No MCP resources available.",
497
+ text: lines.length ? persistIfLarge(lines.join("\n"), { dir: resultsDir(ctx), id: toolCallId }) : "No resources found. MCP servers may still provide tools even if they have no resources.",
426
498
  },
427
499
  ],
428
500
  details: { count: lines.length },
@@ -430,7 +502,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
430
502
  },
431
503
  });
432
504
 
433
- pi.registerTool({
505
+ registerResourceTool({
434
506
  name: "read_mcp_resource",
435
507
  label: "Read MCP Resource",
436
508
  ...ccToolRenderers("Read MCP Resource"),
@@ -439,17 +511,17 @@ export default function mcpExtension(pi: ExtensionAPI) {
439
511
  server: Type.String({ description: "Server name that owns the resource" }),
440
512
  uri: Type.String({ description: "Resource uri" }),
441
513
  }),
442
- async execute(toolCallId, params, _signal, _onUpdate, ctx) {
514
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
443
515
  const connection = connections.get(params.server);
444
516
  if (!connection) return unknownServerError(params.server);
445
517
  try {
446
- const result = await readResource(connection, params.uri);
447
- const text = describeResourceContents(result.contents as McpResourceContents[] | undefined);
518
+ const result = await readResource(connection, params.uri, signal);
519
+ const text = describeResourceContents(result.contents as McpResourceContents[] | undefined) || JSON.stringify({ contents: result.contents ?? [] });
448
520
  return {
449
521
  content: [
450
522
  {
451
523
  type: "text",
452
- text: text ? persistIfLarge(text, { dir: resultsDir(ctx), id: toolCallId }) : "(empty resource)",
524
+ text: persistIfLarge(text, { dir: resultsDir(ctx), id: toolCallId }),
453
525
  },
454
526
  ],
455
527
  details: { server: params.server, uri: params.uri },
@@ -464,7 +536,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
464
536
  },
465
537
  });
466
538
 
467
- pi.registerTool({
539
+ registerResourceTool({
468
540
  name: "read_mcp_resource_dir",
469
541
  label: "List MCP Resource Directory",
470
542
  ...ccToolRenderers("List MCP Resource Directory"),
@@ -474,18 +546,18 @@ export default function mcpExtension(pi: ExtensionAPI) {
474
546
  server: Type.String({ description: "Server name that owns the directory" }),
475
547
  uri: Type.String({ description: "The directory resource uri to list" }),
476
548
  }),
477
- async execute(toolCallId, params, _signal, _onUpdate, ctx) {
549
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
478
550
  const connection = connections.get(params.server);
479
551
  if (!connection) return unknownServerError(params.server);
480
552
  try {
481
- const result = await readResourceDir(connection, params.uri);
553
+ const result = await readResourceDir(connection, params.uri, signal);
482
554
  const entries = (result.resources ?? result.entries ?? []) as Array<{ uri?: string; name?: string; mimeType?: string }>;
483
555
  const lines = entries.map((e) => `${e.uri ?? "(no uri)"}${e.name ? ` — ${e.name}` : ""}${e.mimeType ? ` (${e.mimeType})` : ""}`);
484
556
  return {
485
557
  content: [
486
558
  {
487
559
  type: "text",
488
- text: lines.length ? persistIfLarge(lines.join("\n"), { dir: resultsDir(ctx), id: toolCallId }) : "(empty directory)",
560
+ text: lines.length ? persistIfLarge(lines.join("\n"), { dir: resultsDir(ctx), id: toolCallId }) : 'Directory is empty.\n{"resources":[]}',
489
561
  },
490
562
  ],
491
563
  details: { server: params.server, uri: params.uri },
@@ -503,13 +575,10 @@ export default function mcpExtension(pi: ExtensionAPI) {
503
575
  for (const name of ["list_mcp_resources", "read_mcp_resource", "read_mcp_resource_dir"]) {
504
576
  pi.events.emit(DEFER_CHANNEL, { name, keywords: ["mcp", "resource", "document", "uri", "directory"] });
505
577
  }
578
+ pi.events.emit(MCP_TOOLS_CHANNEL, { tools: [...sharedTools] });
506
579
  };
507
580
 
508
581
  pi.on("session_start", async (_event, ctx) => {
509
- // A new session (/clear, resume in a new process) has no cached prefix yet:
510
- // its message-1 blocks are written fresh (same reset as tool-search).
511
- requestSent = false;
512
- announced = emptyAnnounced();
513
582
  // pi awaits session_start handlers serially before the prompt opens, and
514
583
  // remote servers take seconds to answer — awaiting here was the entire
515
584
  // "slow startup" (4.9s → 0.24s measured, findings §15). In the interactive
@@ -524,6 +593,9 @@ export default function mcpExtension(pi: ExtensionAPI) {
524
593
  // `failures`, so this only catches setup bugs — surface them loud.
525
594
  if (ctx.hasUI) ctx.ui.notify(`MCP startup failed: ${(error as Error).message}`, "error");
526
595
  });
596
+ // The external-includes question follows the startup hooks/MCP consent,
597
+ // but does not wait for slow server connections.
598
+ startupConsentReady(pi.events);
527
599
  connecting.finally(() => {
528
600
  connectSettled = true;
529
601
  // The session may be gone by now (/clear while a remote server was still
@@ -600,12 +672,18 @@ export default function mcpExtension(pi: ExtensionAPI) {
600
672
  if (pluginConfigPaths.has(server.source)) {
601
673
  return { rank: 3, group: "Plugin MCPs", configLocation: "Plugin configuration", scope: "project" };
602
674
  }
603
- if (server.source === join(home, ".claude.json")) {
675
+ if (server.source === claudeJsonPath(home) || server.piOrigin === "user") {
604
676
  return { rank: 0, group: `User MCPs (${shorten(server.source)})`, configLocation: shorten(server.source), scope: "user" };
605
677
  }
606
678
  if (server.source.endsWith(join(".claude", "settings.local.json"))) {
607
679
  return { rank: 2, group: "Project MCPs (.claude/settings.local.json)", configLocation: shorten(server.source), scope: "project" };
608
680
  }
681
+ if (server.piOrigin === "extension") {
682
+ return { rank: 3, group: "Extension MCPs", configLocation: shorten(server.source), scope: "user" };
683
+ }
684
+ if (server.piOrigin === "project") {
685
+ return { rank: 1, group: `Project MCPs (${join(CONFIG_DIR_NAME, "mcp.json")})`, configLocation: shorten(server.source), scope: "project" };
686
+ }
609
687
  return { rank: 1, group: `Project MCPs (${shorten(server.source)})`, configLocation: shorten(server.source), scope: "project" };
610
688
  };
611
689
 
@@ -626,7 +704,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
626
704
  // honestly from whether tokens are stored — not just "failed http".
627
705
  const authState =
628
706
  status === "failed" && server.kind === "http"
629
- ? hasStoredTokens(server.name)
707
+ ? hasStoredTokens(server)
630
708
  ? ("authenticated" as const)
631
709
  : ("notAuthenticated" as const)
632
710
  : undefined;
@@ -656,7 +734,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
656
734
  const MCP_PANEL_MAX_HEIGHT = 22;
657
735
 
658
736
  const openMcpPanel = async (ctx: ExtensionContext): Promise<void> => {
659
- await ctx.ui.custom<null>((tui, theme, _keybindings, done) => {
737
+ await panelUi(pi.events, ctx.ui).custom<null>((tui, theme, _keybindings, done) => {
660
738
  const paint: McpPaint = { fg: safeThemePaint(theme), bold: safeThemeBold(theme) };
661
739
  const state = initialMcpState();
662
740
  let notices: string[] = [];
@@ -692,7 +770,24 @@ export default function mcpExtension(pi: ExtensionAPI) {
692
770
  const runReconnect = async (entry: McpEntry) => {
693
771
  const server = servers.find((s) => s.name === entry.name);
694
772
  if (!server || busy.has(entry.name)) return;
773
+ const refusal = reconnectRefusal(server);
774
+ if (refusal) {
775
+ notices = [refusal];
776
+ syncRepaint();
777
+ return;
778
+ }
695
779
  busy.add(entry.name);
780
+ // Reconnect spawns the command again, so a project server needs the
781
+ // same consent startup asked for (a stored or session approval
782
+ // passes without a prompt).
783
+ const consent = await approveMcpServers([server], pluginConfigPaths, ctx.cwd, home, consentDeps(ctx));
784
+ if (consent.approved.length === 0) {
785
+ busy.delete(entry.name);
786
+ if (!alive()) return;
787
+ notices = [`"${entry.name}" was not approved, so it was not reconnected.`];
788
+ syncRepaint();
789
+ return;
790
+ }
696
791
  notices = [`Reconnecting to "${entry.name}"…`];
697
792
  syncRepaint();
698
793
  const existing = connections.get(entry.name);
@@ -844,16 +939,19 @@ export default function mcpExtension(pi: ExtensionAPI) {
844
939
  };
845
940
 
846
941
  registerLocalCommand(pi, "mcp", {
942
+ reportsResult: true,
847
943
  description: "Manage MCP servers (status, reconnect, authenticate, enable/disable)",
848
944
  handler: async (args, ctx) => {
849
945
  if (servers.length === 0) {
850
- ctx.ui.notify("No MCP servers configured. Add them to .mcp.json or ~/.claude.json.", "info");
851
- return;
946
+ const stdout = noMcpServersMessage(claudeSourcesOn());
947
+ ctx.ui.notify(stdout, "info");
948
+ return stdout;
852
949
  }
853
- if (ctx.hasUI) {
950
+ if (canShowCustomUi(ctx)) {
854
951
  await openMcpPanel(ctx);
855
- return;
952
+ return "MCP dialog dismissed";
856
953
  }
954
+ const readOnly = notifyRpcReadOnly(ctx, "/mcp", "reconnect, authenticate, enable or disable servers");
857
955
  // Non-interactive fallback: a flat status listing, with tool/resource
858
956
  // counts and any warning for connected servers.
859
957
  const lines = servers.map((server) => {
@@ -869,6 +967,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
869
967
  return `${status.padEnd(12)} ${server.name}${suffix} (${server.source})`;
870
968
  });
871
969
  ctx.ui.notify(lines.join("\n"), "info");
970
+ return [readOnly, ...lines].filter(Boolean).join("\n");
872
971
  },
873
972
  });
874
973
  }