@herbertgao/pi-extensions 2026.9.14 → 2026.9.16

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 (206) hide show
  1. package/README.md +8 -3
  2. package/THIRD_PARTY_NOTICES.md +28 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
  4. package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -6
  5. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  6. package/node_modules/@narumitw/pi-btw/package.json +4 -4
  7. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +2 -0
  8. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +16 -4
  9. package/node_modules/pi-jev-auto-mode/CHANGELOG.md +8 -0
  10. package/node_modules/pi-jev-auto-mode/README.md +7 -0
  11. package/node_modules/pi-jev-auto-mode/package.json +1 -1
  12. package/node_modules/pi-jev-auto-mode/src/extension.ts +21 -2
  13. package/node_modules/pi-jev-auto-mode/src/records.ts +68 -2
  14. package/node_modules/pi-jev-auto-mode/src/settings.ts +22 -0
  15. package/node_modules/pi-jev-auto-mode/src/ui.ts +10 -0
  16. package/node_modules/pi-lens/CHANGELOG.md +498 -0
  17. package/node_modules/pi-lens/README.md +45 -2
  18. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +23 -7
  19. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +187 -51
  20. package/node_modules/pi-lens/dist/clients/cache-manager.js +64 -5
  21. package/node_modules/pi-lens/dist/clients/child-unref.js +17 -8
  22. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
  23. package/node_modules/pi-lens/dist/clients/config-locations.js +237 -9
  24. package/node_modules/pi-lens/dist/clients/config-resolve.js +7 -1
  25. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +33 -3
  26. package/node_modules/pi-lens/dist/clients/dependency-checker.js +87 -64
  27. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +39 -3
  28. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +118 -1
  29. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +8 -2
  30. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +23 -4
  31. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +14 -7
  32. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +26 -28
  33. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +38 -25
  34. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +32 -49
  35. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +16 -14
  36. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +25 -7
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +28 -47
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +11 -4
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +27 -22
  40. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +31 -46
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +29 -46
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +91 -11
  43. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +16 -2
  44. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +38 -23
  45. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +27 -15
  46. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +17 -8
  47. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +31 -21
  48. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +34 -60
  49. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +6 -5
  50. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +12 -5
  51. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
  52. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +22 -6
  53. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +39 -16
  54. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +1 -1
  55. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +27 -18
  56. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +18 -19
  57. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +6 -1
  58. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +6 -2
  59. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -3
  60. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +11 -2
  61. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +10 -2
  62. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +11 -2
  63. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +12 -6
  64. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +30 -15
  65. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +23 -0
  66. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +8 -2
  67. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
  68. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +15 -3
  69. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +12 -2
  70. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +27 -39
  71. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +13 -3
  72. package/node_modules/pi-lens/dist/clients/effective-config.js +3 -1
  73. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +42 -11
  74. package/node_modules/pi-lens/dist/clients/formatters.js +49 -11
  75. package/node_modules/pi-lens/dist/clients/git-guard.js +247 -34
  76. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +121 -25
  77. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +186 -0
  78. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +19 -6
  79. package/node_modules/pi-lens/dist/clients/installer/index.js +281 -32
  80. package/node_modules/pi-lens/dist/clients/jscpd-client.js +8 -0
  81. package/node_modules/pi-lens/dist/clients/latency-logger.js +8 -0
  82. package/node_modules/pi-lens/dist/clients/lens-config.js +66 -8
  83. package/node_modules/pi-lens/dist/clients/lsp/client.js +139 -11
  84. package/node_modules/pi-lens/dist/clients/lsp/config.js +9 -7
  85. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +41 -0
  86. package/node_modules/pi-lens/dist/clients/lsp/index.js +1 -1
  87. package/node_modules/pi-lens/dist/clients/lsp/launch.js +23 -2
  88. package/node_modules/pi-lens/dist/clients/lsp/server.js +106 -25
  89. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +14 -0
  90. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +105 -16
  91. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +232 -44
  92. package/node_modules/pi-lens/dist/clients/mcp/review.js +53 -7
  93. package/node_modules/pi-lens/dist/clients/opengrep-client.js +11 -6
  94. package/node_modules/pi-lens/dist/clients/pipeline.js +121 -66
  95. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
  96. package/node_modules/pi-lens/dist/clients/project-lens-config.js +11 -3
  97. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +34 -26
  98. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +16 -10
  99. package/node_modules/pi-lens/dist/clients/ruff-client.js +31 -4
  100. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +38 -4
  101. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +16 -5
  102. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +27 -12
  103. package/node_modules/pi-lens/dist/clients/runtime-turn.js +447 -280
  104. package/node_modules/pi-lens/dist/clients/safe-spawn.js +256 -65
  105. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +84 -1
  106. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +119 -58
  107. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +51 -0
  108. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +112 -0
  109. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +204 -0
  110. package/node_modules/pi-lens/dist/clients/warm-attach.js +59 -0
  111. package/node_modules/pi-lens/dist/clients/widget-state.js +145 -14
  112. package/node_modules/pi-lens/dist/clients/workspace-topology.js +57 -3
  113. package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -13
  114. package/node_modules/pi-lens/dist/index.js +5695 -3733
  115. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +4 -0
  116. package/node_modules/pi-lens/dist/mcp/server.js +33 -21
  117. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  118. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +43 -16
  119. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +118 -7
  120. package/node_modules/pi-lens/docs/adr/0001-stale-advisory-live-arm.md +29 -0
  121. package/node_modules/pi-lens/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
  122. package/node_modules/pi-lens/docs/adr/0003-git-guard-latch-writer.md +203 -0
  123. package/node_modules/pi-lens/docs/adr/0004-disposition-policy-seam.md +38 -0
  124. package/node_modules/pi-lens/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
  125. package/node_modules/pi-lens/docs/adr/0006-derived-state-benchmark-first.md +31 -0
  126. package/node_modules/pi-lens/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
  127. package/node_modules/pi-lens/docs/adr/0008-turn-end-lane-interface.md +165 -0
  128. package/node_modules/pi-lens/docs/adr/0009-reported-path-attribution.md +114 -0
  129. package/node_modules/pi-lens/docs/adr/README.md +26 -0
  130. package/node_modules/pi-lens/docs/agent-tools.md +18 -1
  131. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +4 -3
  132. package/node_modules/pi-lens/docs/configuration.md +55 -1
  133. package/node_modules/pi-lens/docs/engineering-principles.md +202 -0
  134. package/node_modules/pi-lens/docs/environment-variables.md +61 -4
  135. package/node_modules/pi-lens/docs/features.md +2 -2
  136. package/node_modules/pi-lens/docs/globalconfig.md +6 -1
  137. package/node_modules/pi-lens/docs/language-coverage.md +17 -0
  138. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +98 -31
  139. package/node_modules/pi-lens/docs/pi-lens-fixer.md +57 -3
  140. package/node_modules/pi-lens/docs/pi-lens-investigator.md +9 -0
  141. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +16 -0
  142. package/node_modules/pi-lens/docs/pi-lens-subagent.md +9 -0
  143. package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
  144. package/node_modules/pi-lens/docs/release-qa-baseline.md +2 -0
  145. package/node_modules/pi-lens/docs/servercapabilities.md +21 -11
  146. package/node_modules/pi-lens/docs/settings.md +12 -3
  147. package/node_modules/pi-lens/docs/subagent-compat.md +1 -1
  148. package/node_modules/pi-lens/package.json +5 -3
  149. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
  150. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
  151. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
  152. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
  153. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
  154. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
  155. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
  156. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
  157. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
  158. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
  159. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
  160. package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
  161. package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
  162. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
  163. package/node_modules/pi-lens/scripts/install-selftest.mjs +21 -17
  164. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  165. package/node_modules/pi-mcp-adapter/CHANGELOG.md +29 -0
  166. package/node_modules/pi-mcp-adapter/OAUTH.md +369 -0
  167. package/node_modules/pi-mcp-adapter/README.md +12 -5
  168. package/node_modules/pi-mcp-adapter/config.ts +31 -8
  169. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +1 -1
  170. package/node_modules/pi-mcp-adapter/dist/config.js +33 -8
  171. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  172. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +15 -2
  173. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -1
  174. package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +2 -0
  175. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +27 -2
  176. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -1
  177. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +1 -1
  178. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  179. package/node_modules/pi-mcp-adapter/dist/server-manager.js +6 -4
  180. package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -1
  181. package/node_modules/pi-mcp-adapter/dist/types.d.ts +2 -2
  182. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +2 -0
  183. package/node_modules/pi-mcp-adapter/dist/utils.js +10 -3
  184. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  185. package/node_modules/pi-mcp-adapter/index.ts +37 -20
  186. package/node_modules/pi-mcp-adapter/init.ts +14 -15
  187. package/node_modules/pi-mcp-adapter/json-schema-validator.ts +16 -2
  188. package/node_modules/pi-mcp-adapter/mcp-auth.ts +27 -2
  189. package/node_modules/pi-mcp-adapter/metadata-cache.ts +1 -1
  190. package/node_modules/pi-mcp-adapter/package.json +3 -2
  191. package/node_modules/pi-mcp-adapter/proxy-modes.ts +1 -1
  192. package/node_modules/pi-mcp-adapter/server-manager.ts +6 -3
  193. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +99 -6
  194. package/node_modules/pi-mcp-adapter/types.ts +2 -2
  195. package/node_modules/pi-mcp-adapter/ui-session.ts +60 -27
  196. package/node_modules/pi-mcp-adapter/utils.ts +11 -3
  197. package/node_modules/pi-multi-account/CHANGELOG.md +6 -0
  198. package/node_modules/pi-multi-account/README.md +15 -1
  199. package/node_modules/pi-multi-account/index.ts +295 -41
  200. package/node_modules/pi-multi-account/package.json +1 -1
  201. package/node_modules/pi-next-cue/.github/workflows/publish.yml +20 -0
  202. package/node_modules/pi-next-cue/LICENSE +21 -0
  203. package/node_modules/pi-next-cue/README.md +81 -0
  204. package/node_modules/pi-next-cue/index.ts +358 -0
  205. package/node_modules/pi-next-cue/package.json +28 -0
  206. package/package.json +10 -7
