pi-ui-extension 1.1.9 → 1.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,7 +48,18 @@ The bottom of the `SESSIONS` panel exposes four resource categories:
48
48
  - `Extensions`
49
49
  - `MCP`
50
50
 
51
- These shortcuts let you inspect the resources currently loaded by the project/Pi environment, making it easier to verify which agent instructions, skills, extensions, and MCP servers are actually available.
51
+ These shortcuts let you inspect the resources discovered by the project/Pi environment. The MCP view distinguishes configured and registered servers from confirmed connections.
52
+
53
+ #### MCP Status Center
54
+
55
+ Click **MCP** in the sidebar to open a read-only status picker covering global and trusted-project MCP configuration, servers registered by extensions via `pi.registerMcpServer()`, and observations from `pi-mcp-adapter/status/v1`.
56
+
57
+ - See configuration scope, project overrides, and enabled/disabled state (including native Pi `enabled: false`).
58
+ - Connection health is only shown when an adapter explicitly reports it. **Configured, registered, and loaded do not mean connected.**
59
+ - Commands, endpoints, headers, tokens, environment values, and raw errors are never displayed.
60
+ - Tool usage now attributes native Pi `mcp__server__tool` calls to their MCP server, alongside existing gateway and `mcpScript` counts.
61
+
62
+ Pi does not currently expose its complete native MCP connection state to ordinary UI extensions. For live connection diagnostics, reconnects, authentication, or configuration changes, use the active `/mcp` manager (built-in or provided by your MCP adapter).
52
63
 
53
64
  ### 4. CTX context inspection
54
65
 
@@ -89,7 +100,22 @@ For tool definitions, the extension prefers the observable provider-request tool
89
100
 
90
101
  Modes can be changed directly with mouse clicks.
91
102
 
92
- When Fabric takes ownership of tool rendering, the panel remains visible for usage statistics but the local mode selector is hidden and replaced with `View controlled by Fabric`. This avoids conflicting display control.
103
+ Tool View uses Pi's official `pi.registerToolRenderer()` hook for the built-in tools (`read`, `bash`, `edit`, `write`, `grep`, `find`, and `ls`) **and the built-in `codemode` tool**. It never replaces tool definitions or execution functions. Other tools retain their own renderers.
104
+
105
+ For **Codemode** specifically:
106
+
107
+ - **Normal** retains Pi's native script, nested-call, and output rendering (including its own expand/collapse behavior).
108
+ - **Compact** uses a two-line, status-colored summary with Chinese labels: a highlighted Codemode title and script line count, followed by grouped execution status, nested-call counts, output size, and duration. It omits script source, nested-call arguments, and actual output, even if the row is expanded or execution fails. On narrow terminals it keeps each part to one row and prioritizes status and failures over less important metrics.
109
+ - **Hidden** removes the Codemode transcript row, including expanded and failed results, without stopping execution or removing Tool/MCP usage accounting.
110
+
111
+ Example Compact output:
112
+
113
+ ```text
114
+ ▸ codemode 8 行代码
115
+ ✓ 已完成 │ 1 次调用 · 1 成功 │ 59 行输出 · 85ms
116
+ ```
117
+
118
+ Changing the Tool View mode also updates previously rendered tool rows in the current session. Other third-party tools are unaffected.
93
119
 
94
120
  ### 6. Tool / MCP usage statistics
95
121
 
@@ -154,7 +180,7 @@ Example configuration:
154
180
  To pin a specific version:
155
181
 
156
182
  ```bash
157
- pi install npm:pi-ui-extension@1.1.1
183
+ pi install npm:pi-ui-extension@1.1.11
158
184
  ```
159
185
 
160
186
  or:
@@ -162,7 +188,7 @@ or:
162
188
  ```json
163
189
  {
164
190
  "packages": [
165
- "npm:pi-ui-extension@1.1.1"
191
+ "npm:pi-ui-extension@1.1.11"
166
192
  ]
167
193
  }
168
194
  ```
@@ -190,15 +216,15 @@ Configuration form:
190
216
  Pin to a Git tag:
191
217
 
192
218
  ```bash
193
- pi install git:github.com/1993yihuan/pi-ui-extension@v1.1.1
219
+ pi install git:github.com/1993yihuan/pi-ui-extension@v1.1.11
194
220
  ```
195
221
 
196
- ### Temporary usage
222
+ ### Temporary local-source testing
197
223
 
198
- To load the package for a single Pi launch without modifying `settings.json`:
224
+ To try a local checkout without changing `settings.json`, run from the repository root. Disable automatic extension discovery to avoid loading a previously installed copy of `pi-ui-extension` twice:
199
225
 
200
226
  ```bash
201
- pi -e npm:pi-ui-extension
227
+ pi --no-extensions -e builtin:mcp -e ./extensions/index.ts --tui-mode fullscreen
202
228
  ```
203
229
 
204
230
  ### Project-local installation
@@ -289,7 +315,7 @@ npm run typecheck
289
315
  npm test
