Merge branch 'main' into feat/tui-overlay-options

This commit is contained in:
Mario Zechner 2026-01-13 22:06:02 +01:00
commit 7d45e434de
90 changed files with 10277 additions and 1700 deletions

View file

@ -18,6 +18,7 @@ Extensions are TypeScript modules that extend pi's behavior. They can subscribe
- Git checkpointing (stash at each turn, restore on branch)
- Path protection (block writes to `.env`, `node_modules/`)
- Custom compaction (summarize conversation your way)
- Conversation summaries (see `summarize.ts` example)
- Interactive tools (questions, wizards, custom dialogs)
- Stateful tools (todo lists, connection pools)
- External integrations (file watchers, webhooks, CI triggers)
@ -438,7 +439,7 @@ pi.on("before_agent_start", async (event, ctx) => {
});
```
**Examples:** [claude-rules.ts](../examples/extensions/claude-rules.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [ssh.ts](../examples/extensions/ssh.ts)
**Examples:** [claude-rules.ts](../examples/extensions/claude-rules.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [ssh.ts](../examples/extensions/ssh.ts)
#### agent_start / agent_end
@ -452,7 +453,7 @@ pi.on("agent_end", async (event, ctx) => {
});
```
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
#### turn_start / turn_end
@ -468,7 +469,7 @@ pi.on("turn_end", async (event, ctx) => {
});
```
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [status-line.ts](../examples/extensions/status-line.ts)
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [status-line.ts](../examples/extensions/status-line.ts)
#### context
@ -482,7 +483,7 @@ pi.on("context", async (event, ctx) => {
});
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts)
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
### Model Events
@ -527,7 +528,7 @@ pi.on("tool_call", async (event, ctx) => {
});
```
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [protected-paths.ts](../examples/extensions/protected-paths.ts)
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [protected-paths.ts](../examples/extensions/protected-paths.ts)
#### tool_result
@ -549,7 +550,7 @@ pi.on("tool_result", async (event, ctx) => {
});
```
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
### User Bash Events
@ -723,7 +724,7 @@ pi.registerTool({
});
```
**Examples:** [hello.ts](../examples/extensions/hello.ts), [question.ts](../examples/extensions/question.ts), [todo.ts](../examples/extensions/todo.ts), [truncated-tool.ts](../examples/extensions/truncated-tool.ts)
**Examples:** [hello.ts](../examples/extensions/hello.ts), [question.ts](../examples/extensions/question.ts), [questionnaire.ts](../examples/extensions/questionnaire.ts), [todo.ts](../examples/extensions/todo.ts), [truncated-tool.ts](../examples/extensions/truncated-tool.ts)
### pi.sendMessage(message, options?)
@ -748,7 +749,7 @@ pi.sendMessage({
- `"nextTurn"` - Queued for next user prompt. Does not interrupt or trigger anything.
- `triggerTurn: true` - If agent is idle, trigger an LLM response immediately. Only applies to `"steer"` and `"followUp"` modes (ignored for `"nextTurn"`).
**Examples:** [file-trigger.ts](../examples/extensions/file-trigger.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
**Examples:** [file-trigger.ts](../examples/extensions/file-trigger.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
### pi.sendUserMessage(content, options?)
@ -795,7 +796,7 @@ pi.on("session_start", async (_event, ctx) => {
});
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [snake.ts](../examples/extensions/snake.ts), [tools.ts](../examples/extensions/tools.ts)
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [snake.ts](../examples/extensions/snake.ts), [tools.ts](../examples/extensions/tools.ts)
### pi.setSessionName(name)
@ -830,7 +831,7 @@ pi.registerCommand("stats", {
});
```
**Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts), [custom-header.ts](../examples/extensions/custom-header.ts), [handoff.ts](../examples/extensions/handoff.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [send-user-message.ts](../examples/extensions/send-user-message.ts), [snake.ts](../examples/extensions/snake.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
**Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts), [custom-header.ts](../examples/extensions/custom-header.ts), [handoff.ts](../examples/extensions/handoff.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [send-user-message.ts](../examples/extensions/send-user-message.ts), [snake.ts](../examples/extensions/snake.ts), [summarize.ts](../examples/extensions/summarize.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
### pi.registerMessageRenderer(customType, renderer)
@ -849,7 +850,7 @@ pi.registerShortcut("ctrl+shift+p", {
});
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts)
### pi.registerFlag(name, options)
@ -868,7 +869,7 @@ if (pi.getFlag("--plan")) {
}
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts)
### pi.exec(command, args, options?)
@ -892,7 +893,7 @@ const names = all.map(t => t.name); // Just names if needed
pi.setActiveTools(["read", "bash"]); // Switch to read-only
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)
### pi.setModel(model)
@ -1243,7 +1244,7 @@ ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"
```
**Examples:**
- `ctx.ui.select()`: [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [question.ts](../examples/extensions/question.ts)
- `ctx.ui.select()`: [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [question.ts](../examples/extensions/question.ts), [questionnaire.ts](../examples/extensions/questionnaire.ts)
- `ctx.ui.confirm()`: [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts)
- `ctx.ui.editor()`: [handoff.ts](../examples/extensions/handoff.ts)
- `ctx.ui.setEditorText()`: [handoff.ts](../examples/extensions/handoff.ts), [qna.ts](../examples/extensions/qna.ts)
@ -1345,8 +1346,8 @@ ctx.ui.theme.fg("accent", "styled text"); // Access current theme
```
**Examples:**
- `ctx.ui.setStatus()`: [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [status-line.ts](../examples/extensions/status-line.ts)
- `ctx.ui.setWidget()`: [plan-mode.ts](../examples/extensions/plan-mode.ts)
- `ctx.ui.setStatus()`: [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [status-line.ts](../examples/extensions/status-line.ts)
- `ctx.ui.setWidget()`: [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
- `ctx.ui.setFooter()`: [custom-footer.ts](../examples/extensions/custom-footer.ts)
- `ctx.ui.setHeader()`: [custom-header.ts](../examples/extensions/custom-header.ts)
- `ctx.ui.setEditorComponent()`: [modal-editor.ts](../examples/extensions/modal-editor.ts)
@ -1395,9 +1396,22 @@ const result = await ctx.ui.custom<string | null>(
);
```
Overlay components should define a `width` property to control their size. The overlay is centered by default. See [overlay-test.ts](../examples/extensions/overlay-test.ts) for a complete example.
For advanced positioning (anchors, margins, percentages, responsive visibility), pass `overlayOptions`. Use `onHandle` to control visibility programmatically:
**Examples:** [handoff.ts](../examples/extensions/handoff.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [snake.ts](../examples/extensions/snake.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts), [overlay-test.ts](../examples/extensions/overlay-test.ts)
```typescript
const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
{
overlay: true,
overlayOptions: { anchor: "top-right", width: "50%", margin: 2 },
onHandle: (handle) => { /* handle.setHidden(true/false) */ }
}
);
```
See [tui.md](tui.md) for the full `OverlayOptions` API and [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) for examples.
**Examples:** [handoff.ts](../examples/extensions/handoff.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [snake.ts](../examples/extensions/snake.ts), [summarize.ts](../examples/extensions/summarize.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts), [overlay-test.ts](../examples/extensions/overlay-test.ts)
### Custom Editor

View file

@ -735,12 +735,12 @@ import {
discoverAuthStorage,
discoverModels,
discoverSkills,
discoverHooks,
discoverCustomTools,
discoverExtensions,
discoverContextFiles,
discoverPromptTemplates,
loadSettings,
buildSystemPrompt,
createEventBus,
} from "@mariozechner/pi-coding-agent";
// Auth and Models
@ -754,19 +754,16 @@ const builtIn = getModel("anthropic", "claude-opus-4-5"); // Built-in only
// Skills
const { skills, warnings } = discoverSkills(cwd, agentDir, skillsSettings);
// Hooks (async - loads TypeScript)
// Pass eventBus to share pi.events across hooks/tools
// Extensions (async - loads TypeScript)
// Pass eventBus to share pi.events across extensions
const eventBus = createEventBus();
const hooks = await discoverHooks(eventBus, cwd, agentDir);
// Custom tools (async - loads TypeScript)
const tools = await discoverCustomTools(eventBus, cwd, agentDir);
const { extensions, errors } = await discoverExtensions(eventBus, cwd, agentDir);
// Context files
const contextFiles = discoverContextFiles(cwd, agentDir);
// Prompt templates
const commands = discoverPromptTemplates(cwd, agentDir);
const templates = discoverPromptTemplates(cwd, agentDir);
// Settings (global + project merged)
const settings = loadSettings(cwd, agentDir);
@ -816,8 +813,8 @@ import {
SettingsManager,
readTool,
bashTool,
type HookFactory,
type CustomTool,
type ExtensionFactory,
type ToolDefinition,
} from "@mariozechner/pi-coding-agent";
// Set up auth storage (custom location)
@ -831,16 +828,16 @@ if (process.env.MY_KEY) {
// Model registry (no custom models.json)
const modelRegistry = new ModelRegistry(authStorage);
// Inline hook
const auditHook: HookFactory = (api) => {
api.on("tool_call", async (event) => {
// Inline extension
const auditExtension: ExtensionFactory = (pi) => {
pi.on("tool_call", async (event) => {
console.log(`[Audit] ${event.toolName}`);
return undefined;
});
};
// Inline tool
const statusTool: CustomTool = {
const statusTool: ToolDefinition = {
name: "status",
label: "Status",
description: "Get system status",
@ -872,8 +869,8 @@ const { session } = await createAgentSession({
systemPrompt: "You are a minimal assistant. Be concise.",
tools: [readTool, bashTool],
customTools: [{ tool: statusTool }],
hooks: [{ factory: auditHook }],
customTools: [statusTool],
extensions: [auditExtension],
skills: [],
contextFiles: [],
promptTemplates: [],
@ -961,7 +958,7 @@ The SDK is preferred when:
- You want type safety
- You're in the same Node.js process
- You need direct access to agent state
- You want to customize tools/hooks programmatically
- You want to customize tools/extensions programmatically
RPC mode is preferred when:
- You're integrating from another language
@ -984,12 +981,11 @@ discoverModels
// Discovery
discoverSkills
discoverHooks
discoverCustomTools
discoverExtensions
discoverContextFiles
discoverPromptTemplates
// Event Bus (for shared hook/tool communication)
// Event Bus (for shared extension communication)
createEventBus
// Helpers
@ -1015,8 +1011,9 @@ createGrepTool, createFindTool, createLsTool
// Types
type CreateAgentSessionOptions
type CreateAgentSessionResult
type CustomTool
type HookFactory
type ExtensionFactory
type ExtensionAPI
type ToolDefinition
type Skill
type PromptTemplate
type Settings
@ -1024,28 +1021,4 @@ type SkillsSettings
type Tool
```
For hook types, import from the hooks subpath:
```typescript
import type {
HookAPI,
HookMessage,
HookFactory,
HookEventContext,
HookCommandContext,
ToolCallEvent,
ToolResultEvent,
} from "@mariozechner/pi-coding-agent/hooks";
```
For message utilities:
```typescript
import { isHookMessage, createHookMessage } from "@mariozechner/pi-coding-agent";
```
For config utilities:
```typescript
import { getAgentDir } from "@mariozechner/pi-coding-agent/config";
```
For extension types, see [extensions.md](extensions.md) for the full API.

View file

@ -48,6 +48,56 @@ async execute(toolCallId, params, onUpdate, ctx, signal) {
}
```
## Overlays
Overlays render components on top of existing content without clearing the screen. Pass `{ overlay: true }` to `ctx.ui.custom()`:
```typescript
const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new MyDialog({ onClose: done }),
{ overlay: true }
);
```
For positioning and sizing, use `overlayOptions`:
```typescript
const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new SidePanel({ onClose: done }),
{
overlay: true,
overlayOptions: {
// Size: number or percentage string
width: "50%", // 50% of terminal width
minWidth: 40, // minimum 40 columns
maxHeight: "80%", // max 80% of terminal height
// Position: anchor-based (default: "center")
anchor: "right-center", // 9 positions: center, top-left, top-center, etc.
offsetX: -2, // offset from anchor
offsetY: 0,
// Or percentage/absolute positioning
row: "25%", // 25% from top
col: 10, // column 10
// Margins
margin: 2, // all sides, or { top, right, bottom, left }
// Responsive: hide on narrow terminals
visible: (termWidth, termHeight) => termWidth >= 80,
},
// Get handle for programmatic visibility control
onHandle: (handle) => {
// handle.setHidden(true/false) - toggle visibility
// handle.hide() - permanently remove
},
}
);
```
See [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) for comprehensive examples covering anchors, margins, stacking, responsive visibility, and animation.
## Built-in Components
Import from `@mariozechner/pi-tui`: