@herbertgao/pi-extensions 2026.8.4 → 2026.8.5

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 (288) hide show
  1. package/README.md +4 -4
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +5 -8
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
  13. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  14. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  15. package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
  16. package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
  17. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
  18. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +15 -2
  20. package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
  21. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
  22. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
  23. package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
  24. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  25. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  26. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  27. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  30. package/node_modules/@narumitw/pi-btw/README.md +8 -7
  31. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  32. package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
  33. package/node_modules/pi-lens/CHANGELOG.md +2440 -954
  34. package/node_modules/pi-lens/README.md +13 -0
  35. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  36. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  37. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  38. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  39. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  40. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  41. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  42. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  43. package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
  44. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  45. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  46. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  47. package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
  48. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  49. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  50. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  51. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  52. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  53. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  54. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  55. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  56. package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
  57. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  58. package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
  59. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  60. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  61. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  62. package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
  63. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
  64. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  65. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
  66. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  67. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  68. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  69. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  70. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  71. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
  72. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  73. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  74. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  75. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  76. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  77. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  85. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  86. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  87. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  99. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  100. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  101. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  102. package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
  103. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  104. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  105. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  106. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  107. package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
  108. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  109. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  110. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  111. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  112. package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
  113. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  114. package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
  115. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
  116. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  117. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  118. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  119. package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
  120. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  121. package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
  122. package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
  123. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  124. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  125. package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
  126. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  127. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  128. package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
  129. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  130. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  131. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
  132. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  133. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
  134. package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
  135. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  136. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
  137. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  138. package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
  139. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  140. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  141. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  142. package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
  143. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  144. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  145. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  146. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
  147. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  148. package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
  149. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  150. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  151. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  152. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  153. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  154. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  155. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  156. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  157. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  158. package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
  159. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  160. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  161. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  162. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  163. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  164. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  165. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  166. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  167. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  168. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  169. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  170. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  171. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  172. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  173. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  174. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  175. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  176. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  177. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  178. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  179. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  180. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  181. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  182. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
  183. package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
  184. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
  185. package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
  186. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  187. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
  188. package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
  189. package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
  190. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  191. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
  192. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  193. package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
  194. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  195. package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
  196. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  197. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  198. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  199. package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
  200. package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
  201. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  202. package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
  203. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  204. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  205. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  206. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  207. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  208. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  209. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  210. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  211. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  212. package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
  213. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  214. package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
  215. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  216. package/node_modules/pi-lens/dist/index.js +49361 -36888
  217. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  218. package/node_modules/pi-lens/dist/mcp/server.js +211 -44
  219. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  220. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
  221. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
  222. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  223. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  224. package/node_modules/pi-lens/docs/agent-guide.md +2 -2
  225. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  226. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  227. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  228. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
  229. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  230. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  231. package/node_modules/pi-lens/docs/fable.md +27 -0
  232. package/node_modules/pi-lens/docs/features.md +31 -2
  233. package/node_modules/pi-lens/docs/globalconfig.md +45 -1
  234. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  235. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  236. package/node_modules/pi-lens/docs/mcp.md +37 -4
  237. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  238. package/node_modules/pi-lens/docs/settings.md +12 -1
  239. package/node_modules/pi-lens/docs/tools.md +4 -1
  240. package/node_modules/pi-lens/docs/usage.md +18 -0
  241. package/node_modules/pi-lens/package.json +10 -10
  242. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  243. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  244. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  245. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  246. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  247. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  248. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  249. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  250. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  251. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  252. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  253. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  254. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
  255. package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
  256. package/node_modules/pi-mcp-adapter/README.md +34 -3
  257. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  258. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  259. package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
  260. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  261. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  262. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  263. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  264. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  265. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  266. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  267. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  268. package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
  269. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  270. package/node_modules/pi-mcp-adapter/package.json +3 -3
  271. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  272. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  273. package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
  274. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  275. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  276. package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
  277. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  278. package/node_modules/pi-mcp-adapter/types.ts +93 -27
  279. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  280. package/package.json +11 -12
  281. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  282. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  283. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  284. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  285. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  286. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  287. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  288. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -31,7 +31,17 @@ import {
31
31
  } from "@earendil-works/pi-tui"
32
32
  import { Type } from "@sinclair/typebox"
33
33
  import { abortable } from "./abortable.js"
34
- import { renderAgentName } from "./agent-color.js"
34
+ import { hasAgentBadge, renderAgentName } from "./agent-color.js"
35
+ import {
36
+ buildNewAgentFile,
37
+ disableInContent,
38
+ enableInContent,
39
+ findAgentFile,
40
+ isEmptyStub,
41
+ personalAgentsDir,
42
+ projectAgentsDir,
43
+ serializeAgentFile,
44
+ } from "./agent-file-toggle.js"
35
45
  import { AgentManager } from "./agent-manager.js"