290
316
  ```
291
317
 
292
- The test suite covers the main behavior of Sidebar, CTX, Session management, Tool View, MCP resources, Fabric detection, mouse scrolling, and resize handling.
318
+ The test suite covers Sidebar, CTX, Session management, official Tool View renderers, MCP resources, mouse scrolling, and resize handling.
293
319
 
294
320
  During development, Pi can load the local source directory directly:
295
321
 
@@ -314,7 +340,6 @@ pi-ui-extension/
314
340
  │ ├── session/ # Session list and actions
315
341
  │ ├── sidebar/ # Sidebar layout, scrolling, resize
316
342
  │ ├── tool-view/ # Tool View modes and usage statistics
317
- │ ├── fabric-detection.ts # Fabric ownership detection
318
343
  │ └── index.ts # Pi Extension entry point
319
344
  ├── .github/workflows/
320
345
  │ ├── ci.yml # Automatic checks on push / PR
@@ -337,7 +362,7 @@ Pi's extension entry point is declared explicitly in `package.json`:
337
362
 
338
363
  ## CI / Release
339
364
 
340
- Every push to `main` and every pull request runs:
365
+ Every push to `main` and every pull request runs on **both Node.js 22.19.0 and 24**:
341
366
 
342
367
  ```text
343
368
  npm ci
@@ -357,6 +382,7 @@ A `v*` tag triggers the release workflow:
357
382
 
358
383
  ```text
359
384
  Checkout
385
+ → validate release tag format and main ancestry
360
386
  → Node.js 24
361
387
  → npm ci
362
388
  → npm run check
@@ -364,6 +390,7 @@ Checkout
364
390
  → npm pack --dry-run
365
391
  → npm Trusted Publishing (OIDC)
366
392
  → npm publish
393
+ → verify npm Registry visibility (with retries)
367
394
  → GitHub Release
368
395
  ```
369
396
 
@@ -373,16 +400,16 @@ npm publishing uses GitHub Actions OIDC Trusted Publishing, so no long-lived `NP
373
400
 
374
401
  - Does not modify Pi Core.
375
402
  - `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui` are host-provided peer dependencies to avoid duplicate runtime instances.
376
- - Fabric ownership is detected explicitly to avoid conflicting Tool View controls.
403
+ - Tool View changes only how built-in tool calls are rendered, using Pi's public renderer resolver rather than overriding their implementations.
377
404
  - Session and resource operations use Pi public APIs wherever possible.
378
405
  - Context composition is clearly presented as an estimate, not provider-exact token accounting.
379
406
  - Fullscreen sidebar behavior degrades safely when fullscreen layout is unavailable.
380
407
 
381
408
  ## Requirements
382
409
 
383
- - Pi Coding Agent
410
+ - Pi Coding Agent with the official `registerToolRenderer()` API (tested with `@earendil-works/pi-coding-agent@1.1.0`)
384
411
  - Node.js `>= 22.19.0` (use a Node version supported by your Pi installation)
385
- - Development and CI currently use Node.js 24
412
+ - CI validates Node.js 22.19.0 and 24; releases use Node.js 24
386
413
 
387
414
  ## License
388
415
 
package/README.zh-CN.md CHANGED
@@ -48,7 +48,18 @@ Sidebar 支持鼠标交互、滚轮滚动和终端尺寸变化。终端高度不
48
48
  - `Extensions`
49
49
  - `MCP`
50
50
 
51
- 点击后可以直接查看当前项目 / 当前 Pi 环境中已加载的相关资源,便于快速确认 Agent 当前实际使用了哪些上下文、Skill、Extension 和 MCP Server。
51
+ 点击后可以查看当前项目 / Pi 环境发现的相关资源。其中 MCP 状态中心会区分“已配置 / 已注册”和“已确认连接”,避免把配置存在误认为连接成功。
52
+
53
+ #### MCP 状态中心
54
+
55
+ 点击 Sidebar 中的 **MCP** 按钮可打开只读状态中心,查看全局和可信项目配置的 MCP Server、扩展通过 `pi.registerMcpServer()` 注册的服务器,以及 `pi-mcp-adapter/status/v1` 提供的运行时观察结果。
56
+
57
+ - 显示服务器来源、启用/禁用状态与配置覆盖情况;项目级 `enabled: false` 会正确覆盖全局配置。
58
+ - 仅当 Adapter 明确报告连接状态时显示 Connected、Connecting、Failed 等状态;**已配置、已注册或已加载不代表已连接**。
59
+ - 不展示命令、URL、环境变量、请求头、Token 或原始错误内容。
60
+ - 工具调用统计支持 Pi 原生 `mcp__server__tool` 格式,继续兼容既有 MCP Gateway / `mcpScript` 统计。
61
+
62
+ Pi 目前没有向普通 UI 扩展公开完整的原生 MCP 实时连接状态。查看连接错误、重新连接、登录或启停服务器,请使用当前生效的 `/mcp` 管理界面(Pi 内置或由第三方 MCP Adapter 提供);本状态中心不执行连接或配置修改。
52
63
 
53
64
  ### 4. CTX 上下文观察
54
65
 
@@ -89,7 +100,22 @@ Composition 会将当前可观测上下文拆分为:
89
100
 
90
101
  可以直接使用鼠标点击模式进行切换。
91
102
 
92
- 当 Fabric 接管工具展示行为时,扩展会保留 Tool View 面板和调用统计,但不再显示本地模式选择器,并提示 `View controlled by Fabric`,避免与 Fabric 的展示逻辑互相抢占。
103
+ Tool View 使用 Pi 官方的 `pi.registerToolRenderer()` 接口管理内置工具(`read`、`bash`、`edit`、`write`、`grep`、`find` 和 `ls`)**以及内置 `codemode` 工具**的展示,不再重注册工具定义或接管工具执行逻辑。其他 Tool 保留其自身的渲染方式。
104
+
105
+ 对于 **Codemode**:
106
+
107
+ - **Normal**:保留 Pi 原生的脚本、子工具调用、输出详情,以及原生展开 / 收起行为。
108
+ - **Compact**:采用带有状态颜色的双行摘要。第一行突出 Codemode 和代码行数;第二行将状态、子调用统计、输出规模与耗时分组展示,并统一使用中文单位。即使展开或执行失败,也不会展示完整代码、子调用参数或真实输出;终端宽度不足时优先保留状态、失败信息和调用次数,避免摘要自动换成多行。
109
+ - **Hidden**:隐藏 Codemode 工具记录(包括展开和失败结果),但不影响实际执行或 Tool / MCP 调用统计。
110
+
111
+ Compact 效果示例:
112
+
113
+ ```text
114
+ ▸ codemode 8 行代码
115
+ ✓ 已完成 │ 1 次调用 · 1 成功 │ 59 行输出 · 85ms
116
+ ```
117
+
118
+ 切换 Tool View 模式也会更新当前会话中已经展示的历史工具记录;其他第三方工具保持原有渲染方式。
93
119
 
94
120
  ### 6. Tool / MCP 调用统计
95
121
 
@@ -154,7 +180,7 @@ Pi 会安装 package,并将声明写入个人配置:
154
180
  如果希望固定版本:
155
181
 
156
182
  ```bash