@@ -342,7 +342,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
342
342
  const reportedDirectToolNamesByServer = new Map<string, Set<string>>();
343
343
  const registeredNamespaceProxyTools = new Set<string>();
344
344
  const fallbackDeactivatedTools = new Set<string>();
345
- // directTools: "search" — registered inactive, activated by mcp({ search }).
345
+ // directTools: "search" — registered inactive, activated by mcp({ search }) or a successful mcp({ tool }) call.
346
346
  const lazyDirectTools = new Set<string>();
347
347
  const searchActivatedTools = new Set<string>();
348
348
  const toolRenderOptions = resolveMcpToolRenderOptions(earlyConfig.settings);
@@ -450,10 +450,10 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
450
450
  }
451
451
 
452
452
  /**
453
- * Activate the lazy direct tools a search matched, additively. This is the
454
- * one place a search-mode tool becomes active; nothing is ever deactivated
455
- * here. Returns the names that actually changed state so the result can
456
- * report only real additions.
453
+ * Activate lazy direct tools additively — the ones a search matched, or the
454
+ * one a successful proxy call named. This is the one place a search-mode tool
455
+ * becomes active; nothing is ever deactivated here. Returns the names that
456
+ * actually changed state so the result can report only real additions.
457
457
  */
458
458
  function activateSearchMatches(matches: ReadonlyArray<{ server: string; tool: string }>): string[] {
459
459
  const activeTools = getActiveToolsIfReady();
@@ -502,17 +502,14 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
502
502
  const unregisterTool = (pi as ExtensionAPI & { unregisterTool?: (name: string) => boolean }).unregisterTool;
503
503
  const unregistered = toolNames.filter((toolName) => callReentrant(() => unregisterTool?.(toolName)) === true);
504
504
  const fallbackNames = toolNames.filter((toolName) => !unregistered.includes(toolName));
505
- const remove = new Set(toolNames);
506
505
  const activeTools = getActiveToolsIfReady();
507
- if (!activeTools || activeTools.length === 0) {
508
- for (const toolName of fallbackNames) fallbackDeactivatedTools.add(toolName);
509
- return unregistered;
510
- }
506
+ if (!activeTools) return unregistered;
507
+ const removedFallbackNames = fallbackNames.filter((name) => activeTools.includes(name));
508
+ if (removedFallbackNames.length === 0) return unregistered;
509
+ const remove = new Set(removedFallbackNames);
511
510
  const nextActiveTools = activeTools.filter((name) => !remove.has(name));
512
- if (nextActiveTools.length !== activeTools.length) {
513
- for (const toolName of fallbackNames) fallbackDeactivatedTools.add(toolName);
514
- callReentrant(() => pi.setActiveTools(nextActiveTools));
515
- }
511
+ callReentrant(() => pi.setActiveTools(nextActiveTools));
512
+ for (const toolName of removedFallbackNames) fallbackDeactivatedTools.add(toolName);
516
513
  return unregistered;
517
514
  }