36
46
  import {
37
47
  getAgentConversation,
@@ -74,6 +84,7 @@ import {
74
84
  import { getMaxSubagentDepth, setMaxSubagentDepth } from "./nested-tools.js"
75
85
  import {
76
86
  createOutputFilePath,
87
+ ensureOutputFile,
77
88
  getOutputTranscriptDefault,
78
89
  setOutputTranscriptDefault,
79
90
  streamToOutputFile,
@@ -121,6 +132,7 @@ import {
121
132
  } from "./ui/agent-widget.js"
122
133
  import { FleetList, type FleetUICtx } from "./ui/fleet-list.js"
123
134
  import { showSchedulesMenu } from "./ui/schedule-menu.js"
135
+ import { selectItem } from "./ui/select-item.js"
124
136
  import {
125
137
  addUsage,
126
138
  getLifetimeTotal,
@@ -362,6 +374,43 @@ function buildNotificationDetails(
362
374
  }
363
375
  }
364
376
 
377
+ /**
378
+ * Format an agent's tool scope for the Agent tool description.
379
+ *
380
+ * This suffix describes BUILT-IN scope only — extension tools are resolved when
381
+ * the agent runs (extensions can register asynchronously), so they cannot be
382
+ * enumerated while the description is being built. That is why an agent with
383
+ * `tools: "*, ext:mcp/search"` renders "*" and always has.
384
+ *
385
+ * Two distinctions matter, both of them capability claims the orchestrator acts on:
386
+ *
387
+ * - absent vs empty. `builtinToolNames: undefined` means the agent never narrowed
388
+ * its tools (the shipped defaults); `[]` is what `tools: none` and an `ext:`-only
389
+ * `tools:` parse to, and the runtime really does hand those agents no built-ins.
390
+ * Rendering both "*" tells the orchestrator a tool-less agent can run `bash`.
391
+ * - empty-with-extensions vs empty-without. Zero built-ins does NOT imply zero
392
+ * tools: `tools: none` alongside `extensions:` still surfaces every extension
393
+ * tool (see test/fixtures/.pi/agents/tools-none.md, which expects three). Calling
394
+ * that "none" understates the agent instead of overstating it — better, but still
395
+ * wrong, and it would route work away from the only agent able to do it. "none"
396
+ * is therefore reserved for agents that genuinely can call nothing: `isolated`
397
+ * agents and those with `extensions: false`.
398
+ */
399
+ export function formatToolsSuffix(cfg: AgentConfig | undefined): string {
400
+ const tools = cfg?.builtinToolNames
401
+ if (!tools) return "*"
402
+ if (tools.length === 0) {
403
+ // `isolated` overrides extensions to false in the runner, so both mean the
404
+ // agent has no extension tools either — and then it truly has nothing.
405
+ const noExtensionTools = cfg?.isolated === true || cfg?.extensions === false
406
+ return noExtensionTools ? "none" : "no built-ins, extension tools only"
407
+ }
408
+ const isFullSet =
409
+ tools.length === BUILTIN_TOOL_NAMES.length &&
410
+ BUILTIN_TOOL_NAMES.every((t) => tools.includes(t))
411
+ return isFullSet ? "*" : tools.join(", ")
412
+ }
413
+
365
414
  export default function (pi: ExtensionAPI) {
366
415
  // Child AgentSessions load normal extensions. Re-entering this extension there
367
416
  // would create another manager and leak handlers. Nested orchestration is
@@ -640,6 +689,14 @@ export default function (pi: ExtensionAPI) {
640
689
  undefined,
641
690
  (record) => {
642
691
  if (record.parentAgentId) return
692
+ // Agent-tool spawns refresh these surfaces in their tool handler, but RPC
693
+ // and scheduler spawns enter through the manager directly.
694
+ if (currentCtx?.hasUI) {
695
+ widget.ensureTimer()
696
+ widget.update()
697
+ fleet.ensureTimer()
698
+ fleet.update()
699
+ }
643
700
  // Emit started event when agent transitions to running (including from queue)
644
701
  pi.events.emit("subagents:started", {
645
702
  id: record.id,
@@ -752,6 +809,10 @@ export default function (pi: ExtensionAPI) {
752
809
  // bound session_start, so a filtered-out activation never advertises (#142).
753
810
  pi.on("session_start", async (_event, ctx) => {
754
811
  currentCtx = ctx
812
+ if (ctx.hasUI) {
813
+ widget.setUICtx(ctx.ui)
814
+ fleet.setUICtx(ctx.ui as any)
815
+ }
755
816
  manager.clearCompleted(true)
756
817
  // Guard mirrors the `!scheduler.isActive()` pattern below: session_start
757
818
  // fires once per activation, but a double-bind must not leak listeners.
@@ -935,16 +996,6 @@ export default function (pi: ExtensionAPI) {
935
996
  widget.onTurnStart()
936
997
  })
937
998
 
938
- /** Format an agent's tool scope: "*" when it has all built-ins, else a comma-separated list. */
939
- const formatToolsSuffix = (cfg: AgentConfig | undefined): string => {
940
- const tools = cfg?.builtinToolNames
941
- if (!tools || tools.length === 0) return "*"
942
- const isFullSet =
943
- tools.length === BUILTIN_TOOL_NAMES.length &&
944
- BUILTIN_TOOL_NAMES.every((t) => tools.includes(t))
945
- return isFullSet ? "*" : tools.join(", ")
946
- }
947
-
948
999
  /** Build the full type list text dynamically from available agents only. */
949
1000
  const buildTypeListText = () => {
950
1001
  const available = getAvailableTypes()
@@ -1195,7 +1246,7 @@ Terse command-style prompts produce shallow, generic work.
1195
1246
  resume: Type.Optional(
1196
1247
  Type.String({
1197
1248
  description:
1198
- "Optional agent ID to resume from. Continues from previous context.",
1249
+ "Optional agent ID to resume from. Continues from previous context. Combine with run_in_background to resume detached and be notified on completion. An agent can only be resumed once its current run has finished — use steer_subagent to reach one mid-run.",
1199
1250
  }),
1200
1251
  ),
1201
1252
  isolated: Type.Optional(
@@ -1222,20 +1273,34 @@ Terse command-style prompts produce shallow, generic work.
1222
1273
  // ---- Custom rendering: Claude Code style ----
1223
1274
 
1224
1275
  renderCall(args, theme, context) {
1225
- let restoreBackground = theme.getBgAnsi("toolSuccessBg")
1226
- if (context.isPartial)
1227
- restoreBackground = theme.getBgAnsi("toolPendingBg")
1228
- else if (context.isError)
1229
- restoreBackground = theme.getBgAnsi("toolErrorBg")
1230
-
1276
+ // A badge closes its own background, which would clear the tool block's row tint
1277
+ // for the rest of the line, so the badge restores it. The tint is opened here too:
1278
+ // the TUI's Box paints it, but HTML export takes it from CSS, and restoring a
1279
+ // background the line never opened is what banded the export before. The line is
1280
+ // deliberately left open — Box.applyBackgroundToLine pads to width and *then*
1281
+ // wraps, so closing here would leave that padding untinted, and HTML export closes
1282
+ // any open span per line anyway. No badge means no tint, so an uncolored agent
1283
+ // renders exactly the line it always did.
1284
+ const rowBackground = hasAgentBadge(args.subagent_type)
1285
+ ? theme.getBgAnsi(
1286
+ context.isPartial
1287
+ ? "toolPendingBg"
1288
+ : context.isError
1289
+ ? "toolErrorBg"
1290
+ : "toolSuccessBg",
1291
+ )
1292
+ : ""
1231
1293
  const desc = args.description ?? ""
1232
1294
  const name = renderAgentName(args.subagent_type, theme, {
1233
1295
  fallbackColor: "toolTitle",
1234
- restoreBackground,
1296
+ restoreBackground: rowBackground,
1235
1297
  bold: true,
1236
1298
  })
1237
1299
  return new Text(
1238
- "▸ " + name + (desc ? " " + theme.fg("muted", desc) : ""),
1300
+ rowBackground +
1301
+ "▸ " +
1302
+ name +
1303
+ (desc ? " " + theme.fg("muted", desc) : ""),
1239
1304
  0,
1240
1305
  0,
1241
1306
  )
@@ -1555,6 +1620,144 @@ Terse command-style prompts produce shallow, generic work.
1555
1620
  `Agent "${params.resume}" has no active session to resume.`,
1556
1621
  )
1557
1622
  }
1623
+
1624
+ // Background resume: detached run that notifies on completion, mirroring
1625
+ // a background spawn. Previously run_in_background was silently ignored
1626
+ // on resume (this branch returned before the background branch below),
1627
+ // so a resumed agent always blocked the main loop until it finished.
1628
+ if (runInBackground) {
1629
+ const id = existing.id
1630
+ // A detached resume hands control back while the record stays
1631
+ // "running", so nothing stops the model from resuming the same agent
1632
+ // again mid-run. manager.resume() refuses that (it would orphan the
1633
+ // live run's abort controller); say why here, where the model can act
1634
+ // on it, instead of letting it read as a generic failure.
1635
+ if (existing.status === "running" || existing.status === "queued") {
1636
+ return textResult(
1637
+ `Agent "${params.resume}" is still ${existing.status} — it can only be resumed once its current run finishes.\n` +
1638
+ `Use steer_subagent to send it a message mid-run, or get_subagent_result to wait for it.`,
1639
+ )
1640
+ }
1641
+
1642
+ const joinMode = resolveJoinMode(defaultJoinMode, true)
1643
+ existing.toolCallId = toolCallId
1644
+ if (joinMode) existing.joinMode = joinMode
1645
+ // Reuse the agent's transcript rather than starting a fresh one: the
1646
+ // path is deterministic per agent+session, so writing an initial entry
1647
+ // would truncate the previous run's turns (see ensureOutputFile).
1648
+ if (existing.outputFile) {
1649
+ // Preserve the original transcript path across parent session
1650
+ // switches; records intentionally survive those switches for
1651
+ // resume. Only create a path for an older run that never wrote
1652
+ // one and is now being resumed with transcripts enabled.
1653
+ ensureOutputFile(existing.outputFile)
1654
+ } else if (outputTranscript) {
1655
+ existing.outputFile = createOutputFilePath(
1656
+ ctx.cwd,
1657
+ id,
1658
+ ctx.sessionManager.getSessionId(),
1659
+ )
1660
+ ensureOutputFile(existing.outputFile)
1661
+ }
1662
+ // Anchor streaming past the turns already on disk, captured BEFORE the
1663
+ // run starts. The resumed prompt lands as an ordinary user message at
1664
+ // this index, so it is written exactly once.
1665
+ const transcriptAnchor = existing.session.messages.length
1666
+
1667
+ const { state: bgState, callbacks: bgCallbacks } =
1668
+ createActivityTracker(effectiveMaxTurns)
1669
+ // resumeAgent has no onSessionCreated — the session predates this run —
1670
+ // so seed it directly, or the widget shows no context % for the agent.
1671
+ bgState.session = existing.session
1672
+
1673
+ // No `signal`: a background spawn deliberately omits it, and a detached
1674
+ // resume must behave the same. Passing it would abort this agent when
1675
+ // the parent turn is interrupted (user Esc), while agents started with
1676
+ // run_in_background in that same turn keep going.
1677
+ const record = await manager.resume(
1678
+ params.resume,
1679
+ params.prompt,
1680
+ undefined,
1681
+ {
1682
+ isBackground: true,
1683
+ onToolActivity: bgCallbacks.onToolActivity,
1684
+ onTurnEnd: bgCallbacks.onTurnEnd,
1685
+ onAssistantUsage: bgCallbacks.onAssistantUsage,
1686
+ // Fires when the run actually starts — immediately, or on queue
1687
+ // drain. Wiring it here (rather than after resume() returns) means a
1688
+ // resume stopped while still queued never started streaming, so
1689
+ // there is no subscription left behind for a later run to trip over.
1690
+ onStarted: () => {
1691
+ const rec = manager.getRecord(id)
1692
+ if (rec?.session && rec.outputFile) {
1693
+ rec.outputCleanup = streamToOutputFile(
1694
+ rec.session,
1695
+ rec.outputFile,
1696
+ id,
1697
+ ctx.cwd,
1698
+ transcriptAnchor,
1699
+ )
1700
+ }
1701
+ },
1702
+ },
1703
+ )
1704
+ if (!record) {
1705
+ return textResult(`Failed to resume agent "${params.resume}".`)
1706
+ }
1707
+
1708
+ if (joinMode != null && joinMode !== "async") {
1709
+ currentBatchAgents.push({ id, joinMode })
1710
+ if (batchFinalizeTimer) clearTimeout(batchFinalizeTimer)
1711
+ batchFinalizeTimer = setTimeout(finalizeBatch, 100)
1712
+ }
1713
+
1714
+ agentActivity.set(id, bgState)
1715
+ // This agent already finished once, so the widget holds a finished-age
1716
+ // for it that is past the linger limit — without clearing it, the
1717
+ // resumed run's ✓/✗ line never renders and the agent just vanishes.
1718
+ widget.markRunning(id)
1719
+ widget.ensureTimer()
1720
+ widget.update()
1721
+ fleet.ensureTimer()
1722
+ fleet.update()
1723
+
1724
+ // Resume ignores subagent_type (the record keeps the type it was
1725
+ // spawned with), so report the record's own identity — a "created"
1726
+ // event carrying the caller's type would re-register the agent under
1727
+ // the wrong one in cross-extension mirrors keyed by id.
1728
+ pi.events.emit("subagents:created", {
1729
+ id,
1730
+ type: existing.type,
1731
+ description: existing.description,
1732
+ isBackground: true,
1733
+ })
1734
+
1735
+ const isQueued = record.status === "queued"
1736
+ return textResult(
1737
+ `Agent ${isQueued ? "queued" : "resumed"} in background.\n` +
1738
+ `Agent ID: ${id}\n` +
1739
+ `Type: ${existing.type}\n` +
1740
+ (record.outputFile
1741
+ ? `Output file: ${record.outputFile}\n`
1742
+ : "") +
1743
+ (isQueued
1744
+ ? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n`
1745
+ : "") +
1746
+ `\nYou will be notified when this agent completes.\n` +
1747
+ `Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.`,
1748
+ {
1749
+ ...detailBase,
1750
+ subagentType: existing.type,
1751
+ displayName: existing.type,
1752
+ toolUses: record.toolUses,
1753
+ tokens: "",
1754
+ durationMs: 0,
1755
+ status: "background" as const,
1756
+ agentId: id,
1757
+ },
1758
+ )
1759
+ }
1760
+
1558
1761
  const record = await manager.resume(
1559
1762
  params.resume,
1560
1763
  params.prompt,
@@ -1992,27 +2195,9 @@ Terse command-style prompts produce shallow, generic work.
1992
2195
 
1993
2196
  // ---- /agents interactive menu ----
1994
2197
 
1995
- const projectAgentsDir = () => join(process.cwd(), ".pi", "agents")
1996
- const workspaceAgentsDir = () => join(process.cwd(), ".agents", "agents")
1997
- const personalAgentsDir = () => join(getAgentDir(), "agents")
1998
-
1999
- /** Find the file path of a custom agent by name, in discovery-precedence order (project, workspace, then global). */
2000
- function findAgentFile(
2001
- name: string,
2002
- ):
2003
- | { path: string; location: "project" | "workspace" | "personal" }
2004
- | undefined {
2005
- const projectPath = join(projectAgentsDir(), `${name}.md`)
2006
- if (existsSync(projectPath))
2007
- return { path: projectPath, location: "project" }
2008
- const workspacePath = join(workspaceAgentsDir(), `${name}.md`)
2009
- if (existsSync(workspacePath))
2010
- return { path: workspacePath, location: "workspace" }
2011
- const personalPath = join(personalAgentsDir(), `${name}.md`)
2012
- if (existsSync(personalPath))
2013
- return { path: personalPath, location: "personal" }
2014
- return undefined
2015
- }
2198
+ // Directory resolution and the frontmatter edits live in agent-file-toggle.ts
2199
+ // so they are reachable from tests — this command handler is only registered
2200
+ // through `registerCommand`, which every test mocks.
2016
2201
 
2017
2202
  function getModelLabel(type: string, registry?: ModelRegistry): string {
2018
2203
  const cfg = getAgentConfig(type)
@@ -2189,19 +2374,15 @@ Terse command-style prompts produce shallow, generic work.
2189
2374
  return
2190
2375
  }
2191
2376
 
2192
- const options = agents.map((a) => {
2377
+ // Numbered + item-paired. Two same-type agents spawned together with the
2378
+ // same description render identically here, and resolving the choice by
2379
+ // string match would open whichever came first.
2380
+ const record = await selectItem(ctx.ui, "Running agents", agents, (a) => {
2193
2381
  const dn = getDisplayName(a.type)
2194
2382
  const dur = formatDuration(a.startedAt, a.completedAt)
2195
2383
  return `${dn} (${a.description}) · ${a.toolUses} tools · ${a.status} · ${dur}`
2196
2384
  })
2197
-
2198
- const choice = await ctx.ui.select("Running agents", options)
2199
- if (!choice) return
2200
-
2201
- // Find the selected agent by matching the option index
2202
- const idx = options.indexOf(choice)
2203
- if (idx < 0) return
2204
- const record = agents[idx]
2385
+ if (!record) return
2205
2386
 
2206
2387
  await viewAgentConversation(ctx, record)
2207
2388
  // Back-navigation: re-show the list
@@ -2351,40 +2532,7 @@ Terse command-style prompts produce shallow, generic work.
2351
2532
  if (!overwrite) return
2352
2533
  }
2353
2534
 
2354
- // Build the .md file content
2355
- const fmFields: string[] = []
2356
- fmFields.push(`description: ${JSON.stringify(cfg.description)}`)
2357
- if (cfg.displayName) fmFields.push(`display_name: ${cfg.displayName}`)
2358
- if (cfg.color) fmFields.push(`color: ${JSON.stringify(cfg.color)}`)
2359
- fmFields.push(`tools: ${cfg.builtinToolNames?.join(", ") || "all"}`)
2360
- if (cfg.model) fmFields.push(`model: ${cfg.model}`)
2361
- if (cfg.thinking) fmFields.push(`thinking: ${cfg.thinking}`)
2362
- if (cfg.maxTurns) fmFields.push(`max_turns: ${cfg.maxTurns}`)
2363
- if (cfg.allowedSubagents !== undefined) {
2364
- fmFields.push(
2365
- `allowed_subagents: ${cfg.allowedSubagents === "all" ? "all" : cfg.allowedSubagents.join(", ")}`,
2366
- )
2367
- }
2368
- fmFields.push(`prompt_mode: ${cfg.promptMode}`)
2369
- if (cfg.extensions === false) fmFields.push("extensions: false")
2370
- else if (Array.isArray(cfg.extensions))
2371
- fmFields.push(`extensions: ${cfg.extensions.join(", ")}`)
2372
- if (cfg.excludeExtensions?.length)
2373
- fmFields.push(`exclude_extensions: ${cfg.excludeExtensions.join(", ")}`)
2374
- if (cfg.skills === false) fmFields.push("skills: false")
2375
- else if (Array.isArray(cfg.skills))
2376
- fmFields.push(`skills: ${cfg.skills.join(", ")}`)
2377
- if (cfg.disallowedTools?.length)
2378
- fmFields.push(`disallowed_tools: ${cfg.disallowedTools.join(", ")}`)
2379
- if (cfg.inheritContext) fmFields.push("inherit_context: true")
2380
- if (cfg.runInBackground) fmFields.push("run_in_background: true")
2381
- if (cfg.outputTranscript === false)
2382
- fmFields.push("output_transcript: false")
2383
- if (cfg.isolated) fmFields.push("isolated: true")
2384
- if (cfg.memory) fmFields.push(`memory: ${cfg.memory}`)
2385
- if (cfg.isolation) fmFields.push(`isolation: ${cfg.isolation}`)
2386
-
2387
- const content = `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`
2535
+ const content = serializeAgentFile(cfg)
2388
2536
 
2389
2537
  const { writeFileSync } = await import("node:fs")
2390
2538
  writeFileSync(targetPath, content, "utf-8")
@@ -2398,11 +2546,20 @@ Terse command-style prompts produce shallow, generic work.
2398
2546
  if (file) {
2399
2547
  // Existing file — set enabled: false in frontmatter (idempotent)
2400
2548
  const content = readFileSync(file.path, "utf-8")
2401
- if (content.includes("\nenabled: false\n")) {
2549
+ const { content: updated, outcome } = disableInContent(content)
2550
+ if (outcome === "already-disabled") {
2402
2551
  ctx.ui.notify(`${name} is already disabled.`, "info")
2403
2552
  return
2404
2553
  }
2405
- const updated = content.replace(/^---\n/, "---\nenabled: false\n")
2554
+ if (outcome === "no-frontmatter") {
2555
+ // Nothing to edit — say so rather than rewriting the file unchanged and
2556
+ // reporting success for a change that never happened.
2557
+ ctx.ui.notify(
2558
+ `Cannot disable ${name}: ${file.path} has no frontmatter block.`,
2559
+ "error",
2560
+ )
2561
+ return
2562
+ }
2406
2563
  const { writeFileSync } = await import("node:fs")
2407
2564
  writeFileSync(file.path, updated, "utf-8")
2408
2565
  reloadCustomAgents()
@@ -2435,11 +2592,17 @@ Terse command-style prompts produce shallow, generic work.
2435
2592
  if (!file) return
2436
2593
 
2437
2594
  const content = readFileSync(file.path, "utf-8")
2438
- const updated = content.replace(/^(---\n)enabled: false\n/, "$1")
2595
+ const { content: updated, changed } = enableInContent(content)
2596
+ if (!changed && !isEmptyStub(updated)) {
2597
+ // The file carries no `enabled: false` to remove, so it was never disabled
2598
+ // by us — reporting success here would hide a no-op.
2599
+ ctx.ui.notify(`${name} is not disabled in ${file.path}.`, "info")
2600
+ return
2601
+ }
2439
2602
  const { writeFileSync } = await import("node:fs")
2440
2603
 
2441
2604
  // If the file was just a stub ("---\n---\n"), delete it to restore the built-in default
2442
- if (updated.trim() === "---\n---" || updated.trim() === "---\n---\n") {
2605
+ if (isEmptyStub(updated)) {
2443
2606
  unlinkSync(file.path)
2444
2607
  reloadCustomAgents()
2445
2608
  ctx.ui.notify(`Enabled ${name} (removed ${file.path})`, "info")
@@ -2507,7 +2670,7 @@ The file format is a markdown file with YAML frontmatter and a system prompt bod
2507
2670
  ---
2508
2671
  name: <optional UI display name; Claude Code-compatible alias for display_name>
2509
2672
  description: <one-line description shown in UI>
2510
- color: <optional name badge color: red, blue, green, yellow, purple, orange, pink, cyan, an Agency Agents alias, or quoted "#RRGGBB">
2673
+ color: <optional agent name badge color: red, blue, green, yellow, purple, orange, pink, cyan, an Agency Agents alias, or quoted "#RRGGBB">
2511
2674
  tools: <comma-separated built-in tools: read, bash, edit, write, grep, find, ls. Use "none" for no tools. Omit for all tools>
2512
2675
  model: <optional model as "provider/modelId", e.g. "anthropic/claude-haiku-4-5". Omit to inherit parent model>
2513
2676
  thinking: <optional thinking level: ${THINKING_LEVELS.join(", ")}. Omit to inherit>
@@ -2614,16 +2777,12 @@ Write the file using the write tool. Only write the file, nothing else.`
2614
2777
  ])
2615
2778
  if (!modelChoice) return
2616
2779
 
2617
- let modelLine = ""
2618
- if (modelChoice === "haiku")
2619
- modelLine = "\nmodel: anthropic/claude-haiku-4-5"
2620
- else if (modelChoice === "sonnet")
2621
- modelLine = "\nmodel: anthropic/claude-sonnet-4-6"
2622
- else if (modelChoice === "opus")
2623
- modelLine = "\nmodel: anthropic/claude-opus-4-6"
2780
+ let model: string | undefined
2781
+ if (modelChoice === "haiku") model = "anthropic/claude-haiku-4-5"
2782
+ else if (modelChoice === "sonnet") model = "anthropic/claude-sonnet-4-6"
2783
+ else if (modelChoice === "opus") model = "anthropic/claude-opus-4-6"
2624
2784
  else if (modelChoice === "custom...") {
2625
- const customModel = await ctx.ui.input("Model (provider/modelId)")
2626
- if (customModel) modelLine = `\nmodel: ${customModel}`
2785
+ model = (await ctx.ui.input("Model (provider/modelId)")) || undefined
2627
2786
  }
2628
2787
 
2629
2788
  // 5. Thinking
@@ -2634,23 +2793,17 @@ Write the file using the write tool. Only write the file, nothing else.`
2634
2793
  ])
2635
2794
  if (!thinkingChoice) return
2636
2795
 
2637
- let thinkingLine = ""
2638
- if (thinkingChoice !== "inherit")
2639
- thinkingLine = `\nthinking: ${thinkingChoice}`
2640
-
2641
2796
  // 6. System prompt
2642
2797
  const systemPrompt = await ctx.ui.editor("System prompt", "")
2643
2798
  if (systemPrompt === undefined) return
2644
2799
 
2645
- // Build the file
2646
- const content = `---
2647
- description: ${description}
2648
- tools: ${tools}${modelLine}${thinkingLine}
2649
- prompt_mode: replace
2650
- ---
2651
-
2652
- ${systemPrompt}
2653
- `
2800
+ const content = buildNewAgentFile({
2801
+ description,
2802
+ tools,
2803
+ model,
2804
+ thinking: thinkingChoice === "inherit" ? undefined : thinkingChoice,
2805
+ systemPrompt,
2806
+ })
2654
2807
 
2655
2808
  mkdirSync(targetDir, { recursive: true })
2656
2809
  const targetPath = join(targetDir, `${name}.md`)
@@ -2669,7 +2822,16 @@ ${systemPrompt}
2669
2822
  ctx.ui.notify(`Created ${targetPath}`, "info")
2670
2823
  }
2671
2824
 
2672
- function snapshotSettings(): SubagentsSettings {
2825
+ /**
2826
+ * Every settings mutation writes this WHOLE object back to disk, so a field
2827
+ * missing here is erased from the user's subagents.json the next time they
2828
+ * toggle something unrelated. `SubagentsSettings` has every field optional,
2829
+ * so a `: SubagentsSettings` return annotation would let a newly-added setting
2830
+ * be forgotten here and still type-check. `satisfies` instead: it still checks
2831
+ * each value's type and rejects a mistyped key, but leaves the return type
2832
+ * inferred so `_NoMissingSettingsKeys` below can check completeness.
2833
+ */
2834
+ function snapshotSettings() {
2673
2835
  return {
2674
2836
  maxConcurrent: manager.getMaxConcurrent(),
2675
2837
  // 0 = unlimited — per SubagentsSettings.defaultMaxTurns docstring and
@@ -2691,9 +2853,23 @@ ${systemPrompt}
2691
2853
  // explicit configuration — which then fails loudly if general-purpose later
2692
2854
  // goes away. undefined is dropped by JSON.stringify.
2693
2855
  fallbackSubagent: getFallbackSubagent(),
2694
- }
2856
+ } satisfies SubagentsSettings
2695
2857
  }
2696
2858
 
2859
+ // Compile-time completeness guard for snapshotSettings(). If a field is added
2860
+ // to SubagentsSettings and not mirrored above, this Exclude is non-empty and
2861
+ // fails to satisfy `never` — turning a silent settings-erasure bug into a
2862
+ // typecheck error. `npm run typecheck` runs in CI.
2863
+ type _NoMissingSettingsKeys =
2864
+ Exclude<
2865
+ keyof SubagentsSettings,
2866
+ keyof ReturnType<typeof snapshotSettings>
2867
+ > extends never
2868
+ ? true
2869
+ : ["snapshotSettings() is missing a SubagentsSettings key"]
2870
+ const _settingsSnapshotIsComplete: _NoMissingSettingsKeys = true
2871
+ void _settingsSnapshotIsComplete
2872
+
2697
2873
  const NUMERIC_IDS = new Set([
2698
2874
  "maxConcurrent",
2699
2875
  "defaultMaxTurns",
@@ -66,6 +66,23 @@ export function createOutputFilePath(
66
66
  return join(dir, `${agentId}.output`)
67
67
  }
68
68
 
69
+ /**
70
+ * Ensure a transcript file exists without disturbing what is already in it.
71
+ *
72
+ * A resume reuses the agent's existing transcript (same deterministic path), so
73
+ * it must never call `writeInitialEntry` — that truncates, discarding turns the
74
+ * completion notification still points the user at, and any history the session
75
+ * has since compacted away is gone for good. Appending nothing creates the file
76
+ * when this is the agent's first transcript and is a no-op when it is not.
77
+ */
78
+ export function ensureOutputFile(path: string): void {
79
+ try {
80
+ appendFileSync(path, "", "utf-8")
81
+ } catch {
82
+ /* ignore — streaming writes are best-effort too */
83
+ }
84
+ }
85
+
69
86
  /** Write the initial user prompt entry. */
70
87
  export function writeInitialEntry(
71
88
  path: string,
@@ -93,8 +110,14 @@ export function streamToOutputFile(
93
110
  path: string,
94
111
  agentId: string,
95
112
  cwd: string,
113
+ startIndex?: number,
96
114
  ): () => void {
97
- let writtenCount = 1 // initial user prompt already written
115
+ // Index of the first message this stream is responsible for. A spawn writes
116
+ // messages[0] as the initial prompt entry, so it starts at 1. A resume hands
117
+ // in the session's length as of just before the run: the session already
118
+ // holds every prior turn, and re-emitting those would duplicate history that
119
+ // is already in the file.
120
+ let writtenCount = startIndex ?? 1
98
121
 
99
122
  const flush = () => {
100
123
  const messages = session.messages
@@ -358,6 +358,17 @@ export class AgentWidget {
358
358
  }
359
359
  }
360
360
 
361
+ /**
362
+ * Drop an agent's finished-age (call when a settled agent starts running
363
+ * again, i.e. a background resume). markFinished only seeds an age it has not
364
+ * seen before, so a resumed agent would otherwise keep the age from its
365
+ * previous run — already past the linger limit, hiding the new run's
366
+ * completion line entirely.
367
+ */
368
+ markRunning(agentId: string) {
369
+ this.finishedTurnAge.delete(agentId)
370
+ }
371
+
361
372
  /** Render a finished agent line. */
362
373
  private renderFinishedLine(
363
374
  a: {
@@ -536,6 +547,16 @@ export class AgentWidget {
536
547
  let hiddenRunning = 0
537
548
  let hiddenFinished = 0
538
549
 
550
+ // Reserve the queued line's row up front. It is a single summary of N
551
+ // waiting agents, so it cannot be folded into the "+N more" count (which
552
+ // is denominated in agents) without either under-reporting it as 1 or
553
+ // inflating the total with agents that were never getting their own rows.
554
+ // Reserving costs at most one running agent — which IS counted below —
555
+ // and makes the drop unreachable. It matters most exactly when it used to
556
+ // vanish: the pool is saturated and the queue is what the user needs to see.
557
+ const queuedReserve = queuedLine ? 1 : 0
558
+ budget -= queuedReserve
559
+
539
560
  // 1. Running agents (2 lines each)
540
561
  for (const pair of runningLines) {
541
562
  if (budget >= 2) {
@@ -546,8 +567,9 @@ export class AgentWidget {
546
567
  }
547
568
  }
548
569
 
549
- // 2. Queued line
550
- if (queuedLine && budget >= 1) {
570
+ // 2. Queued line (always fits — its row was reserved above)
571
+ if (queuedLine) {
572
+ budget += queuedReserve
551
573
  lines.push(queuedLine)
552
574
  budget--
553
575
  }