157
- pi install npm:pi-ui-extension@1.1.1
183
+ pi install npm:pi-ui-extension@1.1.11
158
184
  ```
159
185
 
160
186
  或者:
@@ -162,7 +188,7 @@ pi install npm:pi-ui-extension@1.1.1
162
188
  ```json
163
189
  {
164
190
  "packages": [
165
- "npm:pi-ui-extension@1.1.1"
191
+ "npm:pi-ui-extension@1.1.11"
166
192
  ]
167
193
  }
168
194
  ```
@@ -190,15 +216,15 @@ pi install git:github.com/1993yihuan/pi-ui-extension
190
216
  固定到 Git tag:
191
217
 
192
218
  ```bash
193
- pi install git:github.com/1993yihuan/pi-ui-extension@v1.1.1
219
+ pi install git:github.com/1993yihuan/pi-ui-extension@v1.1.11
194
220
  ```
195
221
 
196
- ### 临时试用
222
+ ### 临时加载本地源码
197
223
 
198
- 如果只想在一次 Pi 启动中测试,不修改 `settings.json`:
224
+ 如果只想在一次 Pi 启动中测试本地源码,不修改 `settings.json`,请在仓库根目录运行。禁用自动扩展发现可以避免与已经安装的 `pi-ui-extension` 重复加载:
199
225
 
200
226
  ```bash
201
- pi -e npm:pi-ui-extension
227
+ pi --no-extensions -e builtin:mcp -e ./extensions/index.ts --tui-mode fullscreen
202
228
  ```
203
229
 
204
230
  ### 项目级安装
@@ -289,7 +315,7 @@ npm run typecheck
289
315
  npm test
290
316
  ```
291
317
 
292
- 当前自动化测试覆盖 Sidebar、CTX、Session、Tool View、MCP resource、Fabric detection、鼠标滚轮 / resize 等主要行为。
318
+ 当前自动化测试覆盖 Sidebar、CTX、Session、官方 Tool Renderer、MCP resource、鼠标滚轮 / resize 等主要行为。
293
319
 
294
320
  开发时可以直接让 Pi 加载本地目录:
295
321
 
@@ -314,7 +340,6 @@ pi-ui-extension/
314
340
  │ ├── session/ # Session 列表与操作
315
341
  │ ├── sidebar/ # Sidebar 布局、滚动和 resize
316
342
  │ ├── tool-view/ # Tool View 模式与调用统计
317
- │ ├── fabric-detection.ts # Fabric 工具接管检测
318
343
  │ └── index.ts # Pi Extension 入口
319
344
  ├── .github/workflows/
320
345
  │ ├── ci.yml # push / PR 自动检查
@@ -337,7 +362,7 @@ Pi 的扩展入口在 `package.json` 中显式声明:
337
362
 
338
363
  ## CI / Release
339
364
 
340
- 提交到 `main` 或创建 PR 时,GitHub Actions 会自动执行:
365
+ 提交到 `main` 或创建 PR 时,GitHub Actions 会分别在 **Node.js 22.19.0 和 24** 上执行:
341
366
 
342
367
  ```text
343
368
  npm ci
@@ -357,6 +382,7 @@ git push --tags
357
382
 
358
383
  ```text
359
384
  Checkout
385
+ → 校验版本标签格式、确认提交位于 main 历史
360
386
  → Node.js 24
361
387
  → npm ci
362
388
  → npm run check
@@ -364,6 +390,7 @@ Checkout
364
390
  → npm pack --dry-run
