Pi Agent 命令与快捷键注册
通过扩展注册自定义命令、键盘快捷键和 CLI 标志,让 Pi Agent 更好地适应你的工作流。
注册自定义命令
使用 pi.registerCommand() 注册用户可通过 / 调用的命令。
下面的扩展同时演示了基础命令和带参数自动补全的命令:
实例
// 基础命令
pi.registerCommand("stats", {
description: "显示当前会话的统计信息", // 必填,显示在 / 自动补全列表中
handler: async (args, ctx) => {
const count = ctx.sessionManager.getEntries().length;
ctx.ui.notify(`共 ${count} 条消息`, "info");
}
});
// 带参数自动补全的命令
pi.registerCommand("deploy", {
description: "部署到指定环境",
// 定义参数自动补全,prefix 是用户已输入的参数前缀
getArgumentCompletions: (prefix) => {
const envs = ["dev", "staging", "prod"];
const items = envs
.filter(e => e.startsWith(prefix))
.map(e => ({ value: e, label: e }));
return items.length > 0 ? items : null; // 无匹配时返回 null
},
handler: async (args, ctx) => {
if (!args) {
ctx.ui.notify("用法: /deploy <环境名>", "warning");
return;
}
ctx.ui.notify(`正在部署到 ${args} 环境...`, "info");
},
});
保存后执行 /reload 加载扩展,在编辑区输入 /stats 即可看到效果:
/stats 共 42 条消息
如果多个扩展注册了同名的命令,Pi Agent 会保留所有注册并按加载顺序添加数字后缀,例如 /review:1 和 /review:2。
这避免了扩展之间的命名冲突。
扩展主动发消息
除了被动响应命令和事件,扩展还可以主动驱动对话。
pi.sendMessage(message, deliverAs?) 用于注入自定义消息,pi.sendUserMessage(text, deliverAs?) 则模拟一条用户输入。
deliverAs 有三个取值:steer 是默认值,把消息插入当前轮;followUp 在本轮结束后追加;nextTurn 开启新一轮(仅 sendMessage 支持)。
sendUserMessage 总是会触发一轮回复,因为它对 AI 来说就是一次真实的用户发言。
流式输出期间调用这两个方法时,必须显式给出 deliverAs,否则会直接抛错。
实例
// 以下片段位于扩展工厂函数体内
// 模拟用户补充要求:等当前所有工具执行完再追加
pi.sendUserMessage("部署完成后请顺手检查生产环境日志", {
deliverAs: "followUp",
});
// 只往上下文里塞参考信息,不模拟用户发言
pi.sendMessage({
customType: "deploy-context",
content: "当前部署目标是 production",
display: true,
}, {
deliverAs: "steer", // 默认值,插入当前轮
});
sendMessage 还支持 triggerTurn: true,在 agent 空闲时立即触发一次 LLM 响应。
两者的选择标准很简单:想让 AI 真正接话就用 sendUserMessage,只想提供参考信息就用 sendMessage。
命令上下文(ExtensionCommandContext)
命令的 handler 接收的是 ExtensionCommandContext,它继承自 ExtensionContext。
在普通上下文的基础上,它额外提供以下方法:
| 方法 | 说明 |
|---|---|
| ctx.getSystemPromptOptions() | 读取构建系统提示词的基础输入(contextFiles、skills 等),仅命令上下文可用 |
| ctx.waitForIdle() | 等待 AI 完全空闲(包括重试、压缩、跟进消息全部完成) |
| ctx.newSession(options) | 创建新会话,可指定父会话和初始化逻辑 |
| ctx.fork(entryId, options) | 从指定条目分叉出新会话 |
| ctx.navigateTree(targetId, options) | 导航到会话树中的指定位置 |
| ctx.switchSession(sessionPath, options) | 切换到另一个会话文件 |
| ctx.reload() | 执行与 /reload 相同的热重载流程 |
下面的命令把当前项目的所有会话列成选择列表,选中后直接切换过去:
实例
import { SessionManager } from "@earendil-works/pi-coding-agent";
pi.registerCommand("switch", {
description: "切换到另一个会话",
handler: async (args, ctx) => {
// 列出当前项目的所有会话
const sessions = await SessionManager.list(ctx.cwd);
if (sessions.length === 0) {
ctx.ui.notify("没有可用的会话", "warning");
return;
}
// 弹出选择列表
const choice = await ctx.ui.select(
"选择要切换的会话:",
sessions.map(s => s.file),
);
if (choice) {
// 切换到选中的会话
await ctx.switchSession(choice, {
withSession: async (ctx) => {
ctx.ui.notify("已切换会话", "info");
},
});
}
},
});
/switch ┌ 选择要切换的会话: │ > ~/.pi/agent/sessions/my-project/abc123.jsonl │ ~/.pi/agent/sessions/my-project/def456.jsonl └ 已切换会话
会话替换的注意事项:withSession 回调中的 ctx 是一个全新的上下文。
不要使用旧的 pi 对象或命令回调中捕获的 ctx,它们在会话替换后会变成无效状态。
注册快捷键
使用 pi.registerShortcut() 注册自定义键盘快捷键。
下面把「切换 Plan 模式」绑到一个尚无内置占用的组合键上:
实例
pi.registerShortcut("ctrl+shift+g", {
description: "切换 Plan 模式",
handler: async (ctx) => {
ctx.ui.notify("已切换 Plan 模式!", "info");
},
});
启动后按下组合键即可触发:
已切换 Plan 模式!
选择组合键前先核对默认快捷键表,避免覆盖内置绑定。
例如 ctrl+shift+p 已被「向后切换模型」占用,再注册同名组合键会导致两个行为互相抢键。
快捷键格式为 修饰键+按键,例如 ctrl+shift+g、alt+x 等。
注册 CLI 标志
使用 pi.registerFlag() 为 pi 命令添加自定义 CLI 参数。
下面的例子注册一个 --plan 布尔标志,用于让 AI 先出计划再动手:
实例
pi.registerFlag("plan", {
description: "以 Plan 模式启动",
type: "boolean",
default: false,
});
// 在扩展代码中检查标志值
// 注意:pi.getFlag() 要等到命令行参数解析完成后才有值,
// 因此建议把判断放进事件回调里,而不是写在扩展模块顶层。
// 模块顶层求值发生在扩展加载阶段,此时读到的永远是默认值 false。
pi.on("before_agent_start", async (event, ctx) => {
if (!pi.getFlag("plan")) return; // 未启用 --plan 则不做任何修改
return {
systemPrompt: event.systemPrompt
+ "\n\nPlan Mode:先提出计划,经用户确认后再执行。",
};
});
使用方式:
$ pi --plan "帮我实现用户认证功能"
扩展间通信
使用 pi.events 在扩展之间共享事件。
发送方不需要知道谁在监听,监听方也不需要知道事件从哪里来:
实例
// 扩展 A:发送事件
pi.events.emit("deploy:completed", {
env: "production",
timestamp: Date.now(),
});
// 扩展 B:监听事件(通常写在另一个扩展文件里)
pi.events.on("deploy:completed", (data) => {
console.log(`部署完成:${data.env}`);
});
获取已注册的命令
使用 pi.getCommands() 获取当前会话中所有可用的命令。
下面的例子注册一个 /my-commands 命令,用来列出所有由扩展注册的命令:
实例
pi.registerCommand("my-commands", {
description: "列出扩展注册的所有命令",
handler: async (args, ctx) => {
const commands = pi.getCommands();
// 按来源分类
const extCommands = commands.filter(
c => c.source === "extension"
);
const templates = commands.filter(
c => c.source === "prompt"
);
const skills = commands.filter(
c => c.source === "skill"
);
// 把结果展示出来,避免只计算不输出
ctx.ui.notify(
`扩展命令 ${extCommands.length} 个:`
+ extCommands.map(c => "/" + c.name).join("、"),
"info"
);
console.table(commands);
},
});
/my-commands 扩展命令 2 个:/stats、/deploy
每个命令条目包含以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| name | string | 命令名,在编辑区以 /name 的形式调用 |
| description | string | 命令描述,显示在自动补全列表中 |
| source | string | 来源类型:extension、prompt、skill 等 |
| sourceInfo | object | 详细的来源元数据,例如注册它的扩展路径 |
动态工具管理
运行时管理工具的启用和禁用。
典型用途是临时关掉写操作工具,让 AI 在一次任务里只能读不能改:
实例
// 获取当前启用的工具列表
const active = pi.getActiveTools();
// 例如:["read", "bash", "edit", "write"]
// 获取所有已注册工具的元数据
const all = pi.getAllTools();
// 筛选内置工具
const builtinTools = all.filter(
t => t.sourceInfo.source === "builtin"
);
// 筛选扩展注册的工具
const extTools = all.filter(
t => t.sourceInfo.source !== "builtin"
&& t.sourceInfo.source !== "sdk"
);
// 动态启用自定义工具(保留现有工具)
pi.setActiveTools([...new Set([...active, "my_tool"])]);
// 切换到只读模式:只保留读取类工具,
// 不包含 bash / edit / write,AI 在此状态下无法执行命令或改动文件
pi.setActiveTools(["read", "grep", "find", "ls"]);
这里的 setActiveTools 与命令行的 --tools / --exclude-tools 参数作用于同一个工具集合。
CLI 参数决定启动时的初始集合,setActiveTools 在运行时动态调整,两者可以配合使用。
传入 setActiveTools 的变更必须是增量的(additive),在同一次调用中移除当前已激活的工具会失去延迟加载优化。
另外,激活带 promptSnippet 或 promptGuidelines 的工具会重建系统提示词,即使模型支持延迟加载,也可能使缓存的提示词前缀失效。
