Creating Extensions
Extensions are the building blocks of tallow. Each extension provides specific functionality that ships bundled with the application.
Extension Structure
Section titled “Extension Structure”Each extension lives in the extensions/ directory:
extensions/my-extension/├── extension.json # Manifest (required)├── index.ts # Entry point└── README.md # Documentation (optional)The Manifest (extension.json)
Section titled “The Manifest (extension.json)”Every extension needs an extension.json manifest. This file is the source of truth for extension metadata.
{ "name": "my-extension", "version": "0.1.0", "description": "What the extension does in one line", "category": "tool", "tags": ["utility"], "files": ["index.ts"], "relationships": [], "npmDependencies": {}}Fields
Section titled “Fields”| Field | Required | Description |
|---|---|---|
name | Yes | Unique extension name. Must match the directory name. |
version | Yes | Semver version string. |
description | Yes | One-line description. |
category | No | One of tool, ui, utility, command, integration, language-support, context, alias. |
tags | No | Searchable tags. |
files | Yes | Files included with the extension. Glob patterns supported. extension.json is always included automatically. |
relationships | No | Dependencies and integrations (see below). |
npmDependencies | No | npm packages needed at runtime. |
configFiles | No | Config files to copy or merge into the install root. |
piVersion | No | Minimum pi version required. |
Relationships
Section titled “Relationships”Extensions can declare relationships with each other:
{ "relationships": [ { "name": "hooks", "kind": "requires", "reason": "Needs hook system for event handling" }, { "name": "tasks", "kind": "enhances", "reason": "Adds progress tracking to task widget" }, { "name": "old-extension", "kind": "conflicts", "reason": "Replaces old-extension entirely" } ]}requires, Hard dependency. Must be present.enhances, Soft link. If both are present, they integrate.conflicts, Cannot coexist.
Basic Extension
Section titled “Basic Extension”import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
export default function (pi: ExtensionAPI) { // Register a slash command pi.registerCommand("greet", { description: "Say hello", handler: async (args, ctx) => { ctx.ui.notify(`Hello, ${args || "world"}!`, "info"); }, });
// Hook into lifecycle events pi.on("session_start", async (_event, ctx) => { // Runs when a session starts });}Development Workflow
Section titled “Development Workflow”After creating or modifying an extension:
# Typecheck extensionsnpm run typecheck:extensions
# Build the projectnpm run build
# Test by running tallowtallowType Safety
Section titled “Type Safety”Extensions are typechecked against the pi API. The project includes a dedicated tsconfig.extensions.json and the pre-commit hook validates all extensions before allowing commits.
# Run extension typecheck manuallynpm run typecheck:extensionsKey type rules
Section titled “Key type rules”- Tool return types,
execute()must returnAgentToolResult<T>withcontentanddetails. Useas conston content type literals (type: "text" as const). - Widget render callbacks,
setWidgetfactories receive(tui, theme), but viewportwidthcomes from therender(width)method, not fromtui. - globalThis state, If your extension stores cross-reload state on
globalThis, declare the property inextensions/global.d.ts:
declare global { var __piMyExtensionState: Map<string, unknown> | undefined;}export {};Available type packages
Section titled “Available type packages”These are available as devDependencies for import:
@mariozechner/pi-coding-agent,ExtensionAPI,ExtensionContext, tools@mariozechner/pi-tui,TUI,Container,Text,Key@mariozechner/pi-ai,Model,TextContent,ImageContent@mariozechner/pi-agent-core,AgentToolResult,AgentToolUpdateCallback@sinclair/typebox,Typefor parameter schemas
Best Practices
Section titled “Best Practices”- Single responsibility, each extension should do one thing well
- Document your extension, include a README with usage examples
- Handle errors gracefully, provide meaningful error messages via
ctx.ui.notify() - Declare relationships, if your extension depends on or enhances another, say so in the manifest
- Use
deliverAsfor async messages, if callingpi.sendUserMessage()from an async context (subprocess callback, timer), pass{ deliverAs: "followUp" }to avoid errors when the agent is mid-turn