365
391
  → npm Trusted Publishing (OIDC)
366
392
  → npm publish
393
+ → 重试检查 npm Registry 已可查询新版本
367
394
  → GitHub Release
368
395
  ```
369
396
 
@@ -373,16 +400,16 @@ npm 发布使用 GitHub Actions OIDC Trusted Publishing,不需要在 GitHub Re
373
400
 
374
401
  - 不修改 Pi Core。
375
402
  - Pi host 提供的 `@earendil-works/pi-coding-agent` 和 `@earendil-works/pi-tui` 使用 `peerDependencies`,避免产生重复运行时实例。
376
- - 对 Fabric 做显式检测,避免 Tool View 模式控制发生冲突。
403
+ - Tool View 只通过 Pi 官方 Renderer Resolver 控制内置工具的显示,不覆写工具定义或执行逻辑。
377
404
  - Session 与资源操作尽量通过 Pi 已公开 API 完成。
378
405
  - Context composition 明确标记为估算值,不冒充 provider 精确 token 统计。
379
406
  - fullscreen Sidebar 不可用时安全降级,不影响 Pi 主流程。
380
407
 
381
408
  ## Requirements
382
409
 
383
- - Pi Coding Agent
410
+ - 支持官方 `registerToolRenderer()` API 的 Pi Coding Agent(已使用 `@earendil-works/pi-coding-agent@1.1.0` 验证)
384
411
  - Node.js `>= 22.19.0`(建议使用当前 Pi 支持的 Node 版本)
385
- - 开发与 CI 当前使用 Node.js 24
412
+ - CI 覆盖 Node.js 22.19.0 和 24;发布工作流使用 Node.js 24
386
413
 
387
414
  ## License
388
415
 
@@ -6,11 +6,11 @@
6
6
 
7
7
  - approx = 本地可观测文本 UTF-16 长度 / 4,逐消息向上取整;不是模型 tokenizer。已知消息先经公开 SDK `convertToLlm` 转换,再提取文本,因此包含摘要包装、bash 无输出/退出/取消/截断提示;仍按原始角色分类。未知角色安全降级为外层 content 文本,`excludeFromContext` 的 bash 不计入。
8
8
  - 六类为 System、Tools、Messages、Tool Results、Summary、Other。百分比分母是已覆盖分类的估算 token 合计,不是模型窗口;pi usage 单独展示,不校准分类。
9
- - System 使用 ctx.getSystemPrompt() 的可观测字符串;无法取得时 unknown。Tools 通过 pi.getActiveTools()/getAllTools() 仅选取当前启用的工具,逐工具序列化 `{name, description, parameters}`,按 UTF-16 长度 / 4 向上取整后合计,并纳入分类百分比分母。不计 sourceInfo、执行函数及已归入 System 的 promptGuidelines;Fabric 捕获但未直接启用的工具不重复计入。无启用工具为 0;API 不可用、启用工具缺少定义或 schema 无法序列化时保持 unknown,避免显示不完整合计。工具定义在每次现有快照事件刷新时重新读取(包括动态启用/禁用),不在 render 中读取;空闲时注册工具将在下一次快照事件反映。
9
+ - System 使用 ctx.getSystemPrompt() 的可观测字符串;无法取得时 unknown。Tools 通过 pi.getActiveTools()/getAllTools() 仅选取当前启用的工具,逐工具序列化 `{name, description, parameters}`,按 UTF-16 长度 / 4 向上取整后合计,并纳入分类百分比分母。不计 sourceInfo、执行函数及已归入 System 的 promptGuidelines;未直接启用的工具不重复计入。无启用工具为 0;API 不可用、启用工具缺少定义或 schema 无法序列化时保持 unknown,避免显示不完整合计。工具定义在每次现有快照事件刷新时重新读取(包括动态启用/禁用),不在 render 中读取;空闲时注册工具将在下一次快照事件反映。
10
10
  - Tools 在 `before_provider_request` 事件后优先统计 provider-specific payload 的 `tools` 字段序列化文本(仍按 UTF-16 长度 / 4 估算);请求发出前未观察到 payload 时,回退到当前启用工具 schema 的本地估算。它仍不是模型 tokenizer、HTTP 字节数或计费 token,也不包含 headers;如果后续扩展继续改写 payload,面板值可能是本扩展处理时观察到的版本。Images 仅统计图片块数量,不序列化请求或计算未展示的字节数;在 `before_provider_request` 可取得最终 provider payload 时优先统计该 payload,否则统计当前 context 消息;未能观测时显示 `not observed`。
11
11
  - context 事件提供调用前的消息快照;session_start/tree/compact、turn_end、agent_settled 使用 buildContextEntries() 重建 compaction-applied active branch。后者显示 active branch,前者显示 context hook。事件之外只读取缓存,不在 render 中扫描历史。
12
12
  - Messages 包括 user/assistant 文本、thinking 和 tool call 名称/参数;Tool Results 仅计 content;Summary 包括 compaction/branch summary;Other 包括可发送 custom message 和非排除的用户 bash。
13
- - Tool Results 按当前快照工具结果文本估算。Fabric 外层 content 只计一次,不遍历内部操作;不读 details、全量 JSONL、沙箱、截断前原始输出或被压缩结果。已返回 content 内的截断提示本身仍属于可发送文本。
13
+ - Tool Results 按当前快照工具结果文本估算。每条 Tool Result 的 content 只计一次,不遍历内部操作;不读 details、全量 JSONL、沙箱、截断前原始输出或被压缩结果。已返回 content 内的截断提示本身仍属于可发送文本。
14
14
 
15
15
  Composition 的 token 估算使用 `~` 前缀及 K/M 紧凑缩写(四舍五入),百分比保留一位小数。宽度不足时标签与数值拆行;支持的最小侧栏宽 18 可完整显示如 `~100K 100.0%`。
16
16
 
@@ -32,4 +32,4 @@ Composition 的 token 估算使用 `~` 前缀及 K/M 紧凑缩写(四舍五入
32
32
 
33
33
  这是明确标记的本地估算,不宣称最终请求精确统计。后加载扩展仍可能改写 context 或 provider payload;不同 provider 的转换、剔除、包装开销未知。恢复/回合结束时 active branch 可能不同于上次请求经过 context 扩展裁剪后的快照;下一次 context 事件会替换它。模型生成过程中不逐 token 更新分类,最终回合/settled 时更新。只含图片的消息可见文本为零,不代表图片成本为零。
34
34
 
35
- 未修改 pi core、Fabric 或全局配置。全屏鼠标/极短终端还应在用户真实终端手工确认。
35
+ 未修改 Pi Core 或全局配置。全屏鼠标/极短终端还应在用户真实终端手工确认。
@@ -3,8 +3,8 @@ import test from "node:test";
3
3
  import { createReadTool, SessionManager } from "@earendil-works/pi-coding-agent";
4
4
  import { estimateContext, installContextEstimates, messagesFromContextEntries } from "./data.ts";
5
5
 
6
- const result = (n: number) => ({ role: "toolResult", toolName: "fabric_exec", toolCallId: String(n), content: [{ type: "text", text: "x".repeat(n * 4) }], details: { fullOutput: "x".repeat(100000), operations: [{ toolName: "read" }] } });
7
- const tool = { name: "fabric_exec", description: "Execute TypeScript", parameters: createReadTool(process.cwd()).parameters };
6
+ const result = (n: number) => ({ role: "toolResult", toolName: "sample_runner", toolCallId: String(n), content: [{ type: "text", text: "x".repeat(n * 4) }], details: { fullOutput: "x".repeat(100000), operations: [{ toolName: "read" }] } });
7
+ const tool = { name: "sample_runner", description: "Execute TypeScript", parameters: createReadTool(process.cwd()).parameters };
8
8
 
9
9
  test("context estimate keeps tool schemas, summaries, images and active branch semantics", () => {
10
10
  const snapshot = estimateContext([{role:"user",content:"abcd"}, result(4), {role:"compactionSummary",summary:"abcd"}], "abcd", "context hook", [tool]);
@@ -16,7 +16,7 @@ test("context estimate keeps tool schemas, summaries, images and active branch s
16
16
 
17
17
  test("tracker filters active tools, refreshes schemas, caches snapshots and resets", () => {
18
18
  const handlers = new Map<string, Function>();
19
- let active = ["fabric_exec"];
19
+ let active = ["sample_runner"];
20
20
  let all = [tool];
21
21
  const tracker = installContextEstimates({
22
22
  on: (name: string, fn: Function) => handlers.set(name, fn),
@@ -3,7 +3,7 @@ import test from "node:test";
3
3
  import { createReadTool } from "@earendil-works/pi-coding-agent";
4
4
  import { estimateContext, estimateProviderToolsTokens, installContextEstimates } from "./data.ts";
5
5
 
6
- const tool = { name: "fabric_exec", description: "Execute TypeScript", parameters: createReadTool(process.cwd()).parameters };
6
+ const tool = { name: "sample_runner", description: "Execute TypeScript", parameters: createReadTool(process.cwd()).parameters };
7
7
  const expected = Math.ceil(JSON.stringify(tool).length / 4);
8
8
  const toolsCategory = (snapshot: ReturnType<typeof estimateContext>) => snapshot.categories.find(c => c.name === "Tools")!;
9
9
 
@@ -25,7 +25,7 @@ test("Tools counts schema JSON only and contributes to totals and percentages",
25
25
 
26
26
  test("tracker filters active tools, refreshes dynamic schemas, caches reads and resets", () => {
27
27
  const handlers = new Map<string, Function>();
28
- let active = ["fabric_exec", "fabric_exec"];
28
+ let active = ["sample_runner", "sample_runner"];
29
29
  let all = [tool, {...tool,name:"captured_read",description:"not exposed".repeat(100)}];
30
30
  let reads = 0;
31
31
  let fail = false;
@@ -44,7 +44,7 @@ test("tracker filters active tools, refreshes dynamic schemas, caches reads and
44
44
  assert.ok(toolsCategory(tracker.getSnapshot()).tokens! > expected);
45
45
  handlers.get("before_provider_request")!({payload:{tools:[tool]}},ctx);
46
46
  assert.equal(toolsCategory(tracker.getSnapshot()).tokens, Math.ceil(JSON.stringify([tool]).length/4));
47
- all = [{...tool,description:"changed"}]; active = ["fabric_exec"];
47
+ all = [{...tool,description:"changed"}]; active = ["sample_runner"];
48
48
  handlers.get("turn_end")!({},ctx);
49
49
  // The last provider payload remains authoritative through active-branch refresh.
50
50
  assert.equal(toolsCategory(tracker.getSnapshot()).tokens, Math.ceil(JSON.stringify([tool]).length/4));
@@ -12,8 +12,7 @@ import {
12
12
  import { installContextEstimates } from "./ctx/data.ts";
13
13
  import { createActivityTracker, type ActivityTracker } from "./ctx/activity.ts";
14
14
  import { installFooter, type FooterController } from "./footer/index.ts";
15
- import { isFabricFullCodeMode } from "./fabric-detection.ts";
16
- import { mcpResourceEntriesFromConfig, mcpResourceEntriesFromStatus, mergeMcpResourceEntries } from "./mcp/resources.ts";
15
+ import { adapterMcpServers, buildMcpStatusSnapshot, describeMcpStatus, type McpStatusSnapshot } from "./mcp/status-center.ts";
17
16
  import {
18
17
  createDefaultSidebarPanels,
19
18
  SESSION_PANEL_ID,
@@ -103,6 +102,49 @@ function showResourceDialog(
103
102
  });
104
103
  }
105
104
 
105
+ /** Read-only MCP status picker; only safe metadata is displayed. */
106
+ export function showMcpStatusDialog(
107
+ ctx: { ui: { select(title: string, options: string[]): Promise<string | undefined>; editor(title: string, prefill?: string): Promise<string | undefined>; notify(message: string, level: "info" | "warning"): void } },
108
+ snapshot: McpStatusSnapshot,
109
+ ): void {
110
+ const rows = snapshot.servers.map((server) => ({
111
+ name: server.name,
112
+ description: describeMcpStatus(server),
113
+ details: [
114
+ `Server: ${server.name}`,
115
+ `Source: ${server.source}`,
116
+ `Enabled: ${server.enabled ? "yes" : "no"}`,
117
+ `Observed by adapter: ${server.loaded ? "yes" : "no"}`,
118
+ `Status: ${describeMcpStatus(server)}`,
119
+ "",
120
+ "Only an adapter's explicit connection state establishes connected/failed.",
121
+ "Pi native connection health is not exposed to UI extensions.",
122
+ "Use Pi's built-in /mcp manager for live connection details.",
123
+ ].join("\n"),
124
+ }));
125
+ const warnings = snapshot.warnings.map((warning) => ({
126
+ name: "⚠ Configuration warning",
127
+ description: warning,
128
+ details: `${warning}\nCheck your MCP configuration with Pi's built-in /mcp manager.`,
129
+ }));
130
+ const all = [...rows, ...warnings];
131
+ if (all.length === 0) {
132
+ ctx.ui.notify("No MCP servers found. Use /mcp to configure servers.", "info");
133
+ return;
134
+ }
135
+ const choices = resourceDialogOptions(all);
136
+ const enabledCount = snapshot.servers.filter((server) => server.enabled).length;
137
+ void ctx.ui.select(
138
+ `MCP Status · ${enabledCount} enabled / ${snapshot.servers.length} total · ↵ details`,
139
+ choices,
140
+ ).then((choice) => {
141
+ if (!choice) return;
142
+ const index = choices.indexOf(choice);
143
+ const item = all[index];
144
+ if (item) void ctx.ui.editor(`${item.name} · MCP (read-only)`, item.details).then(() => {});
145
+ });
146
+ }
147
+
106
148
  /** Load Agents.md contexts, Skills, and Extensions as display rows. */