518
515
 
@@ -1508,7 +1505,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1508
1505
  (pi.registerTool as (tool: unknown) => unknown)({
1509
1506
  name: "mcpScript",
1510
1507
  label: "MCP Script",
1511
- description: "Run trusted JavaScript that makes multiple MCP tool calls in one request — loop, filter, chain, or fan out between calls. For a single MCP call, search, describe, status check, or auth action, use the mcp tool instead. Discover with await tools.search({ query }) — resolves to { items: [{ path, name, server, description? }], total, hasMore, nextOffset }, not an { ok, data } envelope. Inspect with await tools.describe({ path }) — resolves to the tool descriptor with inputTypeScript, or { path, error: { code, message, suggestions } }. Then call tools.call(path, args) — resolves to { ok: true, data } or { ok: false, error: { code, message } } — or use direct flat calls when the name is already known; use emit(value) for user-visible output. Load the mcp-scripting skill for the full workflow guide.",
1508
+ description: "Run trusted JavaScript that makes multiple MCP tool calls in one request — loop, filter, chain, or fan out between calls. For a single MCP call, search, describe, status check, or auth action, use the mcp tool instead. Discover with await tools.search({ query }) — resolves to { items: [{ path, name, server, description? }], total, hasMore, nextOffset }, not an { ok, data } envelope. Inspect with await tools.describe({ path }) — resolves to the tool descriptor with inputTypeScript, or { path, error: { code, message, suggestions } }. Then call tools.call(path, args) — resolves to { ok: true, data } or { ok: false, error: { code, message } } — or use direct flat calls when the name is already known; use emit(value) for user-visible output.",
1512
1509
  promptSnippet: "Batch multiple MCP tool calls in one JavaScript request (loop, filter, chain)",
1513
1510
  parameters: Type.Object({
1514
1511
  code: Type.String({ description: "Trusted JavaScript MCP script. Use tools.<prefixedToolName>(args) and emit(value)." }),
@@ -1939,7 +1936,23 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1939
1936
  : proxyModes.executeAuthComplete(proxyState, params.server, input);
1940
1937
  }
1941
1938
  if (params.tool) {
1942
- return proxyModes.executeCall(proxyState, params.tool, parsedArgs, params.server, getPiTools, signal);
1939
+ const result = await proxyModes.executeCall(proxyState, params.tool, parsedArgs, params.server, getPiTools, signal);
1940
+ assertRuntimeGuard(proxyGuard);
1941
+ if (lazyDirectTools.size === 0) return result;
1942
+ // A successful proxy call is as clear a signal as a search hit: the
1943
+ // model wants this tool. Activate it the same additive way, keyed on
1944
+ // the registered (prefixed) name the call reports as canonicalTool.
1945
+ // Failed calls (lookup, approval, tool errors) carry details.error
1946
+ // and activate nothing.
1947
+ const identity = result.details as { error?: unknown; server?: unknown; canonicalTool?: unknown } | undefined;
1948
+ if (!identity || identity.error !== undefined || typeof identity.server !== "string" || typeof identity.canonicalTool !== "string") return result;
1949
+ holdLazyToolsInactive();
1950
+ const added = activateSearchMatches([{ server: identity.server, tool: identity.canonicalTool }]);
1951
+ if (added.length === 0) return result;
1952
+ // Content is returned untouched: the call's output is already sized to
1953
+ // the output limits and is what a compact view previews. The tool
1954
+ // description tells the model a called tool is direct from then on.
1955
+ return { ...result, details: { ...(result.details ?? {}), activated: added }, addedToolNames: added };
1943
1956
  }
1944
1957
  if (params.connect) {
1945
1958
  return connectAndReport(proxyState, params.connect, signal, _ctx as ExtensionContext);
@@ -1982,8 +1995,8 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1982
1995
  cache,
1983
1996
  envRaw === undefined || envRaw === "__none__" ? undefined : envDirectToolOverride,
1984
1997
  );
1985
- // Search-mode tools are registered INACTIVE and `mcp({ search })` is their only
1986
- // activation entry point, so dropping the gateway strands them: every tool held, nothing
1998
+ // Search-mode tools are registered INACTIVE and the `mcp` gateway (search, or a
1999
+ // proxy call) is their only activation entry point, so dropping it strands them: every tool held, nothing
1987
2000
  // able to activate one. Keep it whenever any spec depends on it.
1988
2001
  const hasSearchModeSpecs = directSpecs.some((spec) => spec.lazy === true);
1989
2002
  const shouldRegisterProxyTool =
@@ -1999,10 +2012,14 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
1999
2012
  finalizationRegistrations?.add("mcp");
2000
2013
  registerProxyTool(description);
2001
2014
  finalizationGuard?.();
2002
- return;
2003
2015
  }
2004
2016
  const activeTools = getActiveToolsIfReady();
2005
- if (activeTools && !activeTools.includes("mcp")) {
2017
+ if (activeTools?.includes("mcp")) {
2018
+ // Observed host reactivation ends our fallback ownership. A later
2019
+ // host removal must not be mistaken for our own deactivation.
2020
+ fallbackDeactivatedTools.delete("mcp");
2021
+ } else if (activeTools && fallbackDeactivatedTools.delete("mcp")) {
2022
+ // Only undo a fallback deactivation that the adapter still owns.
2006
2023
  callReentrant(() => pi.setActiveTools([...activeTools, "mcp"]));
2007
2024
  }
2008
2025
  return;
@@ -267,28 +267,27 @@ export async function initializeMcp(
267
267
 
268
268
  const allServerEntries = Object.entries(config.mcpServers);
269
269
  const serverEntries = allServerEntries.filter(([, definition]) => !isServerDisabled(definition));
270
- if (serverEntries.length === 0) {
271
- if (allServerEntries.length > 0 && hasUI) {
272
- ui?.notify(`MCP: All ${allServerEntries.length} server(s) are disabled`, "info");
273
- }
274
- publishMcpStatusSnapshot(state);
275
- return state;
270
+ if (serverEntries.length === 0 && allServerEntries.length > 0 && hasUI) {
271
+ ui?.notify(`MCP: All ${allServerEntries.length} server(s) are disabled`, "info");
276
272
  }
277
273
 
278
274
  const idleSetting = typeof config.settings?.idleTimeout === "number" ? config.settings.idleTimeout : 10;
279
275
  lifecycle.setGlobalIdleTimeout(idleSetting);
280
276
 
281
- const cachePath = getMetadataCachePath();
282
- const cacheFileExists = existsSync(cachePath);
283
- let cache = loadMetadataCache();
277
+ let cache: ReturnType<typeof loadMetadataCache> = null;
284
278
  let bootstrapAll = false;
285
279
 
286
- if (!cacheFileExists) {
287
- bootstrapAll = true;
288
- saveMetadataCache({ version: 1, servers: {} });
289
- } else if (!cache) {
290
- cache = { version: 1, servers: {} };
291
- saveMetadataCache(cache);
280
+ if (serverEntries.length > 0) {
281
+ const cachePath = getMetadataCachePath();
282
+ const cacheFileExists = existsSync(cachePath);
283
+ cache = loadMetadataCache();
284
+ if (!cacheFileExists) {
285
+ bootstrapAll = true;
286
+ saveMetadataCache({ version: 1, servers: {} });
287
+ } else if (!cache) {
288
+ cache = { version: 1, servers: {} };
289
+ saveMetadataCache(cache);
290
+ }
292
291
  }
293
292
 
294
293
  const prefix = config.settings?.toolPrefix ?? "server";
@@ -11,6 +11,20 @@ import type {
11
11
  // ajv-formats types target its bundled ajv; the runtime accepts both instances.
12
12
  const addFormats = addFormatsImport as unknown as (instance: Ajv) => void;
13
13
 
14
+ // Rust servers built with schemars annotate numbers with these formats. The
15
+ // `type`, `minimum`, and `maximum` keywords already carry the constraint, so
16
+ // registering them as always-valid only stops Ajv's "unknown format" warnings.
17
+ // int32, int64, float, and double are omitted: ajv-formats validates them.
18
+ const SCHEMARS_NUMERIC_FORMATS = [
19
+ "int", "int8", "int16", "int128",
20
+ "uint", "uint8", "uint16", "uint32", "uint64", "uint128",
21
+ ];
22
+
23
+ function addKnownFormats(ajv: Ajv): void {
24
+ addFormats(ajv);
25
+ for (const format of SCHEMARS_NUMERIC_FORMATS) ajv.addFormat(format, true);
26
+ }
27
+
14
28
  type SchemaDialect =
15
29
  | { status: "unstamped" }
16
30
  | { status: "stamped"; uri: string };
@@ -44,7 +58,7 @@ export function createJsonSchemaValidator(): JsonSchemaValidatorProvider {
44
58
  draft2020Validator ??= (() => {
45
59
  const Ajv2020 = Ajv2020Import as unknown as typeof Ajv;
46
60
  const ajv = new Ajv2020({ strict: false, allErrors: true });
47
- addFormats(ajv);
61
+ addKnownFormats(ajv);
48
62
  return new AjvJsonSchemaValidator(ajv);
49
63
  })();
50
64
  return draft2020Validator.getValidator<T>(schema);
@@ -60,7 +74,7 @@ export function createJsonSchemaValidator(): JsonSchemaValidatorProvider {
60
74
  validateSchema: false,
61
75
  allErrors: true,
62
76
  });
63
- addFormats(ajv);
77
+ addKnownFormats(ajv);
64
78
  return new AjvJsonSchemaValidator(ajv);
65
79
  })();
66
80
  return draft07Validator.getValidator<T>(schema);
@@ -210,6 +210,7 @@ interface AuthEntryChunkManifest {
210
210
  }
211
211
 
212
212
  let KeyringEntryClass: KeyringEntryConstructor | undefined;
213
+ const keyringEntries = new Map<string, KeyringEntry>();
213
214
  const memoryAuthEntries = new Map<string, string>();
214
215
 
215
216
  let testAuthSecretStoreReadCount = 0;
@@ -238,12 +239,29 @@ const memoryAuthSecretStore: AuthSecretStore = {
238
239
 
239
240
  const keyringAuthSecretStore: AuthSecretStore = {
240
241
  read(account) {
241
- return getKeyringEntry(account).getPassword() ?? undefined;
242
+ const cached = keyringEntries.get(account);
243
+ if (cached) {
244
+ try {
245
+ // keyring v2 throws for provider/session failures; null means the credential is absent.
246
+ return cached.getPassword() ?? undefined;
247
+ } catch {
248
+ // A stale native Entry may survive a keyring daemon restart. Retry once.
249
+ }
250
+ keyringEntries.delete(account);
251
+ }
252
+ const fresh = getKeyringEntry(account);
253
+ const value = fresh.getPassword();
254
+ keyringEntries.set(account, fresh);
255
+ return value ?? undefined;
242
256
  },
243
257
  write(account, payload) {
244
- getKeyringEntry(account).setPassword(payload);
258
+ keyringEntries.delete(account);
259
+ const fresh = getKeyringEntry(account);
260
+ fresh.setPassword(payload);
261
+ keyringEntries.set(account, fresh);
245
262
  },
246
263
  remove(account) {
264
+ keyringEntries.delete(account);
247
265
  getKeyringEntry(account).deleteCredential();
248
266
  },
249
267
  };
@@ -305,9 +323,16 @@ const keyRevokedAuthSecretStore: AuthSecretStore = {
305
323
  export function resetTestAuthSecretStore(): void {
306
324
  memoryAuthEntries.clear();
307
325
  authEntryCache.clear();
326
+ keyringEntries.clear();
308
327
  testAuthSecretStoreReadCount = 0;
309
328
  }
310
329
 
330
+ /** Install a fake native Entry for OAuth storage tests without opening a real keyring. */
331
+ export function setTestKeyringEntryClass(entryClass: KeyringEntryConstructor | undefined): void {
332
+ keyringEntries.clear();
333
+ KeyringEntryClass = entryClass;
334
+ }
335
+
311
336
  export function resetAuthEntryCache(): void {
312
337
  authEntryCache.clear();
313
338
  }
@@ -86,7 +86,7 @@ export function computeServerHash(definition: ServerEntry, environment: NodeJS.P
86
86
  // Exclude lifecycle, idleTimeout, requestTimeoutMs, debug — those are runtime behavior settings
87
87
  // that don't change which tools a server exposes.
88
88
  const identity: Record<string, unknown> = {
89
- command: definition.command,
89
+ command: resolveConfigPath(definition.command, environment),
90
90
  args: definition.args,
91
91
  socket: resolveConfigPath(definition.socket, environment),
92
92
  env: isBuiltInAgentPlugin(definition, "env") ? definition.env : interpolateEnvRecord(definition.env, environment),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-adapter",
3
- "version": "2.37.0",
3
+ "version": "2.38.0",
4
4
  "description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
5
5
  "type": "module",
6
6
  "types": "./index.ts",
@@ -163,6 +163,7 @@
163
163
  "banner.png",
164
164
  "skills",
165
165
  "README.md",
166
+ "OAUTH.md",
166
167
  "CHANGELOG.md",
167
168
  "LICENSE",
168
169
  "dist"
@@ -172,7 +173,7 @@
172
173
  "@modelcontextprotocol/core": "2.0.0",
173
174
  "@modelcontextprotocol/ext-apps": "^1.2.2",
174
175
  "@modelcontextprotocol/ext-tasks": "^0.1.0",
175
- "@napi-rs/keyring": "^1.3.0",
176
+ "@napi-rs/keyring": "^2.1.0",
176
177
  "@typesafe-ai/sdk": "0.6.0",
177
178
  "ajv": "^8.17.1",
178
179
  "ajv-formats": "^3.0.1",
@@ -1374,7 +1374,7 @@ export async function executeCall(
1374
1374
  }
1375
1375
 
1376
1376
  const callIdentity = toolMeta.resourceUri
1377
- ? { server: serverName, resourceUri: toolMeta.resourceUri }
1377
+ ? { server: serverName, resourceUri: toolMeta.resourceUri, canonicalTool: toolMeta.name }
1378
1378
  : { server: serverName, tool: toolMeta.originalName, canonicalTool: toolMeta.name };
1379
1379
 
1380
1380
  let connection = state.manager.getConnection(serverName);
@@ -63,6 +63,7 @@ import { attachTaskSession, type RawRequestChannel } from "./mcp-tasks.ts";
63
63
  import type { TaskEnabledSession } from "@modelcontextprotocol/ext-tasks/client";
64
64
  import {
65
65
  interpolateEnvVars,
66
+ expandHomePath,
66
67
  resolveBearerToken,
67
68
  resolveCommandSecret,
68
69
  resolveCommandSecretsRecord,
@@ -1044,11 +1045,13 @@ export class McpServerManager {
1044
1045
 
1045
1046
  if (definition.command) {
1046
1047
  client = this.createClient(name, definition);
1047
- let command = definition.command;
1048
+ let command = resolveConfigPath(definition.command) ?? definition.command;
1048
1049
  const literalArgs = isBuiltInAgentPlugin(definition, "args");
1049
1050
  const literalCwd = isBuiltInAgentPlugin(definition, "cwd");
1050
- let args = literalArgs ? [...(definition.args ?? [])] : (definition.args ?? []).map((argument) => interpolateEnvVars(argument));
1051
- const cwd = (literalCwd ? definition.cwd : resolveConfigPath(definition.cwd)) ?? this.defaultCwd;
1051
+ let args = literalArgs
1052
+ ? [...(definition.args ?? [])]
1053
+ : (definition.args ?? []).map((argument) => expandHomePath(interpolateEnvVars(argument)) ?? argument);
1054
+ const cwd = (literalCwd ? expandHomePath(definition.cwd) : resolveConfigPath(definition.cwd)) ?? this.defaultCwd;
1052
1055
  if (definition.pluginDataDir) mkdirSync(definition.pluginDataDir, { recursive: true });
1053
1056
  if (cwd !== undefined) {
1054
1057
  const cwdStats = statSync(cwd, { throwIfNoEntry: false });
@@ -75,6 +75,8 @@ class CompactMcpToolResult implements Component {
75
75
  private readonly inputPreview: string,
76
76
  private readonly display: McpToolResultDisplay,
77
77
  private readonly theme: RenderTheme,
78
+ /** Shown after the title and never truncated; the title shrinks instead (e.g. `✗3`). */
79
+ private readonly status = "",
78
80
  ) {}
79
81
 
80
82
  render(width: number): string[] {
@@ -113,17 +115,23 @@ class CompactMcpToolResult implements Component {
113
115
  private renderPrefix(width: number): string {
114
116
  if (!this.title) return "";
115
117
  const arrow = " → ";
116
- if (!this.inputPreview) return `${this.theme.fg("toolTitle", this.title)}${arrow}`;
118
+ const statusWidth = this.status ? visibleWidth(this.status) + 1 : 0;
119
+ const head = (maxWidth?: number) => {
120
+ const title = maxWidth === undefined
121
+ ? this.title
122
+ : truncateToWidth(this.title, Math.max(1, maxWidth - statusWidth), "…");
123
+ const status = this.status ? ` ${this.theme.fg("error", this.status)}` : "";
124
+ return `${this.theme.fg("toolTitle", title)}${status}`;
125
+ };
126
+ if (!this.inputPreview) return `${head()}${arrow}`;
117
127
 
118
128
  const maxPrefixWidth = Math.max(12, Math.floor(width * 0.55));
119
- const titleWidth = visibleWidth(this.title);
129
+ const titleWidth = visibleWidth(this.title) + statusWidth;
120
130
  const inputWidth = Math.max(0, maxPrefixWidth - titleWidth - 1);
121
- if (inputWidth <= 3) {
122
- return `${this.theme.fg("toolTitle", truncateToWidth(this.title, maxPrefixWidth, "…"))}${arrow}`;
123
- }
131
+ if (inputWidth <= 3) return `${head(maxPrefixWidth)}${arrow}`;
124
132
 
125
133
  const input = truncateToWidth(this.inputPreview, inputWidth, "…");
126
- return `${this.theme.fg("toolTitle", this.title)} ${this.theme.fg("muted", input)}${arrow}`;
134
+ return `${head()} ${this.theme.fg("muted", input)}${arrow}`;
127
135
  }
128
136
  }
129
137
 
@@ -326,11 +334,91 @@ export function createMcpDirectToolCallRenderer(displayName: string, options = r
326
334
 
327
335
  export function createMcpScriptToolCallRenderer(options = resolveMcpToolRenderOptions()) {
328
336
  return (args: { code: string }, theme?: RenderTheme, context?: McpToolRenderContext) => {
337
+ // Compact mode hides this call row once the result lands, so leave a title for the result row.
338
+ // The tool list comes from the result's `details.calls` trace (see formatMcpScriptCallSummary);
339
+ // the script code itself is never copied into the compact row.
340
+ if (context?.state) context.state.compactTitle = MCP_SCRIPT_TITLE;
329
341
  if (shouldUseCompactFinalRender(options, context)) return new EmptyComponent();
330
342
  return renderToolCallLines(formatMcpScriptToolCallLines(args), theme);
331
343
  };
332
344
  }
333
345
 
346
+ const MCP_SCRIPT_TITLE = "mcpScript";
347
+ const MCP_SCRIPT_SUMMARY_MAX_TOOLS = 4;
348
+
349
+ export interface McpScriptCallSummary {
350
+ /** Traced operations that failed; the compact row shows it as `mcpScript ✗N`. */
351
+ failed: number;
352
+ /** What the script touched; "" when it made no MCP operations. */
353
+ preview: string;
354
+ }
355
+
356
+ const PLAIN_TOOL_PATH = /^[A-Za-z0-9_.\/:@-]+$/;
357
+
358
+ /**
359
+ * Traced paths are script-supplied. Only identifier-like paths (what MCP tool names look like)
360
+ * are shown bare. Anything else is shown as a quoted, ASCII-escaped JSON string, so it cannot
361
+ * break the row, cannot pass for summary syntax (`foo×2`, `a, b`, ` · `, `+1 more`), and stays
362
+ * distinct from the clean path it may resemble (`demo_red` vs `"\u001b[31mdemo_red"`).
363
+ */
364
+ function formatTracedPath(path: string): string {
365
+ if (PLAIN_TOOL_PATH.test(path)) return path;
366
+ // JSON.stringify quotes and escapes C0 controls, quotes and backslashes; escaping all other
367
+ // non-ASCII too rules out DEL, C1 controls, bidi overrides and invisible characters.
368
+ return JSON.stringify(path).replace(/[^\x20-\x7e]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
369
+ }
370
+
371
+ /**
372
+ * Failure count and compact-row preview for an mcpScript run, from the `details.calls` trace.
373
+ * The preview lists distinct tool paths in first-call order with repeat counts, then other
374
+ * operation counts, e.g. `github_search_issues×6, slack_post_message · search`.
375
+ * Scripts that only describe tools list the described paths instead (`describe a, b`).
376
+ * The failure count is rendered as a title status, not in the preview: narrow rows drop the
377
+ * preview first, and the status is never truncated (the title shrinks instead).
378
+ * Returns null for non-script results.
379
+ */
380
+ export function formatMcpScriptCallSummary(
381
+ details: McpToolResultDetails | undefined,
382
+ maxTools = MCP_SCRIPT_SUMMARY_MAX_TOOLS,
383
+ ): McpScriptCallSummary | null {
384
+ if (details?.mode !== "script") return null;
385
+ const operations = Array.isArray(details.calls) ? details.calls : [];
386
+ const callCounts = new Map<string, number>();
387
+ const describeCounts = new Map<string, number>();
388
+ const metaCounts = new Map<string, number>();
389
+ let failed = 0;
390
+ for (const operation of operations) {
391
+ if (typeof operation !== "object" || operation === null) continue;
392
+ const { operation: kind, path, ok } = operation as { operation?: unknown; path?: unknown; ok?: unknown };
393
+ if (typeof kind !== "string") continue;
394
+ if (ok === false) failed += 1;
395
+ const target = kind === "call" ? callCounts : kind === "describe" ? describeCounts : null;
396
+ if (target && typeof path === "string") {
397
+ target.set(path, (target.get(path) ?? 0) + 1);
398
+ } else {
399
+ metaCounts.set(kind, (metaCounts.get(kind) ?? 0) + 1);
400
+ }
401
+ }
402
+
403
+ const withCount = (name: string, count: number) => (count > 1 ? `${name}×${count}` : name);
404
+ const listPaths = (counts: Map<string, number>) => {
405
+ const names = [...counts].map(([path, count]) => withCount(formatTracedPath(path), count));
406
+ return names.length > maxTools
407
+ ? [...names.slice(0, maxTools), `+${names.length - maxTools} more`].join(", ")
408
+ : names.join(", ");
409
+ };
410
+
411
+ let tools = listPaths(callCounts);
412
+ if (!tools && describeCounts.size > 0) {
413
+ tools = `describe ${listPaths(describeCounts)}`;
414
+ } else {
415
+ const describes = [...describeCounts.values()].reduce((sum, count) => sum + count, 0);
416
+ if (describes > 0) metaCounts.set("describe", describes);
417
+ }
418
+ const meta = [...metaCounts].map(([kind, count]) => withCount(kind, count)).join(", ");
419
+ return { failed, preview: [tools, meta].filter(Boolean).join(" · ") };
420
+ }
421
+
334
422
  function blockToLines(block: McpToolContentBlock): string[] {
335
423
  if (block.type === "text") {
336
424
  return block.text.split("\n");
@@ -448,6 +536,11 @@ export function renderMcpToolResult(
448
536
  const expanded = options.expanded || context?.isError === true || hasErrorDetails;
449
537
  if (!expanded && renderOptions.resultRendering === "compact") {
450
538
  const display = formatMcpToolResultLines(result, false, renderOptions.collapsedResultLines);
539
+ const scriptSummary = formatMcpScriptCallSummary(result.details);
540
+ if (scriptSummary) {
541
+ const status = scriptSummary.failed > 0 ? `✗${scriptSummary.failed}` : "";
542
+ return new CompactMcpToolResult(MCP_SCRIPT_TITLE, scriptSummary.preview, display, activeTheme, status);
543
+ }
451
544
  const title = context?.state?.compactTitle ?? formatMcpToolResultIdentity(result.details) ?? "";
452
545
  const inputPreview = context?.state?.compactInputPreview ?? "";
453
546
  return new CompactMcpToolResult(title, inputPreview, display, activeTheme);
@@ -192,7 +192,7 @@ export interface UiServerHandle {
192
192
  sessionToken: string;
193
193
  serverName: string;
194
194
  toolName: string;
195
- viewer?: "browser" | "glimpse" | "suppressed";
195
+ viewer?: "browser" | "glimpse" | "orca" | "suppressed";
196
196
  windowOpen?: boolean;
197
197
  close: (reason?: string) => void;
198
198
  sendToolInput: (args: Record<string, unknown>) => void;
@@ -740,7 +740,7 @@ export interface PromptMetadata {
740
740
  }
741
741
 
742
742
  export interface DirectToolSpec {
743
- /** Registered inactive; `mcp({ search })` activates it (directTools: "search"). */
743
+ /** Registered inactive; `mcp({ search })` or a successful `mcp({ tool })` call activates it (directTools: "search"). */
744
744
  lazy?: boolean;
745
745
  serverName: string;
746
746
  originalName: string;
@@ -35,7 +35,7 @@ export interface UiSessionRequest {
35
35
  onNeedsAuth?: SessionRecoveryDeps["onNeedsAuth"];
36
36
  }
37
37
 
38
- export type UiSessionViewer = "browser" | "glimpse" | "suppressed";
38
+ export type UiSessionViewer = "browser" | "glimpse" | "orca" | "suppressed";
39
39
 
40
40
  export interface UiSessionRuntime {
41
41
  serverName: string;
@@ -143,6 +143,19 @@ async function openInBrowser(state: McpExtensionState, url: string, signal: Abor
143
143
  }
144
144
  }
145
145
 
146
+ async function openInOrcaBrowser(url: string, signal: AbortSignal): Promise<void> {
147
+ throwIfAborted(signal);
148
+ await new Promise<void>((resolve, reject) => {
149
+ execFile("orca", ["goto", "--url", url], { timeout: 10000, signal }, (error) => {
150
+ if (error) {
151
+ reject(error instanceof Error ? error : new Error(String(error)));
152
+ } else {
153
+ resolve();
154
+ }
155
+ });
156
+ });
157
+ }
158
+
146
159
  function isRemoteSession(): boolean {
147
160
  return Boolean(process.env.SSH_CONNECTION || process.env.SSH_TTY);
148
161
  }
@@ -486,7 +499,6 @@ export async function maybeStartUiSession(
486
499
  `If this session is remote, run ssh -L ${handle.port}:127.0.0.1:${handle.port} -L ${handle.proxyPort}:127.0.0.1:${handle.proxyPort} <this-host> first.`,
487
500
  "info",
488
501
  );
489
- log.info("Suppressing MCP UI window (MCP_UI_VIEWER=" + viewerPref + ")", { url: handle.url });
490
502
  } else {
491
503
  const remoteLikely = remoteByEnv || await hasActiveRemoteLogin();
492
504
  const emitRemoteHint = async (openError: string | null, openedOnHost = false) => {
@@ -499,33 +511,17 @@ export async function maybeStartUiSession(
499
511
  openedOnHost,
500
512
  }), openError === null ? "info" : "warning");
501
513
  };
502
- const glimpseDetected = !remoteByEnv && isGlimpseAvailable();
503
- const useGlimpse = !remoteByEnv && (viewerPref === "glimpse" ||
504
- (viewerPref !== "browser" && glimpseDetected));
505
-
506
- if (useGlimpse) {
514
+ // Handle Orca browser viewer
515
+ if (viewerPref === "orca") {
507
516
  try {
508
- const glimpseHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{margin:0;padding:0;width:100vw;height:100vh;overflow:hidden}iframe{width:100%;height:100%;border:none}</style></head><body><iframe src="${handle.url}"></iframe></body></html>`;
509
- const glimpseWindow = await openGlimpseWindow(glimpseHtml, {
510
- title: `MCP · ${request.serverName} · ${request.toolName}`,
511
- width: 1000,
512
- height: 800,
513
- onClosed: () => {
514
- if (active) handle.close("glimpse-closed");
515
- },
516
- });
517
- if (state.owner?.isActive() === false || runtimeSignal.aborted) {
518
- glimpseWindow.close();
519
- throwIfAborted(runtimeSignal);
520
- throw new Error("MCP Glimpse window became stale before registration");
521
- }
522
- activeGlimpseWindow = glimpseWindow;
523
- viewer = "glimpse";
517
+ await openInOrcaBrowser(handle.url, runtimeSignal);
524
518
  if (remoteLikely) {
525
519
  await emitRemoteHint(null, true);
526
520
  }
521
+ viewer = "orca";
527
522
  } catch (error) {
528
- log.debug("Glimpse unavailable, using browser", {
523
+ if (isAbortError(error, runtimeSignal)) throw error;
524
+ log.debug("Orca browser failed, using fallback", {
529
525
  error: error instanceof Error ? error.message : String(error),
530
526
  });
531
527
  const openError = await openInBrowser(state, handle.url, runtimeSignal);
@@ -535,9 +531,46 @@ export async function maybeStartUiSession(
535
531
  viewer = "browser";
536
532
  }
537
533
  } else {
538
- const openError = await openInBrowser(state, handle.url, runtimeSignal);
539
- if (openError !== null || remoteLikely) {
540
- await emitRemoteHint(openError);
534
+ const glimpseDetected = !remoteByEnv && isGlimpseAvailable();
535
+ const useGlimpse = !remoteByEnv && (viewerPref === "glimpse" ||
536
+ (viewerPref !== "browser" && glimpseDetected));
537
+
538
+ if (useGlimpse) {
539
+ try {
540
+ const glimpseHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{margin:0;padding:0;width:100vw;height:100vh;overflow:hidden}iframe{width:100%;height:100%;border:none}</style></head><body><iframe src="${handle.url}"></iframe></body></html>`;
541
+ const glimpseWindow = await openGlimpseWindow(glimpseHtml, {
542
+ title: `MCP · ${request.serverName} · ${request.toolName}`,
543
+ width: 1000,
544
+ height: 800,
545
+ onClosed: () => {
546
+ if (active) handle.close("glimpse-closed");
547
+ },
548
+ });
549
+ if (state.owner?.isActive() === false || runtimeSignal.aborted) {
550
+ glimpseWindow.close();
551
+ throwIfAborted(runtimeSignal);
552
+ throw new Error("MCP Glimpse window became stale before registration");
553
+ }
554
+ activeGlimpseWindow = glimpseWindow;
555
+ viewer = "glimpse";
556
+ if (remoteLikely) {
557
+ await emitRemoteHint(null, true);
558
+ }
559
+ } catch (error) {
560
+ log.debug("Glimpse unavailable, using browser", {
561
+ error: error instanceof Error ? error.message : String(error),
562
+ });
563
+ const openError = await openInBrowser(state, handle.url, runtimeSignal);
564
+ if (openError !== null || remoteLikely) {
565
+ await emitRemoteHint(openError);
566
+ }
567
+ viewer = "browser";
568
+ }
569
+ } else {
570
+ const openError = await openInBrowser(state, handle.url, runtimeSignal);
571
+ if (openError !== null || remoteLikely) {
572
+ await emitRemoteHint(openError);
573
+ }
541
574
  }
542
575
  }
543
576
  }
@@ -247,10 +247,18 @@ export function resolveServerUrl(definition: Pick<ServerEntry, "url">, environme
247
247
  export function resolveConfigPath(value: string | undefined, environment: NodeJS.ProcessEnv = process.env): string | undefined {
248
248
  if (value === undefined) return undefined;
249
249
 
250
- const resolved = interpolateEnvVars(value, environment);
250
+ return expandHomePath(interpolateEnvVars(value, environment));
251
+ }
252
+
253
+ /** Expand a leading home-directory marker without interpolating environment variables. */
254
+ export function expandHomePath(value: string | undefined): string | undefined {
255
+ if (value === undefined) return undefined;
256
+
257
+ const resolved = value;
251
258
  if (resolved === "~") return homedir();
252
- if (resolved.startsWith("~/") || resolved.startsWith("~\\")) {
253
- return join(homedir(), resolved.slice(2));
259
+ if (resolved.startsWith("~/") || (platform() === "win32" && resolved.startsWith("~\\"))) {
260
+ const suffix = platform() === "win32" ? resolved.slice(2).replace(/[\\/]/g, sep) : resolved.slice(2);
261
+ return join(homedir(), suffix);
254
262
  }
255
263
  return resolved;
256
264
  }
@@ -4,6 +4,12 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+ ## [1.23.2] — 2026-09-24
8
+
9
+ ### Fixed
10
+
11
+ - Sessions hosted by pi-web's session daemon no longer deactivate after the first one. The in-process root-activation lease (designed for terminal Pi's `/reload` and in-process children) misclassified every later pi-web session as a subagent child, leaving it in `subagent-child-passive` mode with failover, preflight and auto-continue silently disabled. When `PI_WEB_SESSION=1` is present, each session now activates as an independent root; any other multi-session SDK host can opt in with the host-neutral `PI_MULTI_ACCOUNT_INDEPENDENT_ROOTS=1`. Genuine pi-subagents children still run in a runner process marked `PI_SUBAGENT_CHILD=1` and remain passive.
12
+ - In-process independent roots and same-session rehydrates now share a process-scoped canonical child proxy and publication lifetime. A sibling can exit in either order without restoring real OAuth into child-facing `auth.json` or orphaning `models.json` loopback routes; only the final root closes the listener and restores auth. A root whose proxy is disabled still retires public numbered aliases before another root publishes placeholders, and keeps that listener alive through handoff. The surviving root handles newly discovered slots, while explicit `PI_SUBAGENT_CHILD` remains passive.
7
13
 
8
14
  ## [1.23.1] — 2026-09-23
9
15