背景
Pi Agent 安装 @georgedong32/permission-modes 和 pi-zentui 后出现两个定制需求:
- 保留 Zentui 的 Starship Footer,同时显示当前权限模式。
- 通过
/effort命令查看或调整模型的 Thinking 强度。
相关环境:
- Pi:
0.84.2 - Permission Modes:
@georgedong32/permission-modes@2.6.3 - UI:
pi-zentui
对 Pi Agent 插件机制的理解
Extension 是运行时模块
Pi 的插件主体称为 Extension,本质上是由 Pi 直接加载的 TypeScript 模块。模块默认导出一个工厂函数,并通过传入的 ExtensionAPI 注册能力:
| |
常见扩展点包括:
| API | 用途 |
|---|---|
pi.registerCommand() | 注册 /command 命令 |
pi.registerTool() | 注册可由模型调用的工具 |
pi.registerShortcut() | 注册交互式快捷键 |
pi.on() | 监听会话、模型、工具调用等生命周期事件 |
ctx.ui.setStatus() | 发布可组合的状态信息 |
ctx.ui.setFooter() | 替换整个 Footer |
pi.setThinkingLevel() | 调整当前模型的 Thinking 等级 |
pi.setActiveTools() | 动态启用或禁用工具 |
Extension 使用当前 Pi 进程的系统用户权限运行,因此可以访问文件、网络和进程。它不是安全沙箱,第三方 Extension 安装前需要审查来源和实现。
加载位置与作用域
常用加载位置:
| 位置 | 作用域 |
|---|---|
~/.pi/agent/extensions/*.ts | 当前用户的所有项目 |
.pi/extensions/*.ts | 当前项目,受 Project Trust 控制 |
~/.pi/agent/settings.json 的 packages | 全局 npm 或 Git Pi Package |
.pi/settings.json 的 packages | 项目级 Pi Package |
npm Package 可通过以下命令管理:
| |
本次两个自定义功能放在 ~/.pi/agent/extensions/,原因是它们属于个人工作流,并且不应随某个项目仓库共享。
注册阶段与生命周期
Extension 工厂函数负责注册,不适合直接启动长期定时器或后台资源。需要长期运行的逻辑应在 session_start 中启动,并在 session_shutdown 中释放:
| |
执行 /reload 时,Pi 会先触发旧 Extension 实例的 session_shutdown,重新加载 Extension、Skills、Prompts、Theme 和上下文文件,再触发新实例的 session_start。因此 /reload 是修改本地 Extension 后的正确生效方式。
会话切换、Fork 和 Reload 后,旧的 ctx 及其会话对象会失效,不能被后台回调继续持有。本次桥接定时器在 session_shutdown 中清理,就是为了避免使用失效上下文。
UI 能力的组合方式
Pi 的 UI API 并不都具备相同的组合语义:
setStatus(key, value)使用带键状态,多个 Extension 可以分别发布状态,适合与 Zentui 等聚合 Footer 配合。setFooter(factory)替换整个 Footer,同一时刻只能有一个实际拥有者;多个 Extension 调用时会出现后写覆盖前写。- 自定义 Editor、Working Line 等全局 UI 能力也可能存在所有权竞争,需要查看插件是否支持包装已有组件或显式交接所有权。
因此,本次没有让桥接 Extension 再调用 setFooter(),而是让它发布 permission-mode 状态,由 Zentui 统一渲染 Footer。这比两个插件争夺 Footer 更符合组合式设计。
加载顺序与冲突
多个 Extension 修改同一个全局 UI 资源时,加载顺序会影响结果。本次将 Zentui 放在 packages 数组末尾,使它最后安装并拥有 Footer。
快捷键还存在额外的保留规则。Pi 0.84.2 将 Shift+Tab 保留给内置的 app.thinking.cycle,因此 permission-modes 注册的同名快捷键会被跳过,而不是覆盖内置行为。解决这类冲突时,应通过 ~/.pi/agent/keybindings.json 重新绑定内置动作,为 Extension 释放按键,而不是依赖插件加载顺序。
工具权限拦截通常通过 tool_call 事件实现:Extension 可以检查参数、修改输入或返回 { block: true }。这属于 Pi 进程内的策略层;即使策略实现了 allow / ask / deny,也不等同于 Bubblewrap、容器或虚拟机提供的 OS 级隔离。
本次定制体现的设计原则
- 一个 Extension 一个职责:Footer 桥接与
/effort命令分别放在独立文件。 - 优先组合而不是覆盖:使用
setStatus()接入 Zentui,而不是创建第二个 Footer。 - 不修改依赖源码:避免
pi update --extensions覆盖node_modules中的手工修改。 - 正确管理生命周期:会话开始时初始化,结束时清理定时器。
- 验证实际生效值:设置 Thinking 后通过
getThinkingLevel()检查模型最终采用的等级。 - 区分策略与隔离:Permission Extension 是授权判断层,不应被误认为系统安全边界。
自定义一:在 Zentui Footer 中显示 Permission Mode
问题原因
permission-modes 和 Zentui 都会调用 ctx.ui.setFooter()。Pi 同一时间只能使用一个自定义 Footer,因此后加载的 Extension 会覆盖先加载的 Extension。
permission-modes 当前还会清除名为 modes 的 Extension Status,无法直接利用 Zentui 已有的第三方状态展示能力。
处理方案
创建独立桥接 Extension:
| |
桥接逻辑:
- 读取
permission-modes发布的环境变量PERMISSION_MODES_INHERITED_MODE。 - 将
ask、plan、auto、bypass映射为可读标签。 - 通过
ctx.ui.setStatus("permission-mode", ...)发布状态。 - 每 200 毫秒检查一次模式,仅在模式变化时更新状态。
- 在
session_shutdown时清理定时器,避免资源泄漏。
状态标签:
| 模式 | Footer 标签 |
|---|---|
ask | ● Ask |
plan | ⏸ Plan |
auto | ▶ Auto |
bypass | ⚡ Bypass |
同时调整全局包加载顺序,确保 Zentui 最后安装 Footer:
| |
配置文件位置:
| |
Zentui 会将 permission-mode 视为第三方 Extension Status。显示位置可在 /zentui 的 Extensions 页面中设置为左侧、中间或右侧。
方案特点
- 不直接修改 npm 包中的
node_modules。 - 执行
pi update --extensions后不会因包更新而丢失。 - Permission Modes 继续负责权限控制,Zentui 独占 Footer 渲染。
- 桥接 Extension 只负责状态同步,职责独立。
自定义二:增加 /effort 命令
文件位置
创建独立 Extension:
| |
该 Extension 只负责注册 /effort,不与 Permission Mode 或 Zentui 桥接逻辑混合。
支持的 Thinking 等级
| |
使用方法
不带参数时打开选择菜单:
| |
直接指定等级:
| |
命令还提供参数补全,并验证输入是否合法。设置后通过 pi.getThinkingLevel() 读取实际生效等级;如果当前模型不支持请求的等级并发生降级,会显示实际应用值。
应用配置
创建或修改 Extension 后,应使用独立的重载命令:
| |
/settings reload 不是有效的重载方式;/settings 与 /reload 是两个独立命令。
验证结果
已完成以下静态验证:
permission-mode-zentui.ts通过 Node TypeScript 语法检查。effort-command.ts通过 Node TypeScript 语法检查。~/.pi/agent/settings.json可以被正常解析为 JSON。- 包加载顺序已调整为 Permission Modes 在前、Zentui 在后。
实际 Footer 展示和 /effort 交互需要在当前 Pi 会话执行 /reload 后确认。
注意事项
- Permission Modes 的 Auto 模式依赖分类模型判断风险;分类模型不可用时,写入可能 fail-closed。需要维护全局配置时可先切换到
/ask,再明确批准目标操作。 bypass模式会放行高风险操作,不应作为日常默认模式。- 桥接 Extension 依赖
PERMISSION_MODES_INHERITED_MODE。如果 Permission Modes 后续版本变更或移除该环境变量,需要同步调整桥接实现。 - Pi Extension 与第三方包以当前用户权限运行,不属于 OS 级安全沙箱。