107
149
  export async function loadResourceEntries(
108
150
  pi: ExtensionAPI,
@@ -185,9 +227,11 @@ async function clearOtherSessions(
185
227
  export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
186
228
  const toolView = await installToolViewController(pi);
187
229
  let mcpResources: readonly ResourceEntry[] = [];
188
- let liveMcpResources: readonly ResourceEntry[] = [];
189
- const currentMcpResources = (cwd: string, trusted: boolean): readonly ResourceEntry[] =>
190
- mergeMcpResourceEntries(mcpResourceEntriesFromConfig(getAgentDir(), cwd, trusted), liveMcpResources);
230
+ let liveMcpServers: NonNullable<ReturnType<typeof adapterMcpServers>> = [];
231
+ let registeredServers: unknown = [];
232
+ let mcpCwd = process.cwd();
233
+ let mcpTrusted = false;
234
+ let mcpStatus: McpStatusSnapshot = { servers: [], warnings: [] };
191
235
  let toolViewMode: ToolViewMode | undefined;
192
236
  let footer: FooterController | undefined;
193
237
  let sidebar: SidebarController | undefined;
@@ -221,12 +265,24 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
221
265
  };
222
266
  let switchRequested = false;
223
267
  const getMcpServerNames = () => mcpResources.map((server) => server.name);
224
- const unsubscribeMcpStatus = pi.events.on("pi-mcp-adapter/status/v1", (value) => {
225
- liveMcpResources = mcpResourceEntriesFromStatus(value);
226
- mcpResources = mergeMcpResourceEntries(mcpResources, liveMcpResources);
268
+ const updateMcpStatus = (): void => {
269
+ mcpStatus = buildMcpStatusSnapshot(getAgentDir(), mcpCwd, mcpTrusted, registeredServers, liveMcpServers);
270
+ mcpResources = mcpStatus.servers
271
+ .filter((server) => server.enabled)
272
+ .map((server) => ({ name: server.name, description: describeMcpStatus(server) }));
227
273
  const branch = getCurrentBranch?.();
228
274
  if (branch) toolUsage?.resetSession(branch, getMcpServerNames(), { preserveExchange: true });
229
275
  sidebar?.refresh();
276
+ };
277
+ const unsubscribeMcpStatus = pi.events.on("pi-mcp-adapter/status/v1", (value) => {
278
+ const observation = adapterMcpServers(value);
279
+ if (!observation) return;
280
+ liveMcpServers = observation;
281
+ updateMcpStatus();
282
+ });
283
+ pi.on("mcp_servers_change", (event) => {
284
+ registeredServers = event.servers;
285
+ updateMcpStatus();
230
286
  });
231
287
 
232
288
  pi.registerCommand(SESSION_RENAME_COMMAND, {
@@ -456,7 +512,10 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
456
512
  // Pi resets extension UI before starting/reloading a session. Do not call
457
513
  // stale controllers from the previous runtime after that reset.
458
514
  clearControllers();
459
- mcpResources = currentMcpResources(ctx.cwd, ctx.isProjectTrusted());
515
+ mcpCwd = ctx.cwd;
516
+ mcpTrusted = ctx.isProjectTrusted();
517
+ registeredServers = typeof pi.getMcpServers === "function" ? pi.getMcpServers() : [];
518
+ updateMcpStatus();
460
519
  toolViewMode = toolView.getMode();
461
520
  getCurrentBranch = () => ctx.sessionManager.getBranch();
462
521
  const branch = ctx.sessionManager.getBranch();
@@ -464,7 +523,6 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
464
523
  toolUsage = createToolUsageTracker([], getMcpServerNames);
465
524
  toolUsage.resetSession(branch);
466
525
  if (ctx.mode !== "tui") return;
467
- const showToolViewModeSelector = (): boolean => !isFabricFullCodeMode(pi.getActiveTools());
468
526
 
469
527
  activity = createActivityTracker(ctx.cwd, refreshContextUi);
470
528
  footer = installFooter(
@@ -483,7 +541,6 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
483
541
  onSelectToolViewMode: (mode): void => {
484
542
  void toolView.setMode(mode, ctx);
485
543
  },
486
- showToolViewModeSelector,
487
544
  loadSessions: async (): Promise<readonly SessionListItem[]> => {
488
545
  const currentSessionId = ctx.sessionManager.getSessionId();
489
546
  const sessions = await SessionManager.list(ctx.cwd, ctx.sessionManager.getSessionDir());
@@ -534,7 +591,12 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
534
591
  ctx.ui.notify("Wait for the current run to finish before opening resources", "warning");
535
592
  return;
536
593
  }
537
- void loadResourceEntries(pi, ctx.cwd, button === "mcp" ? currentMcpResources(ctx.cwd, ctx.isProjectTrusted()) : mcpResources, ctx.isProjectTrusted(), button)
594
+ if (button === "mcp") {
595
+ updateMcpStatus();
596
+ showMcpStatusDialog(ctx, mcpStatus);
597
+ return;
598
+ }
599
+ void loadResourceEntries(pi, ctx.cwd, mcpResources, ctx.isProjectTrusted(), button)
538
600
  .then((rows) => {
539
601
  if (!rows || rows.length === 0) {
540
602
  ctx.ui.notify("No resources of this kind loaded", "info");
@@ -568,6 +630,7 @@ export default async function piUiExtension(pi: ExtensionAPI): Promise<void> {
568
630
  pi.on("session_shutdown", () => {
569
631
  unsubscribeToolViewMode();
570
632
  unsubscribeMcpStatus();
633
+ liveMcpServers = [];
571
634
 
572
635
  // The sidebar replaces the fullscreen TUI layout root directly, so Pi
573
636
  // cannot tear it down on our behalf during session replacement. Dispose
@@ -11,6 +11,7 @@ test("entry wires early/late MCP status into restored and live usage without dou
11
11
  const handlers = new Map<string, any[]>(), bus = new Map<string, any>();
12
12
  const listeners = new Set<any>();
13
13
  let branch: any[] = [], footer: any;
14
+ let registrations: any[] = [{ name: "native_mcp", config: { command: "not-executed" }, extensionPath: "/extension.ts" }];
14
15
  const tui: any = {
15
16
  [Symbol.for("@earendil-works/pi-tui/viewport")]: true,
16
17
  terminal: { columns: 240, rows: 100 }, layoutRoot: { render: () => [], invalidate() {} }, inputListeners: listeners,
@@ -19,8 +20,9 @@ test("entry wires early/late MCP status into restored and live usage without dou
19
20
  };
20
21
  const theme: any = { fg: (_: string, text: string) => text, bold: (text: string) => text };
21
22
  const pi: any = {
22
- registerTool() {}, registerCommand() {}, registerShortcut() {},
23
+ registerTool() { throw new Error("Tool View must not register tools"); }, registerToolRenderer() {}, registerCommand() {}, registerShortcut() {},
23
24
  getCommands: () => [], getActiveTools: () => [], getAllTools: () => [], getThinkingLevel: () => "off",
25
+ getMcpServers: () => registrations,
24
26
  on(name: string, fn: any) { handlers.set(name, [...(handlers.get(name) ?? []), fn]); },
25
27
  events: { on(name: string, fn: any) { bus.set(name, fn); return () => bus.delete(name); } },
26
28
  };
@@ -41,6 +43,12 @@ test("entry wires early/late MCP status into restored and live usage without dou
41
43
  await emit("session_start");
42
44
  assert.match(output(), /early\s+0\/1/);
43
45
  await emit("before_agent_start", { systemPromptOptions: {} });
46
+ branch.push({ type: "message", message: { role: "toolResult", toolName: "mcp__native_mcp__ping", details: {} } });
47
+ await emit("tool_execution_end", { toolCallId: "native-1", toolName: "mcp__native_mcp__ping", isError: false, result: { details: {} } });
48
+ assert.match(output(), /native_mcp\s+1\/1/);
49
+ registrations = [];
50
+ await emit("mcp_servers_change", { servers: [] });
51
+ assert.doesNotMatch(output(), /native_mcp\s+1\/1/);
44
52
  branch.push(saved("late"));
45
53
  await emit("tool_execution_end", { toolCallId: "call-1", toolName: "mcpScript", isError: false, result: { details: details("late") } });
46
54
  assert.doesNotMatch(output(), /late\s+1\/1/);
@@ -1,5 +1,4 @@
1
- import { readFileSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { adapterMcpServers, configuredMcpServers } from "./status-center.ts";
3
2
 
4
3
  export type McpResourceEntry = {
5
4
  readonly name: string;
@@ -7,26 +6,9 @@ export type McpResourceEntry = {
7
6
  };
8
7
 
9
8
  export function mcpResourceEntriesFromConfig(agentDir: string, cwd: string, trusted: boolean): readonly McpResourceEntry[] {
10
- const paths = [join(agentDir, "mcp.json")];
11
- if (trusted) paths.push(join(cwd, ".pi", "mcp.json"));
12
- const rows = new Map<string, McpResourceEntry>();
13
- for (const path of paths) {
14
- try {
15
- const config: unknown = JSON.parse(readFileSync(path, "utf8"));
16
- if (!config || typeof config !== "object" || Array.isArray(config)) continue;
17
- const servers = (config as { mcpServers?: unknown }).mcpServers;
18
- if (!servers || typeof servers !== "object" || Array.isArray(servers)) continue;
19
- for (const [name, value] of Object.entries(servers)) {
20
- if (!name.trim() || !value || typeof value !== "object" || Array.isArray(value)) continue;
21
- if ((value as { disabled?: unknown }).disabled === true) {
22
- rows.delete(name);
23
- continue;
24
- }
25
- rows.set(name, { name, description: "Configured MCP server (connection not verified)" });
26
- }
27
- } catch { /* Missing or invalid configuration is not fatal. */ }
28
- }
29
- return [...rows.values()];
9
+ return configuredMcpServers(agentDir, cwd, trusted).servers
10
+ .filter((server) => server.enabled)
11
+ .map(({ name }) => ({ name, description: "Configured MCP server (connection not verified)" }));
30
12
  }
31
13
 
32
14
  export function mergeMcpResourceEntries(configured: readonly McpResourceEntry[], live: readonly McpResourceEntry[]): readonly McpResourceEntry[] {
@@ -35,29 +17,9 @@ export function mergeMcpResourceEntries(configured: readonly McpResourceEntry[],
35
17
  return [...rows.values()];
36
18
  }
37
19
 
38
- type McpStatusServer = {
39
- readonly name?: unknown;
40
- readonly disabled?: unknown;
41
- };
42
-
43
20
  /** Convert the adapter's read-only status snapshot into configured, enabled MCP rows. */
44
21
  export function mcpResourceEntriesFromStatus(value: unknown): readonly McpResourceEntry[] {
45
- if (typeof value !== "object" || value === null || Array.isArray(value)) return [];
46
- const snapshot = value as { version?: unknown; servers?: unknown };
47
- if (snapshot.version !== 1 || !Array.isArray(snapshot.servers)) return [];
48
-
49
- const seen = new Set<string>();
50
- const rows: McpResourceEntry[] = [];
51
- for (const rawServer of snapshot.servers) {
52
- if (typeof rawServer !== "object" || rawServer === null || Array.isArray(rawServer)) continue;
53
- const server = rawServer as McpStatusServer;
54
- const name = typeof server.name === "string" ? server.name.trim() : "";
55
- if (!name || server.disabled === true || seen.has(name)) continue;
56
- seen.add(name);
57
- rows.push({
58
- name,
59
- description: "MCP server loaded in the current Pi session",
60
- });
61
- }
62
- return rows;
22
+ return (adapterMcpServers(value) ?? [])
23
+ .filter((server) => server.enabled)
24
+ .map(({ name }) => ({ name, description: "MCP server loaded in the current Pi session" }));
63
25
  }