Pi Agent Command and Shortcut Registration

Register custom commands, keyboard shortcuts, and CLI flags through extensions to make Pi Agent adapt better to your workflow.


Register Custom Commands

Usagepi.registerCommand()Register commands that users can invoke with /.

The following extension demonstrates both a basic command and a command with parameter auto-completion:

Example

// File path: ~/.pi/agent/extensions/stats-command.ts

// Basic command
pi.registerCommand("stats", {
  description: "Show statistics for the current session", // Required, displayed in the / auto-completion list
  handler: async (args, ctx) => {
    const count = ctx.sessionManager.getEntries().length;
    ctx.ui.notify(`Total ${count}messages`, "info");
  }
});

// Command with parameter auto-completion
pi.registerCommand("deploy", {
  description: "Deploy to the specified environment",
  // Define parameter auto-completion; prefix is the parameter prefix the user has already entered
  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; // Return null when there is no match
  },
  handler: async (args, ctx) => {
    if (!args) {
      ctx.ui.notify("Usage: /deploy <environment name>", "warning");
      return;
    }
    ctx.ui.notify(`Deploying to ${args}environment...`, "info");
  },
});

After saving, run /reload to load the extension, then type /stats in the editor area to see the effect:

/stats
共 42 条消息

If multiple extensions register commands with the same name, Pi Agent keeps all registrations and appends numeric suffixes in load order, e.g., /review:1 and /review:2.

This avoids naming conflicts between extensions.


Extensions Proactively Sending Messages

In addition to passively responding to commands and events, extensions can also proactively drive the conversation.

pi.sendMessage(message, deliverAs?)Used to inject custom messages,pi.sendUserMessage(text, deliverAs?)simulates a user input.

deliverAs has three values: steer is the default, which inserts the message into the current turn; followUp appends it after this turn; nextTurn starts a new turn (only supported by sendMessage).

sendUserMessage always triggers a reply, because to the AI it is a real user utterance.

When calling these two methods during streaming output, you must explicitly provide deliverAs, otherwise an error will be thrown immediately.

Example

// File path: ~/.pi/agent/extensions/follow-up.ts
// The following snippet is inside the extension factory function body

// Simulate a user's supplementary request: append after all current tools have finished executing
pi.sendUserMessage("After deployment, please also check the production environment logs", {
  deliverAs: "followUp",
});

// Only inject reference information into the context, without simulating a user utterance
pi.sendMessage({
  customType: "deploy-context",
  content: "The current deployment target is production",
  display: true,
}, {
  deliverAs: "steer", // Default value, inserted into the current turn
});

sendMessage also supports triggerTurn: true, which immediately triggers an LLM response when the agent is idle.

The choice between the two is simple: use sendUserMessage if you want the AI to actually respond, and sendMessage if you only want to provide reference information.


Command Context (ExtensionCommandContext)

The command handler receives anExtensionCommandContextExtensionCommandContext, which inherits from ExtensionContext.

On top of the normal context, it additionally provides the following methods:

MethodDescription
ctx.getSystemPromptOptions()Read the base input for building the system prompt (contextFiles, skills, etc.), only available in command context.
ctx.waitForIdle()Wait until the AI is completely idle (including retries, compaction, and follow-up messages all completed).
ctx.newSession(options)Create a new session, with the ability to specify a parent session and initialization logic.
ctx.fork(entryId, options)Fork a new session from a specified entry.
ctx.navigateTree(targetId, options)Navigate to a specified position in the session tree.
ctx.switchSession(sessionPath, options)Switch to another session file.
ctx.reload()Perform the same hot-reload process as /reload.

The following command lists all sessions of the current project as a selection list; once one is selected, it switches directly to it:

Example

// File path: ~/.pi/agent/extensions/switch-session.ts
import { SessionManager } from "@earendil-works/pi-coding-agent";

pi.registerCommand("switch", {
  description: "Switch to another session",
  handler: async (args, ctx) => {
    // List all sessions of the current project
    const sessions = await SessionManager.list(ctx.cwd);
    if (sessions.length === 0) {
      ctx.ui.notify("No available sessions", "warning");
      return;
    }

    // Pop up a selection list
    const choice = await ctx.ui.select(
      "Select the session to switch to:",
      sessions.map(s => s.file),
    );

    if (choice) {
      // Switch to the selected session
      await ctx.switchSession(choice, {
        withSession: async (ctx) => {
          ctx.ui.notify("Session switched", "info");
        },
      });
    }
  },
});
/switch
┌ 选择要切换的会话:
│ > ~/.pi/agent/sessions/my-project/abc123.jsonl
│   ~/.pi/agent/sessions/my-project/def456.jsonl
└
已切换会话

Notes on Session Replacement: the ctx in the withSession callback is a brand-new context.

Do not use the old pi object or the ctx captured in command callbacks; they become invalid after session replacement.


Register Keyboard Shortcuts

Usagepi.registerShortcut()Register custom keyboard shortcuts.

The following binds "Toggle Plan Mode" to a key combination that is not yet used by any built-in binding:

Example

// File path: ~/.pi/agent/extensions/plan-shortcut.ts

pi.registerShortcut("ctrl+shift+g", {
  description: "Toggle Plan Mode",
  handler: async (ctx) => {
    ctx.ui.notify("Plan Mode toggled!", "info");
  },
});

After startup, press the key combination to trigger it:

已切换 Plan 模式!

Before choosing a key combination, check the default shortcut table to avoid overriding built-in bindings.

For example, ctrl+shift+p is already taken by "Switch to Previous Model"; registering the same key combination again will cause the two behaviors to conflict.

The shortcut format ismodifier key + key, e.g., ctrl+shift+g, alt+x, etc.


Register CLI Flags

Usagepi.registerFlag()Add custom CLI arguments to the pi command.

The following example registers a --plan boolean flag to have the AI produce a plan first before acting:

Example

// File path: ~/.pi/agent/extensions/plan-flag.ts

pi.registerFlag("plan", {
  description: "Start in Plan Mode",
  type: "boolean",
  default: false,
});

// Check the flag value in extension code
// Note: pi.getFlag() has a value only after command-line argument parsing is complete,
// so it is recommended to put the check inside an event callback rather than at the top level of the extension module.
// Top-level module evaluation happens during extension loading, at which point it always reads the default value false.
pi.on("before_agent_start", async (event, ctx) => {
  if (!pi.getFlag("plan")) return; // If --plan is not enabled, make no modifications
  return {
    systemPrompt: event.systemPrompt
      + "\n\nPlan Mode: propose a plan first, then execute after user confirmation.",
  };
});

Usage:

$ pi --plan "帮我实现用户认证功能"

Inter-Extension Communication

Usagepi.eventsShare events between extensions.

The sender does not need to know who is listening, and the listener does not need to know where the event comes from:

Example

// File path: ~/.pi/agent/extensions/deploy-events.ts

// Extension A: send events
pi.events.emit("deploy:completed", {
  env: "production",
  timestamp: Date.now(),
});

// Extension B: listen for events (usually written in another extension file)
pi.events.on("deploy:completed", (data) => {
  console.log(`Deployment complete: ${data.env}`);
});

Get Registered Commands

Usagepi.getCommands()Get all available commands in the current session.

The following example registers a /my-commands command to list all commands registered by extensions:

Example

// File path: ~/.pi/agent/extensions/list-commands.ts

pi.registerCommand("my-commands", {
  description: "List all commands registered by extensions",
  handler: async (args, ctx) => {
    const commands = pi.getCommands();

    // Categorize by source
    const extCommands = commands.filter(
      c => c.source === "extension"
    );
    const templates = commands.filter(
      c => c.source === "prompt"
    );
    const skills = commands.filter(
      c => c.source === "skill"
    );

    // Display the result to avoid only calculating without outputting
    ctx.ui.notify(
`Extension commands ${extCommands.length}:`
        + extCommands.map(c => "/" + c.name).join("、"),
      "info"
    );
    console.table(commands);
  },
});
/my-commands
扩展命令 2 个:/stats、/deploy

Each command entry contains the following fields:

FieldTypeDescription
namestringCommand name, invoked as /name in the editor area.
descriptionstringCommand description, displayed in the auto-completion list
sourcestringSource type: extension, prompt, skill, etc.
sourceInfoobjectDetailed source metadata, e.g., the extension path that registered it

Dynamic Tool Management

Enable and disable tools at runtime.

A typical use case is to temporarily disable write-operation tools so that the AI can only read but not modify during a single task:

Example

// File path: ~/.pi/agent/extensions/manage-tools.ts

// Get the list of currently enabled tools
const active = pi.getActiveTools();
// For example: ["read", "bash", "edit", "write"]

// Get metadata for all registered tools
const all = pi.getAllTools();

// Filter built-in tools
const builtinTools = all.filter(
  t => t.sourceInfo.source === "builtin"
);

// Filter tools registered by extensions
const extTools = all.filter(
  t => t.sourceInfo.source !== "builtin"
    && t.sourceInfo.source !== "sdk"
);

// Dynamically enable custom tools (keeping existing tools)
pi.setActiveTools([...new Set([...active, "my_tool"])]);

// Switch to read-only mode: keep only read-type tools,
// excluding bash / edit / write; in this state, the AI cannot execute commands or modify files
pi.setActiveTools(["read", "grep", "find", "ls"]);

Here, setActiveTools and the command-line--tools / --exclude-toolsparameter act on the same tool set.

The CLI parameter determines the initial set at startup, while setActiveTools dynamically adjusts it at runtime; the two can be used together.

Changes passed to setActiveTools must be additive; removing currently activated tools in the same call will lose the lazy-loading optimization.

Additionally, activating tools with promptSnippet or promptGuidelines will rebuild the system prompt; even if the model supports lazy loading, it may invalidate the cached prompt prefix.

Other extensions