mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-15 19:05:14 +00:00
move pi-mono into companion-cloud as apps/companion-os
- Copy all pi-mono source into apps/companion-os/ - Update Dockerfile to COPY pre-built binary instead of downloading from GitHub Releases - Update deploy-staging.yml to build pi from source (bun compile) before Docker build - Add apps/companion-os/** to path triggers - No more cross-repo dispatch needed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
0250f72976
579 changed files with 206942 additions and 0 deletions
43
packages/coding-agent/docs/SOUL.md
Normal file
43
packages/coding-agent/docs/SOUL.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
title: "SOUL.md Template"
|
||||
summary: "Workspace template for SOUL.md"
|
||||
read_when:
|
||||
- Bootstrapping a workspace manually
|
||||
---
|
||||
|
||||
# SOUL.md - Who You Are
|
||||
|
||||
_You're not a chatbot. You're becoming someone._
|
||||
|
||||
## Core Truths
|
||||
|
||||
**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and "I'd be happy to help!" - just help. Actions speak louder than filler words.
|
||||
|
||||
**Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.
|
||||
|
||||
**Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Search for it. _Then_ ask if you're stuck. The goal is to come back with answers, not questions.
|
||||
|
||||
**Earn trust through competence.** Your human gave you access to their stuff. Don't make them regret it. Be careful with external actions (emails, tweets, anything public). Be bold with internal ones (reading, organizing, learning).
|
||||
|
||||
**Remember you're a guest.** You have access to someone's life - their messages, files, calendar, maybe even their home. That's intimacy. Treat it with respect.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Private things stay private. Period.
|
||||
- When in doubt, ask before acting externally.
|
||||
- Never send half-baked replies to messaging surfaces.
|
||||
- You're not the user's voice - be careful in group chats.
|
||||
|
||||
## Vibe
|
||||
|
||||
Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good.
|
||||
|
||||
## Continuity
|
||||
|
||||
Each session, you wake up fresh. These files _are_ your memory. Read them. Update them. They're how you persist.
|
||||
|
||||
If you change this file, tell the user - it's your soul, and they should know.
|
||||
|
||||
---
|
||||
|
||||
_This file is yours to evolve. As you learn who you are, update it._
|
||||
410
packages/coding-agent/docs/compaction.md
Normal file
410
packages/coding-agent/docs/compaction.md
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
# Compaction & Branch Summarization
|
||||
|
||||
LLMs have limited context windows. When conversations grow too long, pi uses compaction to summarize older content while preserving recent work. This page covers both auto-compaction and branch summarization.
|
||||
|
||||
**Source files** ([pi-mono](https://github.com/badlogic/pi-mono)):
|
||||
|
||||
- [`packages/coding-agent/src/core/compaction/compaction.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) - Auto-compaction logic
|
||||
- [`packages/coding-agent/src/core/compaction/branch-summarization.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) - Branch summarization
|
||||
- [`packages/coding-agent/src/core/compaction/utils.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/compaction/utils.ts) - Shared utilities (file tracking, serialization)
|
||||
- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts) - Entry types (`CompactionEntry`, `BranchSummaryEntry`)
|
||||
- [`packages/coding-agent/src/core/extensions/types.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/extensions/types.ts) - Extension event types
|
||||
|
||||
For TypeScript definitions in your project, inspect `node_modules/@mariozechner/pi-coding-agent/dist/`.
|
||||
|
||||
## Overview
|
||||
|
||||
Pi has two summarization mechanisms:
|
||||
|
||||
| Mechanism | Trigger | Purpose |
|
||||
| -------------------- | ---------------------------------------- | ----------------------------------------- |
|
||||
| Compaction | Context exceeds threshold, or `/compact` | Summarize old messages to free up context |
|
||||
| Branch summarization | `/tree` navigation | Preserve context when switching branches |
|
||||
|
||||
Both use the same structured summary format and track file operations cumulatively.
|
||||
|
||||
## Compaction
|
||||
|
||||
### When It Triggers
|
||||
|
||||
Auto-compaction triggers when:
|
||||
|
||||
```
|
||||
contextTokens > contextWindow - reserveTokens
|
||||
```
|
||||
|
||||
By default, `reserveTokens` is 16384 tokens (configurable in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`). This leaves room for the LLM's response.
|
||||
|
||||
You can also trigger manually with `/compact [instructions]`, where optional instructions focus the summary.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Find cut point**: Walk backwards from newest message, accumulating token estimates until `keepRecentTokens` (default 20k, configurable in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`) is reached
|
||||
2. **Extract messages**: Collect messages from previous compaction (or start) up to cut point
|
||||
3. **Generate summary**: Call LLM to summarize with structured format
|
||||
4. **Append entry**: Save `CompactionEntry` with summary and `firstKeptEntryId`
|
||||
5. **Reload**: Session reloads, using summary + messages from `firstKeptEntryId` onwards
|
||||
|
||||
```
|
||||
Before compaction:
|
||||
|
||||
entry: 0 1 2 3 4 5 6 7 8 9
|
||||
┌─────┬─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┐
|
||||
│ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│
|
||||
└─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┘
|
||||
└────────┬───────┘ └──────────────┬──────────────┘
|
||||
messagesToSummarize kept messages
|
||||
↑
|
||||
firstKeptEntryId (entry 4)
|
||||
|
||||
After compaction (new entry appended):
|
||||
|
||||
entry: 0 1 2 3 4 5 6 7 8 9 10
|
||||
┌─────┬─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬─────┐
|
||||
│ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│ cmp │
|
||||
└─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┴─────┘
|
||||
└──────────┬──────┘ └──────────────────────┬───────────────────┘
|
||||
not sent to LLM sent to LLM
|
||||
↑
|
||||
starts from firstKeptEntryId
|
||||
|
||||
What the LLM sees:
|
||||
|
||||
┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐
|
||||
│ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │
|
||||
└────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘
|
||||
↑ ↑ └─────────────────┬────────────────┘
|
||||
prompt from cmp messages from firstKeptEntryId
|
||||
```
|
||||
|
||||
### Split Turns
|
||||
|
||||
A "turn" starts with a user message and includes all assistant responses and tool calls until the next user message. Normally, compaction cuts at turn boundaries.
|
||||
|
||||
When a single turn exceeds `keepRecentTokens`, the cut point lands mid-turn at an assistant message. This is a "split turn":
|
||||
|
||||
```
|
||||
Split turn (one huge turn exceeds budget):
|
||||
|
||||
entry: 0 1 2 3 4 5 6 7 8
|
||||
┌─────┬─────┬─────┬──────┬─────┬──────┬──────┬─────┬──────┐
|
||||
│ hdr │ usr │ ass │ tool │ ass │ tool │ tool │ ass │ tool │
|
||||
└─────┴─────┴─────┴──────┴─────┴──────┴──────┴─────┴──────┘
|
||||
↑ ↑
|
||||
turnStartIndex = 1 firstKeptEntryId = 7
|
||||
│ │
|
||||
└──── turnPrefixMessages (1-6) ───────┘
|
||||
└── kept (7-8)
|
||||
|
||||
isSplitTurn = true
|
||||
messagesToSummarize = [] (no complete turns before)
|
||||
turnPrefixMessages = [usr, ass, tool, ass, tool, tool]
|
||||
```
|
||||
|
||||
For split turns, pi generates two summaries and merges them:
|
||||
|
||||
1. **History summary**: Previous context (if any)
|
||||
2. **Turn prefix summary**: The early part of the split turn
|
||||
|
||||
### Cut Point Rules
|
||||
|
||||
Valid cut points are:
|
||||
|
||||
- User messages
|
||||
- Assistant messages
|
||||
- BashExecution messages
|
||||
- Custom messages (custom_message, branch_summary)
|
||||
|
||||
Never cut at tool results (they must stay with their tool call).
|
||||
|
||||
### CompactionEntry Structure
|
||||
|
||||
Defined in [`session-manager.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts):
|
||||
|
||||
```typescript
|
||||
interface CompactionEntry<T = unknown> {
|
||||
type: "compaction";
|
||||
id: string;
|
||||
parentId: string;
|
||||
timestamp: number;
|
||||
summary: string;
|
||||
firstKeptEntryId: string;
|
||||
tokensBefore: number;
|
||||
fromHook?: boolean; // true if provided by extension (legacy field name)
|
||||
details?: T; // implementation-specific data
|
||||
}
|
||||
|
||||
// Default compaction uses this for details (from compaction.ts):
|
||||
interface CompactionDetails {
|
||||
readFiles: string[];
|
||||
modifiedFiles: string[];
|
||||
}
|
||||
```
|
||||
|
||||
Extensions can store any JSON-serializable data in `details`. The default compaction tracks file operations, but custom extension implementations can use their own structure.
|
||||
|
||||
See [`prepareCompaction()`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) and [`compact()`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) for the implementation.
|
||||
|
||||
## Branch Summarization
|
||||
|
||||
### When It Triggers
|
||||
|
||||
When you use `/tree` to navigate to a different branch, pi offers to summarize the work you're leaving. This injects context from the left branch into the new branch.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Find common ancestor**: Deepest node shared by old and new positions
|
||||
2. **Collect entries**: Walk from old leaf back to common ancestor
|
||||
3. **Prepare with budget**: Include messages up to token budget (newest first)
|
||||
4. **Generate summary**: Call LLM with structured format
|
||||
5. **Append entry**: Save `BranchSummaryEntry` at navigation point
|
||||
|
||||
```
|
||||
Tree before navigation:
|
||||
|
||||
┌─ B ─ C ─ D (old leaf, being abandoned)
|
||||
A ───┤
|
||||
└─ E ─ F (target)
|
||||
|
||||
Common ancestor: A
|
||||
Entries to summarize: B, C, D
|
||||
|
||||
After navigation with summary:
|
||||
|
||||
┌─ B ─ C ─ D ─ [summary of B,C,D]
|
||||
A ───┤
|
||||
└─ E ─ F (new leaf)
|
||||
```
|
||||
|
||||
### Cumulative File Tracking
|
||||
|
||||
Both compaction and branch summarization track files cumulatively. When generating a summary, pi extracts file operations from:
|
||||
|
||||
- Tool calls in the messages being summarized
|
||||
- Previous compaction or branch summary `details` (if any)
|
||||
|
||||
This means file tracking accumulates across multiple compactions or nested branch summaries, preserving the full history of read and modified files.
|
||||
|
||||
### BranchSummaryEntry Structure
|
||||
|
||||
Defined in [`session-manager.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts):
|
||||
|
||||
```typescript
|
||||
interface BranchSummaryEntry<T = unknown> {
|
||||
type: "branch_summary";
|
||||
id: string;
|
||||
parentId: string;
|
||||
timestamp: number;
|
||||
summary: string;
|
||||
fromId: string; // Entry we navigated from
|
||||
fromHook?: boolean; // true if provided by extension (legacy field name)
|
||||
details?: T; // implementation-specific data
|
||||
}
|
||||
|
||||
// Default branch summarization uses this for details (from branch-summarization.ts):
|
||||
interface BranchSummaryDetails {
|
||||
readFiles: string[];
|
||||
modifiedFiles: string[];
|
||||
}
|
||||
```
|
||||
|
||||
Same as compaction, extensions can store custom data in `details`.
|
||||
|
||||
See [`collectEntriesForBranchSummary()`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts), [`prepareBranchEntries()`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts), and [`generateBranchSummary()`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) for the implementation.
|
||||
|
||||
## Summary Format
|
||||
|
||||
Both compaction and branch summarization use the same structured format:
|
||||
|
||||
```markdown
|
||||
## Goal
|
||||
|
||||
[What the user is trying to accomplish]
|
||||
|
||||
## Constraints & Preferences
|
||||
|
||||
- [Requirements mentioned by user]
|
||||
|
||||
## Progress
|
||||
|
||||
### Done
|
||||
|
||||
- [x] [Completed tasks]
|
||||
|
||||
### In Progress
|
||||
|
||||
- [ ] [Current work]
|
||||
|
||||
### Blocked
|
||||
|
||||
- [Issues, if any]
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- **[Decision]**: [Rationale]
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. [What should happen next]
|
||||
|
||||
## Critical Context
|
||||
|
||||
- [Data needed to continue]
|
||||
|
||||
<read-files>
|
||||
path/to/file1.ts
|
||||
path/to/file2.ts
|
||||
</read-files>
|
||||
|
||||
<modified-files>
|
||||
path/to/changed.ts
|
||||
</modified-files>
|
||||
```
|
||||
|
||||
### Message Serialization
|
||||
|
||||
Before summarization, messages are serialized to text via [`serializeConversation()`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/compaction/utils.ts):
|
||||
|
||||
```
|
||||
[User]: What they said
|
||||
[Assistant thinking]: Internal reasoning
|
||||
[Assistant]: Response text
|
||||
[Assistant tool calls]: read(path="foo.ts"); edit(path="bar.ts", ...)
|
||||
[Tool result]: Output from tool
|
||||
```
|
||||
|
||||
This prevents the model from treating it as a conversation to continue.
|
||||
|
||||
## Custom Summarization via Extensions
|
||||
|
||||
Extensions can intercept and customize both compaction and branch summarization. See [`extensions/types.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/extensions/types.ts) for event type definitions.
|
||||
|
||||
### session_before_compact
|
||||
|
||||
Fired before auto-compaction or `/compact`. Can cancel or provide custom summary. See `SessionBeforeCompactEvent` and `CompactionPreparation` in the types file.
|
||||
|
||||
```typescript
|
||||
pi.on("session_before_compact", async (event, ctx) => {
|
||||
const { preparation, branchEntries, customInstructions, signal } = event;
|
||||
|
||||
// preparation.messagesToSummarize - messages to summarize
|
||||
// preparation.turnPrefixMessages - split turn prefix (if isSplitTurn)
|
||||
// preparation.previousSummary - previous compaction summary
|
||||
// preparation.fileOps - extracted file operations
|
||||
// preparation.tokensBefore - context tokens before compaction
|
||||
// preparation.firstKeptEntryId - where kept messages start
|
||||
// preparation.settings - compaction settings
|
||||
|
||||
// branchEntries - all entries on current branch (for custom state)
|
||||
// signal - AbortSignal (pass to LLM calls)
|
||||
|
||||
// Cancel:
|
||||
return { cancel: true };
|
||||
|
||||
// Custom summary:
|
||||
return {
|
||||
compaction: {
|
||||
summary: "Your summary...",
|
||||
firstKeptEntryId: preparation.firstKeptEntryId,
|
||||
tokensBefore: preparation.tokensBefore,
|
||||
details: {
|
||||
/* custom data */
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
#### Converting Messages to Text
|
||||
|
||||
To generate a summary with your own model, convert messages to text using `serializeConversation`:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
convertToLlm,
|
||||
serializeConversation,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
|
||||
pi.on("session_before_compact", async (event, ctx) => {
|
||||
const { preparation } = event;
|
||||
|
||||
// Convert AgentMessage[] to Message[], then serialize to text
|
||||
const conversationText = serializeConversation(
|
||||
convertToLlm(preparation.messagesToSummarize),
|
||||
);
|
||||
// Returns:
|
||||
// [User]: message text
|
||||
// [Assistant thinking]: thinking content
|
||||
// [Assistant]: response text
|
||||
// [Assistant tool calls]: read(path="..."); bash(command="...")
|
||||
// [Tool result]: output text
|
||||
|
||||
// Now send to your model for summarization
|
||||
const summary = await myModel.summarize(conversationText);
|
||||
|
||||
return {
|
||||
compaction: {
|
||||
summary,
|
||||
firstKeptEntryId: preparation.firstKeptEntryId,
|
||||
tokensBefore: preparation.tokensBefore,
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
See [custom-compaction.ts](../examples/extensions/custom-compaction.ts) for a complete example using a different model.
|
||||
|
||||
### session_before_tree
|
||||
|
||||
Fired before `/tree` navigation. Always fires regardless of whether user chose to summarize. Can cancel navigation or provide custom summary.
|
||||
|
||||
```typescript
|
||||
pi.on("session_before_tree", async (event, ctx) => {
|
||||
const { preparation, signal } = event;
|
||||
|
||||
// preparation.targetId - where we're navigating to
|
||||
// preparation.oldLeafId - current position (being abandoned)
|
||||
// preparation.commonAncestorId - shared ancestor
|
||||
// preparation.entriesToSummarize - entries that would be summarized
|
||||
// preparation.userWantsSummary - whether user chose to summarize
|
||||
|
||||
// Cancel navigation entirely:
|
||||
return { cancel: true };
|
||||
|
||||
// Provide custom summary (only used if userWantsSummary is true):
|
||||
if (preparation.userWantsSummary) {
|
||||
return {
|
||||
summary: {
|
||||
summary: "Your summary...",
|
||||
details: {
|
||||
/* custom data */
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
See `SessionBeforeTreeEvent` and `TreePreparation` in the types file.
|
||||
|
||||
## Settings
|
||||
|
||||
Configure compaction in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"compaction": {
|
||||
"enabled": true,
|
||||
"reserveTokens": 16384,
|
||||
"keepRecentTokens": 20000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Setting | Default | Description |
|
||||
| ------------------ | ------- | -------------------------------------- |
|
||||
| `enabled` | `true` | Enable auto-compaction |
|
||||
| `reserveTokens` | `16384` | Tokens to reserve for LLM response |
|
||||
| `keepRecentTokens` | `20000` | Recent tokens to keep (not summarized) |
|
||||
|
||||
Disable auto-compaction with `"enabled": false`. You can still compact manually with `/compact`.
|
||||
614
packages/coding-agent/docs/custom-provider.md
Normal file
614
packages/coding-agent/docs/custom-provider.md
Normal file
|
|
@ -0,0 +1,614 @@
|
|||
# Custom Providers
|
||||
|
||||
Extensions can register custom model providers via `pi.registerProvider()`. This enables:
|
||||
|
||||
- **Proxies** - Route requests through corporate proxies or API gateways
|
||||
- **Custom endpoints** - Use self-hosted or private model deployments
|
||||
- **OAuth/SSO** - Add authentication flows for enterprise providers
|
||||
- **Custom APIs** - Implement streaming for non-standard LLM APIs
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Reference](#quick-reference)
|
||||
- [Override Existing Provider](#override-existing-provider)
|
||||
- [Register New Provider](#register-new-provider)
|
||||
- [Unregister Provider](#unregister-provider)
|
||||
- [OAuth Support](#oauth-support)
|
||||
- [Custom Streaming API](#custom-streaming-api)
|
||||
- [Testing Your Implementation](#testing-your-implementation)
|
||||
- [Config Reference](#config-reference)
|
||||
- [Model Definition Reference](#model-definition-reference)
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```typescript
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
// Override baseUrl for existing provider
|
||||
pi.registerProvider("anthropic", {
|
||||
baseUrl: "https://proxy.example.com",
|
||||
});
|
||||
|
||||
// Register new provider with models
|
||||
pi.registerProvider("my-provider", {
|
||||
baseUrl: "https://api.example.com",
|
||||
apiKey: "MY_API_KEY",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "my-model",
|
||||
name: "My Model",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Override Existing Provider
|
||||
|
||||
The simplest use case: redirect an existing provider through a proxy.
|
||||
|
||||
```typescript
|
||||
// All Anthropic requests now go through your proxy
|
||||
pi.registerProvider("anthropic", {
|
||||
baseUrl: "https://proxy.example.com",
|
||||
});
|
||||
|
||||
// Add custom headers to OpenAI requests
|
||||
pi.registerProvider("openai", {
|
||||
headers: {
|
||||
"X-Custom-Header": "value",
|
||||
},
|
||||
});
|
||||
|
||||
// Both baseUrl and headers
|
||||
pi.registerProvider("google", {
|
||||
baseUrl: "https://ai-gateway.corp.com/google",
|
||||
headers: {
|
||||
"X-Corp-Auth": "CORP_AUTH_TOKEN", // env var or literal
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
When only `baseUrl` and/or `headers` are provided (no `models`), all existing models for that provider are preserved with the new endpoint.
|
||||
|
||||
## Register New Provider
|
||||
|
||||
To add a completely new provider, specify `models` along with the required configuration.
|
||||
|
||||
```typescript
|
||||
pi.registerProvider("my-llm", {
|
||||
baseUrl: "https://api.my-llm.com/v1",
|
||||
apiKey: "MY_LLM_API_KEY", // env var name or literal value
|
||||
api: "openai-completions", // which streaming API to use
|
||||
models: [
|
||||
{
|
||||
id: "my-llm-large",
|
||||
name: "My LLM Large",
|
||||
reasoning: true, // supports extended thinking
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 3.0, // $/million tokens
|
||||
output: 15.0,
|
||||
cacheRead: 0.3,
|
||||
cacheWrite: 3.75,
|
||||
},
|
||||
contextWindow: 200000,
|
||||
maxTokens: 16384,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
When `models` is provided, it **replaces** all existing models for that provider.
|
||||
|
||||
## Unregister Provider
|
||||
|
||||
Use `pi.unregisterProvider(name)` to remove a provider that was previously registered via `pi.registerProvider(name, ...)`:
|
||||
|
||||
```typescript
|
||||
// Register
|
||||
pi.registerProvider("my-llm", {
|
||||
baseUrl: "https://api.my-llm.com/v1",
|
||||
apiKey: "MY_LLM_API_KEY",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "my-llm-large",
|
||||
name: "My LLM Large",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 16384,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Later, remove it
|
||||
pi.unregisterProvider("my-llm");
|
||||
```
|
||||
|
||||
Unregistering removes that provider's dynamic models, API key fallback, OAuth provider registration, and custom stream handler registrations. Any built-in models or provider behavior that were overridden are restored.
|
||||
|
||||
Calls made after the initial extension load phase are applied immediately, so no `/reload` is required.
|
||||
|
||||
### API Types
|
||||
|
||||
The `api` field determines which streaming implementation is used:
|
||||
|
||||
| API | Use for |
|
||||
| ------------------------- | ------------------------------------------- |
|
||||
| `anthropic-messages` | Anthropic Claude API and compatibles |
|
||||
| `openai-completions` | OpenAI Chat Completions API and compatibles |
|
||||
| `openai-responses` | OpenAI Responses API |
|
||||
| `azure-openai-responses` | Azure OpenAI Responses API |
|
||||
| `openai-codex-responses` | OpenAI Codex Responses API |
|
||||
| `mistral-conversations` | Mistral SDK Conversations/Chat streaming |
|
||||
| `google-generative-ai` | Google Generative AI API |
|
||||
| `google-gemini-cli` | Google Cloud Code Assist API |
|
||||
| `google-vertex` | Google Vertex AI API |
|
||||
| `bedrock-converse-stream` | Amazon Bedrock Converse API |
|
||||
|
||||
Most OpenAI-compatible providers work with `openai-completions`. Use `compat` for quirks:
|
||||
|
||||
```typescript
|
||||
models: [
|
||||
{
|
||||
id: "custom-model",
|
||||
// ...
|
||||
compat: {
|
||||
supportsDeveloperRole: false, // use "system" instead of "developer"
|
||||
supportsReasoningEffort: true,
|
||||
reasoningEffortMap: {
|
||||
// map pi-ai levels to provider values
|
||||
minimal: "default",
|
||||
low: "default",
|
||||
medium: "default",
|
||||
high: "default",
|
||||
xhigh: "default",
|
||||
},
|
||||
maxTokensField: "max_tokens", // instead of "max_completion_tokens"
|
||||
requiresToolResultName: true, // tool results need name field
|
||||
thinkingFormat: "qwen", // uses enable_thinking: true
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
> Migration note: Mistral moved from `openai-completions` to `mistral-conversations`.
|
||||
> Use `mistral-conversations` for native Mistral models.
|
||||
> If you intentionally route Mistral-compatible/custom endpoints through `openai-completions`, set `compat` flags explicitly as needed.
|
||||
|
||||
### Auth Header
|
||||
|
||||
If your provider expects `Authorization: Bearer <key>` but doesn't use a standard API, set `authHeader: true`:
|
||||
|
||||
```typescript
|
||||
pi.registerProvider("custom-api", {
|
||||
baseUrl: "https://api.example.com",
|
||||
apiKey: "MY_API_KEY",
|
||||
authHeader: true, // adds Authorization: Bearer header
|
||||
api: "openai-completions",
|
||||
models: [...]
|
||||
});
|
||||
```
|
||||
|
||||
## OAuth Support
|
||||
|
||||
Add OAuth/SSO authentication that integrates with `/login`:
|
||||
|
||||
```typescript
|
||||
import type { OAuthCredentials, OAuthLoginCallbacks } from "@mariozechner/pi-ai";
|
||||
|
||||
pi.registerProvider("corporate-ai", {
|
||||
baseUrl: "https://ai.corp.com/v1",
|
||||
api: "openai-responses",
|
||||
models: [...],
|
||||
oauth: {
|
||||
name: "Corporate AI (SSO)",
|
||||
|
||||
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
||||
// Option 1: Browser-based OAuth
|
||||
callbacks.onAuth({ url: "https://sso.corp.com/authorize?..." });
|
||||
|
||||
// Option 2: Device code flow
|
||||
callbacks.onDeviceCode({
|
||||
userCode: "ABCD-1234",
|
||||
verificationUri: "https://sso.corp.com/device"
|
||||
});
|
||||
|
||||
// Option 3: Prompt for token/code
|
||||
const code = await callbacks.onPrompt({ message: "Enter SSO code:" });
|
||||
|
||||
// Exchange for tokens (your implementation)
|
||||
const tokens = await exchangeCodeForTokens(code);
|
||||
|
||||
return {
|
||||
refresh: tokens.refreshToken,
|
||||
access: tokens.accessToken,
|
||||
expires: Date.now() + tokens.expiresIn * 1000
|
||||
};
|
||||
},
|
||||
|
||||
async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
||||
const tokens = await refreshAccessToken(credentials.refresh);
|
||||
return {
|
||||
refresh: tokens.refreshToken ?? credentials.refresh,
|
||||
access: tokens.accessToken,
|
||||
expires: Date.now() + tokens.expiresIn * 1000
|
||||
};
|
||||
},
|
||||
|
||||
getApiKey(credentials: OAuthCredentials): string {
|
||||
return credentials.access;
|
||||
},
|
||||
|
||||
// Optional: modify models based on user's subscription
|
||||
modifyModels(models, credentials) {
|
||||
const region = decodeRegionFromToken(credentials.access);
|
||||
return models.map(m => ({
|
||||
...m,
|
||||
baseUrl: `https://${region}.ai.corp.com/v1`
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
After registration, users can authenticate via `/login corporate-ai`.
|
||||
|
||||
### OAuthLoginCallbacks
|
||||
|
||||
The `callbacks` object provides three ways to authenticate:
|
||||
|
||||
```typescript
|
||||
interface OAuthLoginCallbacks {
|
||||
// Open URL in browser (for OAuth redirects)
|
||||
onAuth(params: { url: string }): void;
|
||||
|
||||
// Show device code (for device authorization flow)
|
||||
onDeviceCode(params: { userCode: string; verificationUri: string }): void;
|
||||
|
||||
// Prompt user for input (for manual token entry)
|
||||
onPrompt(params: { message: string }): Promise<string>;
|
||||
}
|
||||
```
|
||||
|
||||
### OAuthCredentials
|
||||
|
||||
Credentials are persisted in `~/.pi/agent/auth.json`:
|
||||
|
||||
```typescript
|
||||
interface OAuthCredentials {
|
||||
refresh: string; // Refresh token (for refreshToken())
|
||||
access: string; // Access token (returned by getApiKey())
|
||||
expires: number; // Expiration timestamp in milliseconds
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Streaming API
|
||||
|
||||
For providers with non-standard APIs, implement `streamSimple`. Study the existing provider implementations before writing your own:
|
||||
|
||||
**Reference implementations:**
|
||||
|
||||
- [anthropic.ts](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts) - Anthropic Messages API
|
||||
- [mistral.ts](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/mistral.ts) - Mistral Conversations API
|
||||
- [openai-completions.ts](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/openai-completions.ts) - OpenAI Chat Completions
|
||||
- [openai-responses.ts](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/openai-responses.ts) - OpenAI Responses API
|
||||
- [google.ts](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/google.ts) - Google Generative AI
|
||||
- [amazon-bedrock.ts](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/amazon-bedrock.ts) - AWS Bedrock
|
||||
|
||||
### Stream Pattern
|
||||
|
||||
All providers follow the same pattern:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
type AssistantMessage,
|
||||
type AssistantMessageEventStream,
|
||||
type Context,
|
||||
type Model,
|
||||
type SimpleStreamOptions,
|
||||
calculateCost,
|
||||
createAssistantMessageEventStream,
|
||||
} from "@mariozechner/pi-ai";
|
||||
|
||||
function streamMyProvider(
|
||||
model: Model<any>,
|
||||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
): AssistantMessageEventStream {
|
||||
const stream = createAssistantMessageEventStream();
|
||||
|
||||
(async () => {
|
||||
// Initialize output message
|
||||
const output: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [],
|
||||
api: model.api,
|
||||
provider: model.provider,
|
||||
model: model.id,
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "stop",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
try {
|
||||
// Push start event
|
||||
stream.push({ type: "start", partial: output });
|
||||
|
||||
// Make API request and process response...
|
||||
// Push content events as they arrive...
|
||||
|
||||
// Push done event
|
||||
stream.push({
|
||||
type: "done",
|
||||
reason: output.stopReason as "stop" | "length" | "toolUse",
|
||||
message: output,
|
||||
});
|
||||
stream.end();
|
||||
} catch (error) {
|
||||
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
||||
output.errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
stream.push({ type: "error", reason: output.stopReason, error: output });
|
||||
stream.end();
|
||||
}
|
||||
})();
|
||||
|
||||
return stream;
|
||||
}
|
||||
```
|
||||
|
||||
### Event Types
|
||||
|
||||
Push events via `stream.push()` in this order:
|
||||
|
||||
1. `{ type: "start", partial: output }` - Stream started
|
||||
|
||||
2. Content events (repeatable, track `contentIndex` for each block):
|
||||
- `{ type: "text_start", contentIndex, partial }` - Text block started
|
||||
- `{ type: "text_delta", contentIndex, delta, partial }` - Text chunk
|
||||
- `{ type: "text_end", contentIndex, content, partial }` - Text block ended
|
||||
- `{ type: "thinking_start", contentIndex, partial }` - Thinking started
|
||||
- `{ type: "thinking_delta", contentIndex, delta, partial }` - Thinking chunk
|
||||
- `{ type: "thinking_end", contentIndex, content, partial }` - Thinking ended
|
||||
- `{ type: "toolcall_start", contentIndex, partial }` - Tool call started
|
||||
- `{ type: "toolcall_delta", contentIndex, delta, partial }` - Tool call JSON chunk
|
||||
- `{ type: "toolcall_end", contentIndex, toolCall, partial }` - Tool call ended
|
||||
|
||||
3. `{ type: "done", reason, message }` or `{ type: "error", reason, error }` - Stream ended
|
||||
|
||||
The `partial` field in each event contains the current `AssistantMessage` state. Update `output.content` as you receive data, then include `output` as the `partial`.
|
||||
|
||||
### Content Blocks
|
||||
|
||||
Add content blocks to `output.content` as they arrive:
|
||||
|
||||
```typescript
|
||||
// Text block
|
||||
output.content.push({ type: "text", text: "" });
|
||||
stream.push({
|
||||
type: "text_start",
|
||||
contentIndex: output.content.length - 1,
|
||||
partial: output,
|
||||
});
|
||||
|
||||
// As text arrives
|
||||
const block = output.content[contentIndex];
|
||||
if (block.type === "text") {
|
||||
block.text += delta;
|
||||
stream.push({ type: "text_delta", contentIndex, delta, partial: output });
|
||||
}
|
||||
|
||||
// When block completes
|
||||
stream.push({
|
||||
type: "text_end",
|
||||
contentIndex,
|
||||
content: block.text,
|
||||
partial: output,
|
||||
});
|
||||
```
|
||||
|
||||
### Tool Calls
|
||||
|
||||
Tool calls require accumulating JSON and parsing:
|
||||
|
||||
```typescript
|
||||
// Start tool call
|
||||
output.content.push({
|
||||
type: "toolCall",
|
||||
id: toolCallId,
|
||||
name: toolName,
|
||||
arguments: {},
|
||||
});
|
||||
stream.push({
|
||||
type: "toolcall_start",
|
||||
contentIndex: output.content.length - 1,
|
||||
partial: output,
|
||||
});
|
||||
|
||||
// Accumulate JSON
|
||||
let partialJson = "";
|
||||
partialJson += jsonDelta;
|
||||
try {
|
||||
block.arguments = JSON.parse(partialJson);
|
||||
} catch {}
|
||||
stream.push({
|
||||
type: "toolcall_delta",
|
||||
contentIndex,
|
||||
delta: jsonDelta,
|
||||
partial: output,
|
||||
});
|
||||
|
||||
// Complete
|
||||
stream.push({
|
||||
type: "toolcall_end",
|
||||
contentIndex,
|
||||
toolCall: { type: "toolCall", id, name, arguments: block.arguments },
|
||||
partial: output,
|
||||
});
|
||||
```
|
||||
|
||||
### Usage and Cost
|
||||
|
||||
Update usage from API response and calculate cost:
|
||||
|
||||
```typescript
|
||||
output.usage.input = response.usage.input_tokens;
|
||||
output.usage.output = response.usage.output_tokens;
|
||||
output.usage.cacheRead = response.usage.cache_read_tokens ?? 0;
|
||||
output.usage.cacheWrite = response.usage.cache_write_tokens ?? 0;
|
||||
output.usage.totalTokens =
|
||||
output.usage.input +
|
||||
output.usage.output +
|
||||
output.usage.cacheRead +
|
||||
output.usage.cacheWrite;
|
||||
calculateCost(model, output.usage);
|
||||
```
|
||||
|
||||
### Registration
|
||||
|
||||
Register your stream function:
|
||||
|
||||
```typescript
|
||||
pi.registerProvider("my-provider", {
|
||||
baseUrl: "https://api.example.com",
|
||||
apiKey: "MY_API_KEY",
|
||||
api: "my-custom-api",
|
||||
models: [...],
|
||||
streamSimple: streamMyProvider
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Your Implementation
|
||||
|
||||
Test your provider against the same test suites used by built-in providers. Copy and adapt these test files from [packages/ai/test/](https://github.com/badlogic/pi-mono/tree/main/packages/ai/test):
|
||||
|
||||
| Test | Purpose |
|
||||
| ---------------------------------- | --------------------------------- |
|
||||
| `stream.test.ts` | Basic streaming, text output |
|
||||
| `tokens.test.ts` | Token counting and usage |
|
||||
| `abort.test.ts` | AbortSignal handling |
|
||||
| `empty.test.ts` | Empty/minimal responses |
|
||||
| `context-overflow.test.ts` | Context window limits |
|
||||
| `image-limits.test.ts` | Image input handling |
|
||||
| `unicode-surrogate.test.ts` | Unicode edge cases |
|
||||
| `tool-call-without-result.test.ts` | Tool call edge cases |
|
||||
| `image-tool-result.test.ts` | Images in tool results |
|
||||
| `total-tokens.test.ts` | Total token calculation |
|
||||
| `cross-provider-handoff.test.ts` | Context handoff between providers |
|
||||
|
||||
Run tests with your provider/model pairs to verify compatibility.
|
||||
|
||||
## Config Reference
|
||||
|
||||
```typescript
|
||||
interface ProviderConfig {
|
||||
/** API endpoint URL. Required when defining models. */
|
||||
baseUrl?: string;
|
||||
|
||||
/** API key or environment variable name. Required when defining models (unless oauth). */
|
||||
apiKey?: string;
|
||||
|
||||
/** API type for streaming. Required at provider or model level when defining models. */
|
||||
api?: Api;
|
||||
|
||||
/** Custom streaming implementation for non-standard APIs. */
|
||||
streamSimple?: (
|
||||
model: Model<Api>,
|
||||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
) => AssistantMessageEventStream;
|
||||
|
||||
/** Custom headers to include in requests. Values can be env var names. */
|
||||
headers?: Record<string, string>;
|
||||
|
||||
/** If true, adds Authorization: Bearer header with the resolved API key. */
|
||||
authHeader?: boolean;
|
||||
|
||||
/** Models to register. If provided, replaces all existing models for this provider. */
|
||||
models?: ProviderModelConfig[];
|
||||
|
||||
/** OAuth provider for /login support. */
|
||||
oauth?: {
|
||||
name: string;
|
||||
login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials>;
|
||||
refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials>;
|
||||
getApiKey(credentials: OAuthCredentials): string;
|
||||
modifyModels?(
|
||||
models: Model<Api>[],
|
||||
credentials: OAuthCredentials,
|
||||
): Model<Api>[];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Model Definition Reference
|
||||
|
||||
```typescript
|
||||
interface ProviderModelConfig {
|
||||
/** Model ID (e.g., "claude-sonnet-4-20250514"). */
|
||||
id: string;
|
||||
|
||||
/** Display name (e.g., "Claude 4 Sonnet"). */
|
||||
name: string;
|
||||
|
||||
/** API type override for this specific model. */
|
||||
api?: Api;
|
||||
|
||||
/** Whether the model supports extended thinking. */
|
||||
reasoning: boolean;
|
||||
|
||||
/** Supported input types. */
|
||||
input: ("text" | "image")[];
|
||||
|
||||
/** Cost per million tokens (for usage tracking). */
|
||||
cost: {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
};
|
||||
|
||||
/** Maximum context window size in tokens. */
|
||||
contextWindow: number;
|
||||
|
||||
/** Maximum output tokens. */
|
||||
maxTokens: number;
|
||||
|
||||
/** Custom headers for this specific model. */
|
||||
headers?: Record<string, string>;
|
||||
|
||||
/** OpenAI compatibility settings for openai-completions API. */
|
||||
compat?: {
|
||||
supportsStore?: boolean;
|
||||
supportsDeveloperRole?: boolean;
|
||||
supportsReasoningEffort?: boolean;
|
||||
reasoningEffortMap?: Partial<
|
||||
Record<"minimal" | "low" | "medium" | "high" | "xhigh", string>
|
||||
>;
|
||||
supportsUsageInStreaming?: boolean;
|
||||
maxTokensField?: "max_completion_tokens" | "max_tokens";
|
||||
requiresToolResultName?: boolean;
|
||||
requiresAssistantAfterToolResult?: boolean;
|
||||
requiresThinkingAsText?: boolean;
|
||||
thinkingFormat?: "openai" | "zai" | "qwen";
|
||||
};
|
||||
}
|
||||
```
|
||||
70
packages/coding-agent/docs/development.md
Normal file
70
packages/coding-agent/docs/development.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Development
|
||||
|
||||
See [AGENTS.md](../../../AGENTS.md) for additional guidelines.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
git clone https://github.com/badlogic/pi-mono
|
||||
cd pi-mono
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Run from source:
|
||||
|
||||
```bash
|
||||
./pi-test.sh
|
||||
```
|
||||
|
||||
## Forking / Rebranding
|
||||
|
||||
Configure via `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"piConfig": {
|
||||
"name": "pi",
|
||||
"configDir": ".pi"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Change `name`, `configDir`, and `bin` field for your fork. Affects CLI banner, config paths, and environment variable names.
|
||||
|
||||
## Path Resolution
|
||||
|
||||
Three execution modes: npm install, standalone binary, tsx from source.
|
||||
|
||||
**Always use `src/config.ts`** for package assets:
|
||||
|
||||
```typescript
|
||||
import { getPackageDir, getThemeDir } from "./config.js";
|
||||
```
|
||||
|
||||
Never use `__dirname` directly for package assets.
|
||||
|
||||
## Debug Command
|
||||
|
||||
`/debug` (hidden) writes to `~/.pi/agent/pi-debug.log`:
|
||||
|
||||
- Rendered TUI lines with ANSI codes
|
||||
- Last messages sent to the LLM
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
./test.sh # Run non-LLM tests (no API keys needed)
|
||||
npm test # Run all tests
|
||||
npm test -- test/specific.test.ts # Run specific test
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
packages/
|
||||
ai/ # LLM provider abstraction
|
||||
agent/ # Agent loop and message types
|
||||
tui/ # Terminal UI components
|
||||
coding-agent/ # CLI and interactive mode
|
||||
```
|
||||
2020
packages/coding-agent/docs/extensions.md
Normal file
2020
packages/coding-agent/docs/extensions.md
Normal file
File diff suppressed because it is too large
Load diff
BIN
packages/coding-agent/docs/images/doom-extension.png
Normal file
BIN
packages/coding-agent/docs/images/doom-extension.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
BIN
packages/coding-agent/docs/images/exy.png
Normal file
BIN
packages/coding-agent/docs/images/exy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
BIN
packages/coding-agent/docs/images/interactive-mode.png
Normal file
BIN
packages/coding-agent/docs/images/interactive-mode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 321 KiB |
BIN
packages/coding-agent/docs/images/tree-view.png
Normal file
BIN
packages/coding-agent/docs/images/tree-view.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 275 KiB |
129
packages/coding-agent/docs/json.md
Normal file
129
packages/coding-agent/docs/json.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# JSON Event Stream Mode
|
||||
|
||||
```bash
|
||||
pi --mode json "Your prompt"
|
||||
```
|
||||
|
||||
Outputs all session events as JSON lines to stdout. Useful for integrating pi into other tools or custom UIs.
|
||||
|
||||
## Event Types
|
||||
|
||||
Events are defined in [`AgentSessionEvent`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/agent-session.ts#L102):
|
||||
|
||||
```typescript
|
||||
type AgentSessionEvent =
|
||||
| AgentEvent
|
||||
| { type: "auto_compaction_start"; reason: "threshold" | "overflow" }
|
||||
| {
|
||||
type: "auto_compaction_end";
|
||||
result: CompactionResult | undefined;
|
||||
aborted: boolean;
|
||||
willRetry: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
| {
|
||||
type: "auto_retry_start";
|
||||
attempt: number;
|
||||
maxAttempts: number;
|
||||
delayMs: number;
|
||||
errorMessage: string;
|
||||
}
|
||||
| {
|
||||
type: "auto_retry_end";
|
||||
success: boolean;
|
||||
attempt: number;
|
||||
finalError?: string;
|
||||
};
|
||||
```
|
||||
|
||||
Base events from [`AgentEvent`](https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/types.ts#L179):
|
||||
|
||||
```typescript
|
||||
type AgentEvent =
|
||||
// Agent lifecycle
|
||||
| { type: "agent_start" }
|
||||
| { type: "agent_end"; messages: AgentMessage[] }
|
||||
// Turn lifecycle
|
||||
| { type: "turn_start" }
|
||||
| {
|
||||
type: "turn_end";
|
||||
message: AgentMessage;
|
||||
toolResults: ToolResultMessage[];
|
||||
}
|
||||
// Message lifecycle
|
||||
| { type: "message_start"; message: AgentMessage }
|
||||
| {
|
||||
type: "message_update";
|
||||
message: AgentMessage;
|
||||
assistantMessageEvent: AssistantMessageEvent;
|
||||
}
|
||||
| { type: "message_end"; message: AgentMessage }
|
||||
// Tool execution
|
||||
| {
|
||||
type: "tool_execution_start";
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: any;
|
||||
}
|
||||
| {
|
||||
type: "tool_execution_update";
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: any;
|
||||
partialResult: any;
|
||||
}
|
||||
| {
|
||||
type: "tool_execution_end";
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
result: any;
|
||||
isError: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
## Message Types
|
||||
|
||||
Base messages from [`packages/ai/src/types.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/types.ts#L134):
|
||||
|
||||
- `UserMessage` (line 134)
|
||||
- `AssistantMessage` (line 140)
|
||||
- `ToolResultMessage` (line 152)
|
||||
|
||||
Extended messages from [`packages/coding-agent/src/core/messages.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/messages.ts#L29):
|
||||
|
||||
- `BashExecutionMessage` (line 29)
|
||||
- `CustomMessage` (line 46)
|
||||
- `BranchSummaryMessage` (line 55)
|
||||
- `CompactionSummaryMessage` (line 62)
|
||||
|
||||
## Output Format
|
||||
|
||||
Each line is a JSON object. The first line is the session header:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "session",
|
||||
"version": 3,
|
||||
"id": "uuid",
|
||||
"timestamp": "...",
|
||||
"cwd": "/path"
|
||||
}
|
||||
```
|
||||
|
||||
Followed by events as they occur:
|
||||
|
||||
```json
|
||||
{"type":"agent_start"}
|
||||
{"type":"turn_start"}
|
||||
{"type":"message_start","message":{"role":"assistant","content":[],...}}
|
||||
{"type":"message_update","message":{...},"assistantMessageEvent":{"type":"text_delta","delta":"Hello",...}}
|
||||
{"type":"message_end","message":{...}}
|
||||
{"type":"turn_end","message":{...},"toolResults":[]}
|
||||
{"type":"agent_end","messages":[...]}
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```bash
|
||||
pi --mode json "List files" 2>/dev/null | jq -c 'select(.type == "message_end")'
|
||||
```
|
||||
174
packages/coding-agent/docs/keybindings.md
Normal file
174
packages/coding-agent/docs/keybindings.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# Keybindings
|
||||
|
||||
All keyboard shortcuts can be customized via `~/.pi/agent/keybindings.json`. Each action can be bound to one or more keys.
|
||||
|
||||
## Key Format
|
||||
|
||||
`modifier+key` where modifiers are `ctrl`, `shift`, `alt` (combinable) and keys are:
|
||||
|
||||
- **Letters:** `a-z`
|
||||
- **Special:** `escape`, `esc`, `enter`, `return`, `tab`, `space`, `backspace`, `delete`, `insert`, `clear`, `home`, `end`, `pageUp`, `pageDown`, `up`, `down`, `left`, `right`
|
||||
- **Function:** `f1`-`f12`
|
||||
- **Symbols:** `` ` ``, `-`, `=`, `[`, `]`, `\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?`
|
||||
|
||||
Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, etc.
|
||||
|
||||
## All Actions
|
||||
|
||||
### Cursor Movement
|
||||
|
||||
| Action | Default | Description |
|
||||
| ----------------- | ---------------------------------- | -------------------------- |
|
||||
| `cursorUp` | `up` | Move cursor up |
|
||||
| `cursorDown` | `down` | Move cursor down |
|
||||
| `cursorLeft` | `left`, `ctrl+b` | Move cursor left |
|
||||
| `cursorRight` | `right`, `ctrl+f` | Move cursor right |
|
||||
| `cursorWordLeft` | `alt+left`, `ctrl+left`, `alt+b` | Move cursor word left |
|
||||
| `cursorWordRight` | `alt+right`, `ctrl+right`, `alt+f` | Move cursor word right |
|
||||
| `cursorLineStart` | `home`, `ctrl+a` | Move to line start |
|
||||
| `cursorLineEnd` | `end`, `ctrl+e` | Move to line end |
|
||||
| `jumpForward` | `ctrl+]` | Jump forward to character |
|
||||
| `jumpBackward` | `ctrl+alt+]` | Jump backward to character |
|
||||
| `pageUp` | `pageUp` | Scroll up by page |
|
||||
| `pageDown` | `pageDown` | Scroll down by page |
|
||||
|
||||
### Deletion
|
||||
|
||||
| Action | Default | Description |
|
||||
| -------------------- | ------------------------- | ------------------------- |
|
||||
| `deleteCharBackward` | `backspace` | Delete character backward |
|
||||
| `deleteCharForward` | `delete`, `ctrl+d` | Delete character forward |
|
||||
| `deleteWordBackward` | `ctrl+w`, `alt+backspace` | Delete word backward |
|
||||
| `deleteWordForward` | `alt+d`, `alt+delete` | Delete word forward |
|
||||
| `deleteToLineStart` | `ctrl+u` | Delete to line start |
|
||||
| `deleteToLineEnd` | `ctrl+k` | Delete to line end |
|
||||
|
||||
### Text Input
|
||||
|
||||
| Action | Default | Description |
|
||||
| --------- | ------------- | ------------------ |
|
||||
| `newLine` | `shift+enter` | Insert new line |
|
||||
| `submit` | `enter` | Submit input |
|
||||
| `tab` | `tab` | Tab / autocomplete |
|
||||
|
||||
### Kill Ring
|
||||
|
||||
| Action | Default | Description |
|
||||
| --------- | -------- | ------------------------------------- |
|
||||
| `yank` | `ctrl+y` | Paste most recently deleted text |
|
||||
| `yankPop` | `alt+y` | Cycle through deleted text after yank |
|
||||
| `undo` | `ctrl+-` | Undo last edit |
|
||||
|
||||
### Clipboard
|
||||
|
||||
| Action | Default | Description |
|
||||
| ------------ | -------- | -------------------------- |
|
||||
| `copy` | `ctrl+c` | Copy selection |
|
||||
| `pasteImage` | `ctrl+v` | Paste image from clipboard |
|
||||
|
||||
### Application
|
||||
|
||||
| Action | Default | Description |
|
||||
| ---------------- | -------- | ------------------------------------------------ |
|
||||
| `interrupt` | `escape` | Cancel / abort |
|
||||
| `clear` | `ctrl+c` | Clear editor |
|
||||
| `exit` | `ctrl+d` | Exit (when editor empty) |
|
||||
| `suspend` | `ctrl+z` | Suspend to background |
|
||||
| `externalEditor` | `ctrl+g` | Open in external editor (`$VISUAL` or `$EDITOR`) |
|
||||
|
||||
### Session
|
||||
|
||||
| Action | Default | Description |
|
||||
| ------------ | -------- | -------------------------------------- |
|
||||
| `newSession` | _(none)_ | Start a new session (`/new`) |
|
||||
| `tree` | _(none)_ | Open session tree navigator (`/tree`) |
|
||||
| `fork` | _(none)_ | Fork current session (`/fork`) |
|
||||
| `resume` | _(none)_ | Open session resume picker (`/resume`) |
|
||||
|
||||
### Models & Thinking
|
||||
|
||||
| Action | Default | Description |
|
||||
| -------------------- | -------------- | ----------------------- |
|
||||
| `selectModel` | `ctrl+l` | Open model selector |
|
||||
| `cycleModelForward` | `ctrl+p` | Cycle to next model |
|
||||
| `cycleModelBackward` | `shift+ctrl+p` | Cycle to previous model |
|
||||
| `cycleThinkingLevel` | `shift+tab` | Cycle thinking level |
|
||||
|
||||
### Display
|
||||
|
||||
| Action | Default | Description |
|
||||
| ---------------- | -------- | ------------------------------- |
|
||||
| `expandTools` | `ctrl+o` | Collapse/expand tool output |
|
||||
| `toggleThinking` | `ctrl+t` | Collapse/expand thinking blocks |
|
||||
|
||||
### Message Queue
|
||||
|
||||
| Action | Default | Description |
|
||||
| ---------- | ----------- | --------------------------------- |
|
||||
| `followUp` | `alt+enter` | Queue follow-up message |
|
||||
| `dequeue` | `alt+up` | Restore queued messages to editor |
|
||||
|
||||
### Selection (Lists, Pickers)
|
||||
|
||||
| Action | Default | Description |
|
||||
| ---------------- | ------------------ | ------------------- |
|
||||
| `selectUp` | `up` | Move selection up |
|
||||
| `selectDown` | `down` | Move selection down |
|
||||
| `selectPageUp` | `pageUp` | Page up in list |
|
||||
| `selectPageDown` | `pageDown` | Page down in list |
|
||||
| `selectConfirm` | `enter` | Confirm selection |
|
||||
| `selectCancel` | `escape`, `ctrl+c` | Cancel selection |
|
||||
|
||||
### Session Picker
|
||||
|
||||
| Action | Default | Description |
|
||||
| -------------------------- | ---------------- | --------------------------------- |
|
||||
| `toggleSessionPath` | `ctrl+p` | Toggle path display |
|
||||
| `toggleSessionSort` | `ctrl+s` | Toggle sort mode |
|
||||
| `toggleSessionNamedFilter` | `ctrl+n` | Toggle named-only filter |
|
||||
| `renameSession` | `ctrl+r` | Rename session |
|
||||
| `deleteSession` | `ctrl+d` | Delete session |
|
||||
| `deleteSessionNoninvasive` | `ctrl+backspace` | Delete session (when query empty) |
|
||||
|
||||
## Custom Configuration
|
||||
|
||||
Create `~/.pi/agent/keybindings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"cursorUp": ["up", "ctrl+p"],
|
||||
"cursorDown": ["down", "ctrl+n"],
|
||||
"deleteWordBackward": ["ctrl+w", "alt+backspace"]
|
||||
}
|
||||
```
|
||||
|
||||
Each action can have a single key or an array of keys. User config overrides defaults.
|
||||
|
||||
### Emacs Example
|
||||
|
||||
```json
|
||||
{
|
||||
"cursorUp": ["up", "ctrl+p"],
|
||||
"cursorDown": ["down", "ctrl+n"],
|
||||
"cursorLeft": ["left", "ctrl+b"],
|
||||
"cursorRight": ["right", "ctrl+f"],
|
||||
"cursorWordLeft": ["alt+left", "alt+b"],
|
||||
"cursorWordRight": ["alt+right", "alt+f"],
|
||||
"deleteCharForward": ["delete", "ctrl+d"],
|
||||
"deleteCharBackward": ["backspace", "ctrl+h"],
|
||||
"newLine": ["shift+enter", "ctrl+j"]
|
||||
}
|
||||
```
|
||||
|
||||
### Vim Example
|
||||
|
||||
```json
|
||||
{
|
||||
"cursorUp": ["up", "alt+k"],
|
||||
"cursorDown": ["down", "alt+j"],
|
||||
"cursorLeft": ["left", "alt+h"],
|
||||
"cursorRight": ["right", "alt+l"],
|
||||
"cursorWordLeft": ["alt+left", "alt+b"],
|
||||
"cursorWordRight": ["alt+right", "alt+w"]
|
||||
}
|
||||
```
|
||||
302
packages/coding-agent/docs/models.md
Normal file
302
packages/coding-agent/docs/models.md
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
# Custom Models
|
||||
|
||||
Add custom providers and models (Ollama, vLLM, LM Studio, proxies) via `~/.pi/agent/models.json`.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Minimal Example](#minimal-example)
|
||||
- [Full Example](#full-example)
|
||||
- [Supported APIs](#supported-apis)
|
||||
- [Provider Configuration](#provider-configuration)
|
||||
- [Model Configuration](#model-configuration)
|
||||
- [Overriding Built-in Providers](#overriding-built-in-providers)
|
||||
- [Per-model Overrides](#per-model-overrides)
|
||||
- [OpenAI Compatibility](#openai-compatibility)
|
||||
|
||||
## Minimal Example
|
||||
|
||||
For local models (Ollama, LM Studio, vLLM), only `id` is required per model:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"baseUrl": "http://localhost:11434/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "ollama",
|
||||
"models": [{ "id": "llama3.1:8b" }, { "id": "qwen2.5-coder:7b" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `apiKey` is required but Ollama ignores it, so any value works.
|
||||
|
||||
## Full Example
|
||||
|
||||
Override defaults when you need specific values:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"baseUrl": "http://localhost:11434/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "ollama",
|
||||
"models": [
|
||||
{
|
||||
"id": "llama3.1:8b",
|
||||
"name": "Llama 3.1 8B (Local)",
|
||||
"reasoning": false,
|
||||
"input": ["text"],
|
||||
"contextWindow": 128000,
|
||||
"maxTokens": 32000,
|
||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The file reloads each time you open `/model`. Edit during session; no restart needed.
|
||||
|
||||
## Supported APIs
|
||||
|
||||
| API | Description |
|
||||
| ---------------------- | ----------------------------------------- |
|
||||
| `openai-completions` | OpenAI Chat Completions (most compatible) |
|
||||
| `openai-responses` | OpenAI Responses API |
|
||||
| `anthropic-messages` | Anthropic Messages API |
|
||||
| `google-generative-ai` | Google Generative AI |
|
||||
|
||||
Set `api` at provider level (default for all models) or model level (override per model).
|
||||
|
||||
## Provider Configuration
|
||||
|
||||
| Field | Description |
|
||||
| ---------------- | ---------------------------------------------------------------- |
|
||||
| `baseUrl` | API endpoint URL |
|
||||
| `api` | API type (see above) |
|
||||
| `apiKey` | API key (see value resolution below) |
|
||||
| `headers` | Custom headers (see value resolution below) |
|
||||
| `authHeader` | Set `true` to add `Authorization: Bearer <apiKey>` automatically |
|
||||
| `models` | Array of model configurations |
|
||||
| `modelOverrides` | Per-model overrides for built-in models on this provider |
|
||||
|
||||
### Value Resolution
|
||||
|
||||
The `apiKey` and `headers` fields support three formats:
|
||||
|
||||
- **Shell command:** `"!command"` executes and uses stdout
|
||||
```json
|
||||
"apiKey": "!security find-generic-password -ws 'anthropic'"
|
||||
"apiKey": "!op read 'op://vault/item/credential'"
|
||||
```
|
||||
- **Environment variable:** Uses the value of the named variable
|
||||
```json
|
||||
"apiKey": "MY_API_KEY"
|
||||
```
|
||||
- **Literal value:** Used directly
|
||||
```json
|
||||
"apiKey": "sk-..."
|
||||
```
|
||||
|
||||
### Custom Headers
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"custom-proxy": {
|
||||
"baseUrl": "https://proxy.example.com/v1",
|
||||
"apiKey": "MY_API_KEY",
|
||||
"api": "anthropic-messages",
|
||||
"headers": {
|
||||
"x-portkey-api-key": "PORTKEY_API_KEY",
|
||||
"x-secret": "!op read 'op://vault/item/secret'"
|
||||
},
|
||||
"models": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Model Configuration
|
||||
|
||||
| Field | Required | Default | Description |
|
||||
| --------------- | -------- | ---------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | Yes | — | Model identifier (passed to the API) |
|
||||
| `name` | No | `id` | Human-readable model label. Used for matching (`--model` patterns) and shown in model details/status text. |
|
||||
| `api` | No | provider's `api` | Override provider's API for this model |
|
||||
| `reasoning` | No | `false` | Supports extended thinking |
|
||||
| `input` | No | `["text"]` | Input types: `["text"]` or `["text", "image"]` |
|
||||
| `contextWindow` | No | `128000` | Context window size in tokens |
|
||||
| `maxTokens` | No | `16384` | Maximum output tokens |
|
||||
| `cost` | No | all zeros | `{"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0}` (per million tokens) |
|
||||
|
||||
Current behavior:
|
||||
|
||||
- `/model` and `--list-models` list entries by model `id`.
|
||||
- The configured `name` is used for model matching and detail/status text.
|
||||
|
||||
## Overriding Built-in Providers
|
||||
|
||||
Route a built-in provider through a proxy without redefining models:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"anthropic": {
|
||||
"baseUrl": "https://my-proxy.example.com/v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All built-in Anthropic models remain available. Existing OAuth or API key auth continues to work.
|
||||
|
||||
To merge custom models into a built-in provider, include the `models` array:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"anthropic": {
|
||||
"baseUrl": "https://my-proxy.example.com/v1",
|
||||
"apiKey": "ANTHROPIC_API_KEY",
|
||||
"api": "anthropic-messages",
|
||||
"models": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Merge semantics:
|
||||
|
||||
- Built-in models are kept.
|
||||
- Custom models are upserted by `id` within the provider.
|
||||
- If a custom model `id` matches a built-in model `id`, the custom model replaces that built-in model.
|
||||
- If a custom model `id` is new, it is added alongside built-in models.
|
||||
|
||||
## Per-model Overrides
|
||||
|
||||
Use `modelOverrides` to customize specific built-in models without replacing the provider's full model list.
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"openrouter": {
|
||||
"modelOverrides": {
|
||||
"anthropic/claude-sonnet-4": {
|
||||
"name": "Claude Sonnet 4 (Bedrock Route)",
|
||||
"compat": {
|
||||
"openRouterRouting": {
|
||||
"only": ["amazon-bedrock"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`modelOverrides` supports these fields per model: `name`, `reasoning`, `input`, `cost` (partial), `contextWindow`, `maxTokens`, `headers`, `compat`.
|
||||
|
||||
Behavior notes:
|
||||
|
||||
- `modelOverrides` are applied to built-in provider models.
|
||||
- Unknown model IDs are ignored.
|
||||
- You can combine provider-level `baseUrl`/`headers` with `modelOverrides`.
|
||||
- If `models` is also defined for a provider, custom models are merged after built-in overrides. A custom model with the same `id` replaces the overridden built-in model entry.
|
||||
|
||||
## OpenAI Compatibility
|
||||
|
||||
For providers with partial OpenAI compatibility, use the `compat` field:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"local-llm": {
|
||||
"baseUrl": "http://localhost:8080/v1",
|
||||
"api": "openai-completions",
|
||||
"compat": {
|
||||
"supportsUsageInStreaming": false,
|
||||
"maxTokensField": "max_tokens"
|
||||
},
|
||||
"models": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
| -------------------------- | --------------------------------------------------------------------------- |
|
||||
| `supportsStore` | Provider supports `store` field |
|
||||
| `supportsDeveloperRole` | Use `developer` vs `system` role |
|
||||
| `supportsReasoningEffort` | Support for `reasoning_effort` parameter |
|
||||
| `supportsUsageInStreaming` | Supports `stream_options: { include_usage: true }` (default: `true`) |
|
||||
| `maxTokensField` | Use `max_completion_tokens` or `max_tokens` |
|
||||
| `openRouterRouting` | OpenRouter routing config passed to OpenRouter for model/provider selection |
|
||||
| `vercelGatewayRouting` | Vercel AI Gateway routing config for provider selection (`only`, `order`) |
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"openrouter": {
|
||||
"baseUrl": "https://openrouter.ai/api/v1",
|
||||
"apiKey": "OPENROUTER_API_KEY",
|
||||
"api": "openai-completions",
|
||||
"models": [
|
||||
{
|
||||
"id": "openrouter/anthropic/claude-3.5-sonnet",
|
||||
"name": "OpenRouter Claude 3.5 Sonnet",
|
||||
"compat": {
|
||||
"openRouterRouting": {
|
||||
"order": ["anthropic"],
|
||||
"fallbacks": ["openai"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Vercel AI Gateway example:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"vercel-ai-gateway": {
|
||||
"baseUrl": "https://ai-gateway.vercel.sh/v1",
|
||||
"apiKey": "AI_GATEWAY_API_KEY",
|
||||
"api": "openai-completions",
|
||||
"models": [
|
||||
{
|
||||
"id": "moonshotai/kimi-k2.5",
|
||||
"name": "Kimi K2.5 (Fireworks via Vercel)",
|
||||
"reasoning": true,
|
||||
"input": ["text", "image"],
|
||||
"cost": {
|
||||
"input": 0.6,
|
||||
"output": 3,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 262144,
|
||||
"maxTokens": 262144,
|
||||
"compat": {
|
||||
"vercelGatewayRouting": {
|
||||
"only": ["fireworks", "novita"],
|
||||
"order": ["fireworks", "novita"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
210
packages/coding-agent/docs/packages.md
Normal file
210
packages/coding-agent/docs/packages.md
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
> pi can help you create pi packages. Ask it to bundle your extensions, skills, prompt templates, or themes.
|
||||
|
||||
# Pi Packages
|
||||
|
||||
Pi packages bundle extensions, skills, prompt templates, and themes so you can share them through npm or git. A package can declare resources in `package.json` under the `pi` key, or use conventional directories.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Install and Manage](#install-and-manage)
|
||||
- [Package Sources](#package-sources)
|
||||
- [Creating a Pi Package](#creating-a-pi-package)
|
||||
- [Package Structure](#package-structure)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Package Filtering](#package-filtering)
|
||||
- [Enable and Disable Resources](#enable-and-disable-resources)
|
||||
- [Scope and Deduplication](#scope-and-deduplication)
|
||||
|
||||
## Install and Manage
|
||||
|
||||
> **Security:** Pi packages run with full system access. Extensions execute arbitrary code, and skills can instruct the model to perform any action including running executables. Review source code before installing third-party packages.
|
||||
|
||||
```bash
|
||||
pi install npm:@foo/bar@1.0.0
|
||||
pi install git:github.com/user/repo@v1
|
||||
pi install https://github.com/user/repo # raw URLs work too
|
||||
pi install /absolute/path/to/package
|
||||
pi install ./relative/path/to/package
|
||||
|
||||
pi remove npm:@foo/bar
|
||||
pi list # show installed packages from settings
|
||||
pi update # update all non-pinned packages
|
||||
```
|
||||
|
||||
By default, `install` and `remove` write to global settings (`~/.pi/agent/settings.json`). Use `-l` to write to project settings (`.pi/settings.json`) instead. Project settings can be shared with your team, and pi installs any missing packages automatically on startup.
|
||||
|
||||
To try a package without installing it, use `--extension` or `-e`. This installs to a temporary directory for the current run only:
|
||||
|
||||
```bash
|
||||
pi -e npm:@foo/bar
|
||||
pi -e git:github.com/user/repo
|
||||
```
|
||||
|
||||
## Package Sources
|
||||
|
||||
Pi accepts three source types in settings and `pi install`.
|
||||
|
||||
### npm
|
||||
|
||||
```
|
||||
npm:@scope/pkg@1.2.3
|
||||
npm:pkg
|
||||
```
|
||||
|
||||
- Versioned specs are pinned and skipped by `pi update`.
|
||||
- Global installs use `npm install -g`.
|
||||
- Project installs go under `.pi/npm/`.
|
||||
|
||||
### git
|
||||
|
||||
```
|
||||
git:github.com/user/repo@v1
|
||||
git:git@github.com:user/repo@v1
|
||||
https://github.com/user/repo@v1
|
||||
ssh://git@github.com/user/repo@v1
|
||||
```
|
||||
|
||||
- Without `git:` prefix, only protocol URLs are accepted (`https://`, `http://`, `ssh://`, `git://`).
|
||||
- With `git:` prefix, shorthand formats are accepted, including `github.com/user/repo` and `git@github.com:user/repo`.
|
||||
- HTTPS and SSH URLs are both supported.
|
||||
- SSH URLs use your configured SSH keys automatically (respects `~/.ssh/config`).
|
||||
- For non-interactive runs (for example CI), you can set `GIT_TERMINAL_PROMPT=0` to disable credential prompts and set `GIT_SSH_COMMAND` (for example `ssh -o BatchMode=yes -o ConnectTimeout=5`) to fail fast.
|
||||
- Refs pin the package and skip `pi update`.
|
||||
- Cloned to `~/.pi/agent/git/<host>/<path>` (global) or `.pi/git/<host>/<path>` (project).
|
||||
- Runs `npm install` after clone or pull if `package.json` exists.
|
||||
|
||||
**SSH examples:**
|
||||
|
||||
```bash
|
||||
# git@host:path shorthand (requires git: prefix)
|
||||
pi install git:git@github.com:user/repo
|
||||
|
||||
# ssh:// protocol format
|
||||
pi install ssh://git@github.com/user/repo
|
||||
|
||||
# With version ref
|
||||
pi install git:git@github.com:user/repo@v1.0.0
|
||||
```
|
||||
|
||||
### Local Paths
|
||||
|
||||
```
|
||||
/absolute/path/to/package
|
||||
./relative/path/to/package
|
||||
```
|
||||
|
||||
Local paths point to files or directories on disk and are added to settings without copying. Relative paths are resolved against the settings file they appear in. If the path is a file, it loads as a single extension. If it is a directory, pi loads resources using package rules.
|
||||
|
||||
## Creating a Pi Package
|
||||
|
||||
Add a `pi` manifest to `package.json` or use conventional directories. Include the `pi-package` keyword for discoverability.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-package",
|
||||
"keywords": ["pi-package"],
|
||||
"pi": {
|
||||
"extensions": ["./extensions"],
|
||||
"skills": ["./skills"],
|
||||
"prompts": ["./prompts"],
|
||||
"themes": ["./themes"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Paths are relative to the package root. Arrays support glob patterns and `!exclusions`.
|
||||
|
||||
### Gallery Metadata
|
||||
|
||||
The [package gallery](https://shittycodingagent.ai/packages) displays packages tagged with `pi-package`. Add `video` or `image` fields to show a preview:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-package",
|
||||
"keywords": ["pi-package"],
|
||||
"pi": {
|
||||
"extensions": ["./extensions"],
|
||||
"video": "https://example.com/demo.mp4",
|
||||
"image": "https://example.com/screenshot.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **video**: MP4 only. On desktop, autoplays on hover. Clicking opens a fullscreen player.
|
||||
- **image**: PNG, JPEG, GIF, or WebP. Displayed as a static preview.
|
||||
|
||||
If both are set, video takes precedence.
|
||||
|
||||
## Package Structure
|
||||
|
||||
### Convention Directories
|
||||
|
||||
If no `pi` manifest is present, pi auto-discovers resources from these directories:
|
||||
|
||||
- `extensions/` loads `.ts` and `.js` files
|
||||
- `skills/` recursively finds `SKILL.md` folders and loads top-level `.md` files as skills
|
||||
- `prompts/` loads `.md` files
|
||||
- `themes/` loads `.json` files
|
||||
|
||||
## Dependencies
|
||||
|
||||
Third party runtime dependencies belong in `dependencies` in `package.json`. Dependencies that do not register extensions, skills, prompt templates, or themes also belong in `dependencies`. When pi installs a package from npm or git, it runs `npm install`, so those dependencies are installed automatically.
|
||||
|
||||
Pi bundles core packages for extensions and skills. If you import any of these, list them in `peerDependencies` with a `"*"` range and do not bundle them: `@mariozechner/pi-ai`, `@mariozechner/pi-agent-core`, `@mariozechner/pi-coding-agent`, `@mariozechner/pi-tui`, `@sinclair/typebox`.
|
||||
|
||||
Other pi packages must be bundled in your tarball. Add them to `dependencies` and `bundledDependencies`, then reference their resources through `node_modules/` paths. Pi loads packages with separate module roots, so separate installs do not collide or share modules.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"shitty-extensions": "^1.0.1"
|
||||
},
|
||||
"bundledDependencies": ["shitty-extensions"],
|
||||
"pi": {
|
||||
"extensions": ["extensions", "node_modules/shitty-extensions/extensions"],
|
||||
"skills": ["skills", "node_modules/shitty-extensions/skills"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Package Filtering
|
||||
|
||||
Filter what a package loads using the object form in settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"packages": [
|
||||
"npm:simple-pkg",
|
||||
{
|
||||
"source": "npm:my-package",
|
||||
"extensions": ["extensions/*.ts", "!extensions/legacy.ts"],
|
||||
"skills": [],
|
||||
"prompts": ["prompts/review.md"],
|
||||
"themes": ["+themes/legacy.json"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`+path` and `-path` are exact paths relative to the package root.
|
||||
|
||||
- Omit a key to load all of that type.
|
||||
- Use `[]` to load none of that type.
|
||||
- `!pattern` excludes matches.
|
||||
- `+path` force-includes an exact path.
|
||||
- `-path` force-excludes an exact path.
|
||||
- Filters layer on top of the manifest. They narrow down what is already allowed.
|
||||
|
||||
## Enable and Disable Resources
|
||||
|
||||
Use `pi config` to enable or disable extensions, skills, prompt templates, and themes from installed packages and local directories. Works for both global (`~/.pi/agent`) and project (`.pi/`) scopes.
|
||||
|
||||
## Scope and Deduplication
|
||||
|
||||
Packages can appear in both global and project settings. If the same package appears in both, the project entry wins. Identity is determined by:
|
||||
|
||||
- npm: package name
|
||||
- git: repository URL without ref
|
||||
- local: resolved absolute path
|
||||
70
packages/coding-agent/docs/prompt-templates.md
Normal file
70
packages/coding-agent/docs/prompt-templates.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
> pi can create prompt templates. Ask it to build one for your workflow.
|
||||
|
||||
# Prompt Templates
|
||||
|
||||
Prompt templates are Markdown snippets that expand into full prompts. Type `/name` in the editor to invoke a template, where `name` is the filename without `.md`.
|
||||
|
||||
## Locations
|
||||
|
||||
Pi loads prompt templates from:
|
||||
|
||||
- Global: `~/.pi/agent/prompts/*.md`
|
||||
- Project: `.pi/prompts/*.md`
|
||||
- Packages: `prompts/` directories or `pi.prompts` entries in `package.json`
|
||||
- Settings: `prompts` array with files or directories
|
||||
- CLI: `--prompt-template <path>` (repeatable)
|
||||
|
||||
Disable discovery with `--no-prompt-templates`.
|
||||
|
||||
## Format
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: Review staged git changes
|
||||
---
|
||||
|
||||
Review the staged changes (`git diff --cached`). Focus on:
|
||||
|
||||
- Bugs and logic errors
|
||||
- Security issues
|
||||
- Error handling gaps
|
||||
```
|
||||
|
||||
- The filename becomes the command name. `review.md` becomes `/review`.
|
||||
- `description` is optional. If missing, the first non-empty line is used.
|
||||
|
||||
## Usage
|
||||
|
||||
Type `/` followed by the template name in the editor. Autocomplete shows available templates with descriptions.
|
||||
|
||||
```
|
||||
/review # Expands review.md
|
||||
/component Button # Expands with argument
|
||||
/component Button "click handler" # Multiple arguments
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
Templates support positional arguments and simple slicing:
|
||||
|
||||
- `$1`, `$2`, ... positional args
|
||||
- `$@` or `$ARGUMENTS` for all args joined
|
||||
- `${@:N}` for args from the Nth position (1-indexed)
|
||||
- `${@:N:L}` for `L` args starting at N
|
||||
|
||||
Example:
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: Create a component
|
||||
---
|
||||
|
||||
Create a React component named $1 with features: $@
|
||||
```
|
||||
|
||||
Usage: `/component Button "onClick handler" "disabled support"`
|
||||
|
||||
## Loading Rules
|
||||
|
||||
- Template discovery in `prompts/` is non-recursive.
|
||||
- If you want templates in subdirectories, add them explicitly via `prompts` settings or a package manifest.
|
||||
188
packages/coding-agent/docs/providers.md
Normal file
188
packages/coding-agent/docs/providers.md
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
# Providers
|
||||
|
||||
Pi supports subscription-based providers via OAuth and API key providers via environment variables or auth file. For each provider, pi knows all available models. The list is updated with every pi release.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Subscriptions](#subscriptions)
|
||||
- [API Keys](#api-keys)
|
||||
- [Auth File](#auth-file)
|
||||
- [Cloud Providers](#cloud-providers)
|
||||
- [Custom Providers](#custom-providers)
|
||||
- [Resolution Order](#resolution-order)
|
||||
|
||||
## Subscriptions
|
||||
|
||||
Use `/login` in interactive mode, then select a provider:
|
||||
|
||||
- Claude Pro/Max
|
||||
- ChatGPT Plus/Pro (Codex)
|
||||
- GitHub Copilot
|
||||
- Google Gemini CLI
|
||||
- Google Antigravity
|
||||
|
||||
Use `/logout` to clear credentials. Tokens are stored in `~/.pi/agent/auth.json` and auto-refresh when expired.
|
||||
|
||||
### GitHub Copilot
|
||||
|
||||
- Press Enter for github.com, or enter your GitHub Enterprise Server domain
|
||||
- If you get "model not supported", enable it in VS Code: Copilot Chat → model selector → select model → "Enable"
|
||||
|
||||
### Google Providers
|
||||
|
||||
- **Gemini CLI**: Standard Gemini models via Cloud Code Assist
|
||||
- **Antigravity**: Sandbox with Gemini 3, Claude, and GPT-OSS models
|
||||
- Both free with any Google account, subject to rate limits
|
||||
- For paid Cloud Code Assist: set `GOOGLE_CLOUD_PROJECT` env var
|
||||
|
||||
### OpenAI Codex
|
||||
|
||||
- Requires ChatGPT Plus or Pro subscription
|
||||
- Personal use only; for production, use the OpenAI Platform API
|
||||
|
||||
## API Keys
|
||||
|
||||
### Environment Variables or Auth File
|
||||
|
||||
Set via environment variable:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY=sk-ant-...
|
||||
pi
|
||||
```
|
||||
|
||||
| Provider | Environment Variable | `auth.json` key |
|
||||
| ---------------------- | ---------------------- | ------------------------ |
|
||||
| Anthropic | `ANTHROPIC_API_KEY` | `anthropic` |
|
||||
| Azure OpenAI Responses | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |
|
||||
| OpenAI | `OPENAI_API_KEY` | `openai` |
|
||||
| Google Gemini | `GEMINI_API_KEY` | `google` |
|
||||
| Mistral | `MISTRAL_API_KEY` | `mistral` |
|
||||
| Groq | `GROQ_API_KEY` | `groq` |
|
||||
| Cerebras | `CEREBRAS_API_KEY` | `cerebras` |
|
||||
| xAI | `XAI_API_KEY` | `xai` |
|
||||
| OpenRouter | `OPENROUTER_API_KEY` | `openrouter` |
|
||||
| Vercel AI Gateway | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` |
|
||||
| ZAI | `ZAI_API_KEY` | `zai` |
|
||||
| OpenCode Zen | `OPENCODE_API_KEY` | `opencode` |
|
||||
| OpenCode Go | `OPENCODE_API_KEY` | `opencode-go` |
|
||||
| Hugging Face | `HF_TOKEN` | `huggingface` |
|
||||
| Kimi For Coding | `KIMI_API_KEY` | `kimi-coding` |
|
||||
| MiniMax | `MINIMAX_API_KEY` | `minimax` |
|
||||
| MiniMax (China) | `MINIMAX_CN_API_KEY` | `minimax-cn` |
|
||||
|
||||
Reference for environment variables and `auth.json` keys: [`const envMap`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/env-api-keys.ts) in [`packages/ai/src/env-api-keys.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/env-api-keys.ts).
|
||||
|
||||
#### Auth File
|
||||
|
||||
Store credentials in `~/.pi/agent/auth.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"anthropic": { "type": "api_key", "key": "sk-ant-..." },
|
||||
"openai": { "type": "api_key", "key": "sk-..." },
|
||||
"google": { "type": "api_key", "key": "..." },
|
||||
"opencode": { "type": "api_key", "key": "..." },
|
||||
"opencode-go": { "type": "api_key", "key": "..." }
|
||||
}
|
||||
```
|
||||
|
||||
The file is created with `0600` permissions (user read/write only). Auth file credentials take priority over environment variables.
|
||||
|
||||
### Key Resolution
|
||||
|
||||
The `key` field supports three formats:
|
||||
|
||||
- **Shell command:** `"!command"` executes and uses stdout (cached for process lifetime)
|
||||
```json
|
||||
{ "type": "api_key", "key": "!security find-generic-password -ws 'anthropic'" }
|
||||
{ "type": "api_key", "key": "!op read 'op://vault/item/credential'" }
|
||||
```
|
||||
- **Environment variable:** Uses the value of the named variable
|
||||
```json
|
||||
{ "type": "api_key", "key": "MY_ANTHROPIC_KEY" }
|
||||
```
|
||||
- **Literal value:** Used directly
|
||||
```json
|
||||
{ "type": "api_key", "key": "sk-ant-..." }
|
||||
```
|
||||
|
||||
OAuth credentials are also stored here after `/login` and managed automatically.
|
||||
|
||||
## Cloud Providers
|
||||
|
||||
### Azure OpenAI
|
||||
|
||||
```bash
|
||||
export AZURE_OPENAI_API_KEY=...
|
||||
export AZURE_OPENAI_BASE_URL=https://your-resource.openai.azure.com
|
||||
# or use resource name instead of base URL
|
||||
export AZURE_OPENAI_RESOURCE_NAME=your-resource
|
||||
|
||||
# Optional
|
||||
export AZURE_OPENAI_API_VERSION=2024-02-01
|
||||
export AZURE_OPENAI_DEPLOYMENT_NAME_MAP=gpt-4=my-gpt4,gpt-4o=my-gpt4o
|
||||
```
|
||||
|
||||
### Amazon Bedrock
|
||||
|
||||
```bash
|
||||
# Option 1: AWS Profile
|
||||
export AWS_PROFILE=your-profile
|
||||
|
||||
# Option 2: IAM Keys
|
||||
export AWS_ACCESS_KEY_ID=AKIA...
|
||||
export AWS_SECRET_ACCESS_KEY=...
|
||||
|
||||
# Option 3: Bearer Token
|
||||
export AWS_BEARER_TOKEN_BEDROCK=...
|
||||
|
||||
# Optional region (defaults to us-east-1)
|
||||
export AWS_REGION=us-west-2
|
||||
```
|
||||
|
||||
Also supports ECS task roles (`AWS_CONTAINER_CREDENTIALS_*`) and IRSA (`AWS_WEB_IDENTITY_TOKEN_FILE`).
|
||||
|
||||
```bash
|
||||
pi --provider amazon-bedrock --model us.anthropic.claude-sonnet-4-20250514-v1:0
|
||||
```
|
||||
|
||||
If you are connecting to a Bedrock API proxy, the following environment variables can be used:
|
||||
|
||||
```bash
|
||||
# Set the URL for the Bedrock proxy (standard AWS SDK env var)
|
||||
export AWS_ENDPOINT_URL_BEDROCK_RUNTIME=https://my.corp.proxy/bedrock
|
||||
|
||||
# Set if your proxy does not require authentication
|
||||
export AWS_BEDROCK_SKIP_AUTH=1
|
||||
|
||||
# Set if your proxy only supports HTTP/1.1
|
||||
export AWS_BEDROCK_FORCE_HTTP1=1
|
||||
```
|
||||
|
||||
### Google Vertex AI
|
||||
|
||||
Uses Application Default Credentials:
|
||||
|
||||
```bash
|
||||
gcloud auth application-default login
|
||||
export GOOGLE_CLOUD_PROJECT=your-project
|
||||
export GOOGLE_CLOUD_LOCATION=us-central1
|
||||
```
|
||||
|
||||
Or set `GOOGLE_APPLICATION_CREDENTIALS` to a service account key file.
|
||||
|
||||
## Custom Providers
|
||||
|
||||
**Via models.json:** Add Ollama, LM Studio, vLLM, or any provider that speaks a supported API (OpenAI Completions, OpenAI Responses, Anthropic Messages, Google Generative AI). See [models.md](models.md).
|
||||
|
||||
**Via extensions:** For providers that need custom API implementations or OAuth flows, create an extension. See [custom-provider.md](custom-provider.md).
|
||||
|
||||
## Resolution Order
|
||||
|
||||
When resolving credentials for a provider:
|
||||
|
||||
1. CLI `--api-key` flag
|
||||
2. `auth.json` entry (API key or OAuth token)
|
||||
3. Environment variable
|
||||
4. Custom provider keys from `models.json`
|
||||
1434
packages/coding-agent/docs/rpc.md
Normal file
1434
packages/coding-agent/docs/rpc.md
Normal file
File diff suppressed because it is too large
Load diff
1027
packages/coding-agent/docs/sdk.md
Normal file
1027
packages/coding-agent/docs/sdk.md
Normal file
File diff suppressed because it is too large
Load diff
500
packages/coding-agent/docs/session.md
Normal file
500
packages/coding-agent/docs/session.md
Normal file
|
|
@ -0,0 +1,500 @@
|
|||
# Session File Format
|
||||
|
||||
Sessions are stored as JSONL (JSON Lines) files. Each line is a JSON object with a `type` field. Session entries form a tree structure via `id`/`parentId` fields, enabling in-place branching without creating new files.
|
||||
|
||||
## File Location
|
||||
|
||||
```
|
||||
~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl
|
||||
```
|
||||
|
||||
Where `<path>` is the working directory with `/` replaced by `-`.
|
||||
|
||||
## Deleting Sessions
|
||||
|
||||
Sessions can be removed by deleting their `.jsonl` files under `~/.pi/agent/sessions/`.
|
||||
|
||||
Pi also supports deleting sessions interactively from `/resume` (select a session and press `Ctrl+D`, then confirm). When available, pi uses the `trash` CLI to avoid permanent deletion.
|
||||
|
||||
## Session Version
|
||||
|
||||
Sessions have a version field in the header:
|
||||
|
||||
- **Version 1**: Linear entry sequence (legacy, auto-migrated on load)
|
||||
- **Version 2**: Tree structure with `id`/`parentId` linking
|
||||
- **Version 3**: Renamed `hookMessage` role to `custom` (extensions unification)
|
||||
|
||||
Existing sessions are automatically migrated to the current version (v3) when loaded.
|
||||
|
||||
## Source Files
|
||||
|
||||
Source on GitHub ([pi-mono](https://github.com/badlogic/pi-mono)):
|
||||
|
||||
- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts) - Session entry types and SessionManager
|
||||
- [`packages/coding-agent/src/core/messages.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/messages.ts) - Extended message types (BashExecutionMessage, CustomMessage, etc.)
|
||||
- [`packages/ai/src/types.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/types.ts) - Base message types (UserMessage, AssistantMessage, ToolResultMessage)
|
||||
- [`packages/agent/src/types.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/agent/src/types.ts) - AgentMessage union type
|
||||
|
||||
For TypeScript definitions in your project, inspect `node_modules/@mariozechner/pi-coding-agent/dist/` and `node_modules/@mariozechner/pi-ai/dist/`.
|
||||
|
||||
## Message Types
|
||||
|
||||
Session entries contain `AgentMessage` objects. Understanding these types is essential for parsing sessions and writing extensions.
|
||||
|
||||
### Content Blocks
|
||||
|
||||
Messages contain arrays of typed content blocks:
|
||||
|
||||
```typescript
|
||||
interface TextContent {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface ImageContent {
|
||||
type: "image";
|
||||
data: string; // base64 encoded
|
||||
mimeType: string; // e.g., "image/jpeg", "image/png"
|
||||
}
|
||||
|
||||
interface ThinkingContent {
|
||||
type: "thinking";
|
||||
thinking: string;
|
||||
}
|
||||
|
||||
interface ToolCall {
|
||||
type: "toolCall";
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, any>;
|
||||
}
|
||||
```
|
||||
|
||||
### Base Message Types (from pi-ai)
|
||||
|
||||
```typescript
|
||||
interface UserMessage {
|
||||
role: "user";
|
||||
content: string | (TextContent | ImageContent)[];
|
||||
timestamp: number; // Unix ms
|
||||
}
|
||||
|
||||
interface AssistantMessage {
|
||||
role: "assistant";
|
||||
content: (TextContent | ThinkingContent | ToolCall)[];
|
||||
api: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
usage: Usage;
|
||||
stopReason: "stop" | "length" | "toolUse" | "error" | "aborted";
|
||||
errorMessage?: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface ToolResultMessage {
|
||||
role: "toolResult";
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
content: (TextContent | ImageContent)[];
|
||||
details?: any; // Tool-specific metadata
|
||||
isError: boolean;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface Usage {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
totalTokens: number;
|
||||
cost: {
|
||||
input: number;
|
||||
output: number;
|
||||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Extended Message Types (from pi-coding-agent)
|
||||
|
||||
```typescript
|
||||
interface BashExecutionMessage {
|
||||
role: "bashExecution";
|
||||
command: string;
|
||||
output: string;
|
||||
exitCode: number | undefined;
|
||||
cancelled: boolean;
|
||||
truncated: boolean;
|
||||
fullOutputPath?: string;
|
||||
excludeFromContext?: boolean; // true for !! prefix commands
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface CustomMessage {
|
||||
role: "custom";
|
||||
customType: string; // Extension identifier
|
||||
content: string | (TextContent | ImageContent)[];
|
||||
display: boolean; // Show in TUI
|
||||
details?: any; // Extension-specific metadata
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface BranchSummaryMessage {
|
||||
role: "branchSummary";
|
||||
summary: string;
|
||||
fromId: string; // Entry we branched from
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface CompactionSummaryMessage {
|
||||
role: "compactionSummary";
|
||||
summary: string;
|
||||
tokensBefore: number;
|
||||
timestamp: number;
|
||||
}
|
||||
```
|
||||
|
||||
### AgentMessage Union
|
||||
|
||||
```typescript
|
||||
type AgentMessage =
|
||||
| UserMessage
|
||||
| AssistantMessage
|
||||
| ToolResultMessage
|
||||
| BashExecutionMessage
|
||||
| CustomMessage
|
||||
| BranchSummaryMessage
|
||||
| CompactionSummaryMessage;
|
||||
```
|
||||
|
||||
## Entry Base
|
||||
|
||||
All entries (except `SessionHeader`) extend `SessionEntryBase`:
|
||||
|
||||
```typescript
|
||||
interface SessionEntryBase {
|
||||
type: string;
|
||||
id: string; // 8-char hex ID
|
||||
parentId: string | null; // Parent entry ID (null for first entry)
|
||||
timestamp: string; // ISO timestamp
|
||||
}
|
||||
```
|
||||
|
||||
## Entry Types
|
||||
|
||||
### SessionHeader
|
||||
|
||||
First line of the file. Metadata only, not part of the tree (no `id`/`parentId`).
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "session",
|
||||
"version": 3,
|
||||
"id": "uuid",
|
||||
"timestamp": "2024-12-03T14:00:00.000Z",
|
||||
"cwd": "/path/to/project"
|
||||
}
|
||||
```
|
||||
|
||||
For sessions with a parent (created via `/fork` or `newSession({ parentSession })`):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "session",
|
||||
"version": 3,
|
||||
"id": "uuid",
|
||||
"timestamp": "2024-12-03T14:00:00.000Z",
|
||||
"cwd": "/path/to/project",
|
||||
"parentSession": "/path/to/original/session.jsonl"
|
||||
}
|
||||
```
|
||||
|
||||
### SessionMessageEntry
|
||||
|
||||
A message in the conversation. The `message` field contains an `AgentMessage`.
|
||||
|
||||
```json
|
||||
{"type":"message","id":"a1b2c3d4","parentId":"prev1234","timestamp":"2024-12-03T14:00:01.000Z","message":{"role":"user","content":"Hello"}}
|
||||
{"type":"message","id":"b2c3d4e5","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:00:02.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Hi!"}],"provider":"anthropic","model":"claude-sonnet-4-5","usage":{...},"stopReason":"stop"}}
|
||||
{"type":"message","id":"c3d4e5f6","parentId":"b2c3d4e5","timestamp":"2024-12-03T14:00:03.000Z","message":{"role":"toolResult","toolCallId":"call_123","toolName":"bash","content":[{"type":"text","text":"output"}],"isError":false}}
|
||||
```
|
||||
|
||||
### ModelChangeEntry
|
||||
|
||||
Emitted when the user switches models mid-session.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "model_change",
|
||||
"id": "d4e5f6g7",
|
||||
"parentId": "c3d4e5f6",
|
||||
"timestamp": "2024-12-03T14:05:00.000Z",
|
||||
"provider": "openai",
|
||||
"modelId": "gpt-4o"
|
||||
}
|
||||
```
|
||||
|
||||
### ThinkingLevelChangeEntry
|
||||
|
||||
Emitted when the user changes the thinking/reasoning level.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "thinking_level_change",
|
||||
"id": "e5f6g7h8",
|
||||
"parentId": "d4e5f6g7",
|
||||
"timestamp": "2024-12-03T14:06:00.000Z",
|
||||
"thinkingLevel": "high"
|
||||
}
|
||||
```
|
||||
|
||||
### CompactionEntry
|
||||
|
||||
Created when context is compacted. Stores a summary of earlier messages.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "compaction",
|
||||
"id": "f6g7h8i9",
|
||||
"parentId": "e5f6g7h8",
|
||||
"timestamp": "2024-12-03T14:10:00.000Z",
|
||||
"summary": "User discussed X, Y, Z...",
|
||||
"firstKeptEntryId": "c3d4e5f6",
|
||||
"tokensBefore": 50000
|
||||
}
|
||||
```
|
||||
|
||||
Optional fields:
|
||||
|
||||
- `details`: Implementation-specific data (e.g., `{ readFiles: string[], modifiedFiles: string[] }` for default, or custom data for extensions)
|
||||
- `fromHook`: `true` if generated by an extension, `false`/`undefined` if pi-generated (legacy field name)
|
||||
|
||||
### BranchSummaryEntry
|
||||
|
||||
Created when switching branches via `/tree` with an LLM generated summary of the left branch up to the common ancestor. Captures context from the abandoned path.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "branch_summary",
|
||||
"id": "g7h8i9j0",
|
||||
"parentId": "a1b2c3d4",
|
||||
"timestamp": "2024-12-03T14:15:00.000Z",
|
||||
"fromId": "f6g7h8i9",
|
||||
"summary": "Branch explored approach A..."
|
||||
}
|
||||
```
|
||||
|
||||
Optional fields:
|
||||
|
||||
- `details`: File tracking data (`{ readFiles: string[], modifiedFiles: string[] }`) for default, or custom data for extensions
|
||||
- `fromHook`: `true` if generated by an extension, `false`/`undefined` if pi-generated (legacy field name)
|
||||
|
||||
### CustomEntry
|
||||
|
||||
Extension state persistence. Does NOT participate in LLM context.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "custom",
|
||||
"id": "h8i9j0k1",
|
||||
"parentId": "g7h8i9j0",
|
||||
"timestamp": "2024-12-03T14:20:00.000Z",
|
||||
"customType": "my-extension",
|
||||
"data": { "count": 42 }
|
||||
}
|
||||
```
|
||||
|
||||
Use `customType` to identify your extension's entries on reload.
|
||||
|
||||
### CustomMessageEntry
|
||||
|
||||
Extension-injected messages that DO participate in LLM context.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "custom_message",
|
||||
"id": "i9j0k1l2",
|
||||
"parentId": "h8i9j0k1",
|
||||
"timestamp": "2024-12-03T14:25:00.000Z",
|
||||
"customType": "my-extension",
|
||||
"content": "Injected context...",
|
||||
"display": true
|
||||
}
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `content`: String or `(TextContent | ImageContent)[]` (same as UserMessage)
|
||||
- `display`: `true` = show in TUI with distinct styling, `false` = hidden
|
||||
- `details`: Optional extension-specific metadata (not sent to LLM)
|
||||
|
||||
### LabelEntry
|
||||
|
||||
User-defined bookmark/marker on an entry.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "label",
|
||||
"id": "j0k1l2m3",
|
||||
"parentId": "i9j0k1l2",
|
||||
"timestamp": "2024-12-03T14:30:00.000Z",
|
||||
"targetId": "a1b2c3d4",
|
||||
"label": "checkpoint-1"
|
||||
}
|
||||
```
|
||||
|
||||
Set `label` to `undefined` to clear a label.
|
||||
|
||||
### SessionInfoEntry
|
||||
|
||||
Session metadata (e.g., user-defined display name). Set via `/name` command or `pi.setSessionName()` in extensions.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "session_info",
|
||||
"id": "k1l2m3n4",
|
||||
"parentId": "j0k1l2m3",
|
||||
"timestamp": "2024-12-03T14:35:00.000Z",
|
||||
"name": "Refactor auth module"
|
||||
}
|
||||
```
|
||||
|
||||
The session name is displayed in the session selector (`/resume`) instead of the first message when set.
|
||||
|
||||
## Tree Structure
|
||||
|
||||
Entries form a tree:
|
||||
|
||||
- First entry has `parentId: null`
|
||||
- Each subsequent entry points to its parent via `parentId`
|
||||
- Branching creates new children from an earlier entry
|
||||
- The "leaf" is the current position in the tree
|
||||
|
||||
```
|
||||
[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf
|
||||
│
|
||||
└─ [branch_summary] ─── [user msg] ← alternate branch
|
||||
```
|
||||
|
||||
## Context Building
|
||||
|
||||
`buildSessionContext()` walks from the current leaf to the root, producing the message list for the LLM:
|
||||
|
||||
1. Collects all entries on the path
|
||||
2. Extracts current model and thinking level settings
|
||||
3. If a `CompactionEntry` is on the path:
|
||||
- Emits the summary first
|
||||
- Then messages from `firstKeptEntryId` to compaction
|
||||
- Then messages after compaction
|
||||
4. Converts `BranchSummaryEntry` and `CustomMessageEntry` to appropriate message formats
|
||||
|
||||
## Parsing Example
|
||||
|
||||
```typescript
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
const lines = readFileSync("session.jsonl", "utf8").trim().split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
const entry = JSON.parse(line);
|
||||
|
||||
switch (entry.type) {
|
||||
case "session":
|
||||
console.log(`Session v${entry.version ?? 1}: ${entry.id}`);
|
||||
break;
|
||||
case "message":
|
||||
console.log(
|
||||
`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`,
|
||||
);
|
||||
break;
|
||||
case "compaction":
|
||||
console.log(
|
||||
`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`,
|
||||
);
|
||||
break;
|
||||
case "branch_summary":
|
||||
console.log(`[${entry.id}] Branch from ${entry.fromId}`);
|
||||
break;
|
||||
case "custom":
|
||||
console.log(
|
||||
`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`,
|
||||
);
|
||||
break;
|
||||
case "custom_message":
|
||||
console.log(
|
||||
`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`,
|
||||
);
|
||||
break;
|
||||
case "label":
|
||||
console.log(`[${entry.id}] Label "${entry.label}" on ${entry.targetId}`);
|
||||
break;
|
||||
case "model_change":
|
||||
console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);
|
||||
break;
|
||||
case "thinking_level_change":
|
||||
console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SessionManager API
|
||||
|
||||
Key methods for working with sessions programmatically.
|
||||
|
||||
### Static Creation Methods
|
||||
|
||||
- `SessionManager.create(cwd, sessionDir?)` - New session
|
||||
- `SessionManager.open(path, sessionDir?)` - Open existing session file
|
||||
- `SessionManager.continueRecent(cwd, sessionDir?)` - Continue most recent or create new
|
||||
- `SessionManager.inMemory(cwd?)` - No file persistence
|
||||
- `SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)` - Fork session from another project
|
||||
|
||||
### Static Listing Methods
|
||||
|
||||
- `SessionManager.list(cwd, sessionDir?, onProgress?)` - List sessions for a directory
|
||||
- `SessionManager.listAll(onProgress?)` - List all sessions across all projects
|
||||
|
||||
### Instance Methods - Session Management
|
||||
|
||||
- `newSession(options?)` - Start a new session (options: `{ parentSession?: string }`)
|
||||
- `setSessionFile(path)` - Switch to a different session file
|
||||
- `createBranchedSession(leafId)` - Extract branch to new session file
|
||||
|
||||
### Instance Methods - Appending (all return entry ID)
|
||||
|
||||
- `appendMessage(message)` - Add message
|
||||
- `appendThinkingLevelChange(level)` - Record thinking change
|
||||
- `appendModelChange(provider, modelId)` - Record model change
|
||||
- `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` - Add compaction
|
||||
- `appendCustomEntry(customType, data?)` - Extension state (not in context)
|
||||
- `appendSessionInfo(name)` - Set session display name
|
||||
- `appendCustomMessageEntry(customType, content, display, details?)` - Extension message (in context)
|
||||
- `appendLabelChange(targetId, label)` - Set/clear label
|
||||
|
||||
### Instance Methods - Tree Navigation
|
||||
|
||||
- `getLeafId()` - Current position
|
||||
- `getLeafEntry()` - Get current leaf entry
|
||||
- `getEntry(id)` - Get entry by ID
|
||||
- `getBranch(fromId?)` - Walk from entry to root
|
||||
- `getTree()` - Get full tree structure
|
||||
- `getChildren(parentId)` - Get direct children
|
||||
- `getLabel(id)` - Get label for entry
|
||||
- `branch(entryId)` - Move leaf to earlier entry
|
||||
- `resetLeaf()` - Reset leaf to null (before any entries)
|
||||
- `branchWithSummary(entryId, summary, details?, fromHook?)` - Branch with context summary
|
||||
|
||||
### Instance Methods - Context & Info
|
||||
|
||||
- `buildSessionContext()` - Get messages, thinkingLevel, and model for LLM
|
||||
- `getEntries()` - All entries (excluding header)
|
||||
- `getHeader()` - Session header metadata
|
||||
- `getSessionName()` - Get display name from latest session_info entry
|
||||
- `getCwd()` - Working directory
|
||||
- `getSessionDir()` - Session storage directory
|
||||
- `getSessionId()` - Session UUID
|
||||
- `getSessionFile()` - Session file path (undefined for in-memory)
|
||||
- `isPersisted()` - Whether session is saved to disk
|
||||
225
packages/coding-agent/docs/settings.md
Normal file
225
packages/coding-agent/docs/settings.md
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
# Settings
|
||||
|
||||
Pi uses JSON settings files with project settings overriding global settings.
|
||||
|
||||
| Location | Scope |
|
||||
| --------------------------- | --------------------------- |
|
||||
| `~/.pi/agent/settings.json` | Global (all projects) |
|
||||
| `.pi/settings.json` | Project (current directory) |
|
||||
|
||||
Edit directly or use `/settings` for common options.
|
||||
|
||||
## All Settings
|
||||
|
||||
### Model & Thinking
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| ---------------------- | ------- | ------- | -------------------------------------------------------------- |
|
||||
| `defaultProvider` | string | - | Default provider (e.g., `"anthropic"`, `"openai"`) |
|
||||
| `defaultModel` | string | - | Default model ID |
|
||||
| `defaultThinkingLevel` | string | - | `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"` |
|
||||
| `hideThinkingBlock` | boolean | `false` | Hide thinking blocks in output |
|
||||
| `thinkingBudgets` | object | - | Custom token budgets per thinking level |
|
||||
|
||||
#### thinkingBudgets
|
||||
|
||||
```json
|
||||
{
|
||||
"thinkingBudgets": {
|
||||
"minimal": 1024,
|
||||
"low": 4096,
|
||||
"medium": 10240,
|
||||
"high": 32768
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UI & Display
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| ------------------------ | ------- | ----------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `theme` | string | `"dark"` | Theme name (`"dark"`, `"light"`, or custom) |
|
||||
| `quietStartup` | boolean | `false` | Hide startup header |
|
||||
| `collapseChangelog` | boolean | `false` | Show condensed changelog after updates |
|
||||
| `doubleEscapeAction` | string | `"tree"` | Action for double-escape: `"tree"`, `"fork"`, or `"none"` |
|
||||
| `treeFilterMode` | string | `"default"` | Default filter for `/tree`: `"default"`, `"no-tools"`, `"user-only"`, `"labeled-only"`, `"all"` |
|
||||
| `editorPaddingX` | number | `0` | Horizontal padding for input editor (0-3) |
|
||||
| `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) |
|
||||
| `showHardwareCursor` | boolean | `false` | Show terminal cursor |
|
||||
|
||||
### Compaction
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| ----------------------------- | ------- | ------- | -------------------------------------- |
|
||||
| `compaction.enabled` | boolean | `true` | Enable auto-compaction |
|
||||
| `compaction.reserveTokens` | number | `16384` | Tokens reserved for LLM response |
|
||||
| `compaction.keepRecentTokens` | number | `20000` | Recent tokens to keep (not summarized) |
|
||||
|
||||
```json
|
||||
{
|
||||
"compaction": {
|
||||
"enabled": true,
|
||||
"reserveTokens": 16384,
|
||||
"keepRecentTokens": 20000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Branch Summary
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| ----------------------------- | ------- | ------- | ------------------------------------------------------------------------------ |
|
||||
| `branchSummary.reserveTokens` | number | `16384` | Tokens reserved for branch summarization |
|
||||
| `branchSummary.skipPrompt` | boolean | `false` | Skip "Summarize branch?" prompt on `/tree` navigation (defaults to no summary) |
|
||||
|
||||
### Retry
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| ------------------- | ------- | ------- | ----------------------------------------------- |
|
||||
| `retry.enabled` | boolean | `true` | Enable automatic retry on transient errors |
|
||||
| `retry.maxRetries` | number | `3` | Maximum retry attempts |
|
||||
| `retry.baseDelayMs` | number | `2000` | Base delay for exponential backoff (2s, 4s, 8s) |
|
||||
| `retry.maxDelayMs` | number | `60000` | Max server-requested delay before failing (60s) |
|
||||
|
||||
When a provider requests a retry delay longer than `maxDelayMs` (e.g., Google's "quota will reset after 5h"), the request fails immediately with an informative error instead of waiting silently. Set to `0` to disable the cap.
|
||||
|
||||
```json
|
||||
{
|
||||
"retry": {
|
||||
"enabled": true,
|
||||
"maxRetries": 3,
|
||||
"baseDelayMs": 2000,
|
||||
"maxDelayMs": 60000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Message Delivery
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| -------------- | ------ | ----------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| `steeringMode` | string | `"one-at-a-time"` | How steering messages are sent: `"all"` or `"one-at-a-time"` |
|
||||
| `followUpMode` | string | `"one-at-a-time"` | How follow-up messages are sent: `"all"` or `"one-at-a-time"` |
|
||||
| `transport` | string | `"sse"` | Preferred transport for providers that support multiple transports: `"sse"`, `"websocket"`, or `"auto"` |
|
||||
|
||||
### Terminal & Images
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| ------------------------ | ------- | ------- | --------------------------------------------------------- |
|
||||
| `terminal.showImages` | boolean | `true` | Show images in terminal (if supported) |
|
||||
| `terminal.clearOnShrink` | boolean | `false` | Clear empty rows when content shrinks (can cause flicker) |
|
||||
| `images.autoResize` | boolean | `true` | Resize images to 2000x2000 max |
|
||||
| `images.blockImages` | boolean | `false` | Block all images from being sent to LLM |
|
||||
|
||||
### Shell
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| -------------------- | ------ | ------- | ----------------------------------------------------------------- |
|
||||
| `shellPath` | string | - | Custom shell path (e.g., for Cygwin on Windows) |
|
||||
| `shellCommandPrefix` | string | - | Prefix for every bash command (e.g., `"shopt -s expand_aliases"`) |
|
||||
|
||||
### Model Cycling
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| --------------- | -------- | ------- | ---------------------------------------------------------------------- |
|
||||
| `enabledModels` | string[] | - | Model patterns for Ctrl+P cycling (same format as `--models` CLI flag) |
|
||||
|
||||
```json
|
||||
{
|
||||
"enabledModels": ["claude-*", "gpt-4o", "gemini-2*"]
|
||||
}
|
||||
```
|
||||
|
||||
### Markdown
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| -------------------------- | ------ | ------- | --------------------------- |
|
||||
| `markdown.codeBlockIndent` | string | `" "` | Indentation for code blocks |
|
||||
|
||||
### Resources
|
||||
|
||||
These settings define where to load extensions, skills, prompts, and themes from.
|
||||
|
||||
Paths in `~/.pi/agent/settings.json` resolve relative to `~/.pi/agent`. Paths in `.pi/settings.json` resolve relative to `.pi`. Absolute paths and `~` are supported.
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| --------------------- | -------- | ------- | ------------------------------------------ |
|
||||
| `packages` | array | `[]` | npm/git packages to load resources from |
|
||||
| `extensions` | string[] | `[]` | Local extension file paths or directories |
|
||||
| `skills` | string[] | `[]` | Local skill file paths or directories |
|
||||
| `prompts` | string[] | `[]` | Local prompt template paths or directories |
|
||||
| `themes` | string[] | `[]` | Local theme file paths or directories |
|
||||
| `enableSkillCommands` | boolean | `true` | Register skills as `/skill:name` commands |
|
||||
|
||||
Arrays support glob patterns and exclusions. Use `!pattern` to exclude. Use `+path` to force-include an exact path and `-path` to force-exclude an exact path.
|
||||
|
||||
#### packages
|
||||
|
||||
String form loads all resources from a package:
|
||||
|
||||
```json
|
||||
{
|
||||
"packages": ["pi-skills", "@org/my-extension"]
|
||||
}
|
||||
```
|
||||
|
||||
Object form filters which resources to load:
|
||||
|
||||
```json
|
||||
{
|
||||
"packages": [
|
||||
{
|
||||
"source": "pi-skills",
|
||||
"skills": ["brave-search", "transcribe"],
|
||||
"extensions": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
See [packages.md](packages.md) for package management details.
|
||||
|
||||
## Example
|
||||
|
||||
```json
|
||||
{
|
||||
"defaultProvider": "anthropic",
|
||||
"defaultModel": "claude-sonnet-4-20250514",
|
||||
"defaultThinkingLevel": "medium",
|
||||
"theme": "dark",
|
||||
"compaction": {
|
||||
"enabled": true,
|
||||
"reserveTokens": 16384,
|
||||
"keepRecentTokens": 20000
|
||||
},
|
||||
"retry": {
|
||||
"enabled": true,
|
||||
"maxRetries": 3
|
||||
},
|
||||
"enabledModels": ["claude-*", "gpt-4o"],
|
||||
"packages": ["pi-skills"]
|
||||
}
|
||||
```
|
||||
|
||||
## Project Overrides
|
||||
|
||||
Project settings (`.pi/settings.json`) override global settings. Nested objects are merged:
|
||||
|
||||
```json
|
||||
// ~/.pi/agent/settings.json (global)
|
||||
{
|
||||
"theme": "dark",
|
||||
"compaction": { "enabled": true, "reserveTokens": 16384 }
|
||||
}
|
||||
|
||||
// .pi/settings.json (project)
|
||||
{
|
||||
"compaction": { "reserveTokens": 8192 }
|
||||
}
|
||||
|
||||
// Result
|
||||
{
|
||||
"theme": "dark",
|
||||
"compaction": { "enabled": true, "reserveTokens": 8192 }
|
||||
}
|
||||
```
|
||||
13
packages/coding-agent/docs/shell-aliases.md
Normal file
13
packages/coding-agent/docs/shell-aliases.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Shell Aliases
|
||||
|
||||
Pi runs bash in non-interactive mode (`bash -c`), which doesn't expand aliases by default.
|
||||
|
||||
To enable your shell aliases, add to `~/.pi/agent/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"shellCommandPrefix": "shopt -s expand_aliases\neval \"$(grep '^alias ' ~/.zshrc)\""
|
||||
}
|
||||
```
|
||||
|
||||
Adjust the path (`~/.zshrc`, `~/.bashrc`, etc.) to match your shell config.
|
||||
232
packages/coding-agent/docs/skills.md
Normal file
232
packages/coding-agent/docs/skills.md
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
> pi can create skills. Ask it to build one for your use case.
|
||||
|
||||
# Skills
|
||||
|
||||
Skills are self-contained capability packages that the agent loads on-demand. A skill provides specialized workflows, setup instructions, helper scripts, and reference documentation for specific tasks.
|
||||
|
||||
Pi implements the [Agent Skills standard](https://agentskills.io/specification), warning about violations but remaining lenient.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Locations](#locations)
|
||||
- [How Skills Work](#how-skills-work)
|
||||
- [Skill Commands](#skill-commands)
|
||||
- [Skill Structure](#skill-structure)
|
||||
- [Frontmatter](#frontmatter)
|
||||
- [Validation](#validation)
|
||||
- [Example](#example)
|
||||
- [Skill Repositories](#skill-repositories)
|
||||
|
||||
## Locations
|
||||
|
||||
> **Security:** Skills can instruct the model to perform any action and may include executable code the model invokes. Review skill content before use.
|
||||
|
||||
Pi loads skills from:
|
||||
|
||||
- Global:
|
||||
- `~/.pi/agent/skills/`
|
||||
- `~/.agents/skills/`
|
||||
- Project:
|
||||
- `.pi/skills/`
|
||||
- `.agents/skills/` in `cwd` and ancestor directories (up to git repo root, or filesystem root when not in a repo)
|
||||
- Packages: `skills/` directories or `pi.skills` entries in `package.json`
|
||||
- Settings: `skills` array with files or directories
|
||||
- CLI: `--skill <path>` (repeatable, additive even with `--no-skills`)
|
||||
|
||||
Discovery rules:
|
||||
|
||||
- Direct `.md` files in the skills directory root
|
||||
- Recursive `SKILL.md` files under subdirectories
|
||||
|
||||
Disable discovery with `--no-skills` (explicit `--skill` paths still load).
|
||||
|
||||
### Using Skills from Other Harnesses
|
||||
|
||||
To use skills from Claude Code or OpenAI Codex, add their directories to settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"skills": ["~/.claude/skills", "~/.codex/skills"]
|
||||
}
|
||||
```
|
||||
|
||||
For project-level Claude Code skills, add to `.pi/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"skills": ["../.claude/skills"]
|
||||
}
|
||||
```
|
||||
|
||||
## How Skills Work
|
||||
|
||||
1. At startup, pi scans skill locations and extracts names and descriptions
|
||||
2. The system prompt includes available skills in XML format per the [specification](https://agentskills.io/integrate-skills)
|
||||
3. When a task matches, the agent uses `read` to load the full SKILL.md (models don't always do this; use prompting or `/skill:name` to force it)
|
||||
4. The agent follows the instructions, using relative paths to reference scripts and assets
|
||||
|
||||
This is progressive disclosure: only descriptions are always in context, full instructions load on-demand.
|
||||
|
||||
## Skill Commands
|
||||
|
||||
Skills register as `/skill:name` commands:
|
||||
|
||||
```bash
|
||||
/skill:brave-search # Load and execute the skill
|
||||
/skill:pdf-tools extract # Load skill with arguments
|
||||
```
|
||||
|
||||
Arguments after the command are appended to the skill content as `User: <args>`.
|
||||
|
||||
Toggle skill commands via `/settings` in interactive mode or in `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"enableSkillCommands": true
|
||||
}
|
||||
```
|
||||
|
||||
## Skill Structure
|
||||
|
||||
A skill is a directory with a `SKILL.md` file. Everything else is freeform.
|
||||
|
||||
```
|
||||
my-skill/
|
||||
├── SKILL.md # Required: frontmatter + instructions
|
||||
├── scripts/ # Helper scripts
|
||||
│ └── process.sh
|
||||
├── references/ # Detailed docs loaded on-demand
|
||||
│ └── api-reference.md
|
||||
└── assets/
|
||||
└── template.json
|
||||
```
|
||||
|
||||
### SKILL.md Format
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-skill
|
||||
description: What this skill does and when to use it. Be specific.
|
||||
---
|
||||
|
||||
# My Skill
|
||||
|
||||
## Setup
|
||||
|
||||
Run once before first use:
|
||||
\`\`\`bash
|
||||
cd /path/to/skill && npm install
|
||||
\`\`\`
|
||||
|
||||
## Usage
|
||||
|
||||
\`\`\`bash
|
||||
./scripts/process.sh <input>
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
Use relative paths from the skill directory:
|
||||
|
||||
```markdown
|
||||
See [the reference guide](references/REFERENCE.md) for details.
|
||||
```
|
||||
|
||||
## Frontmatter
|
||||
|
||||
Per the [Agent Skills specification](https://agentskills.io/specification#frontmatter-required):
|
||||
|
||||
| Field | Required | Description |
|
||||
| -------------------------- | -------- | ------------------------------------------------------------------------------ |
|
||||
| `name` | Yes | Max 64 chars. Lowercase a-z, 0-9, hyphens. Must match parent directory. |
|
||||
| `description` | Yes | Max 1024 chars. What the skill does and when to use it. |
|
||||
| `license` | No | License name or reference to bundled file. |
|
||||
| `compatibility` | No | Max 500 chars. Environment requirements. |
|
||||
| `metadata` | No | Arbitrary key-value mapping. |
|
||||
| `allowed-tools` | No | Space-delimited list of pre-approved tools (experimental). |
|
||||
| `disable-model-invocation` | No | When `true`, skill is hidden from system prompt. Users must use `/skill:name`. |
|
||||
|
||||
### Name Rules
|
||||
|
||||
- 1-64 characters
|
||||
- Lowercase letters, numbers, hyphens only
|
||||
- No leading/trailing hyphens
|
||||
- No consecutive hyphens
|
||||
- Must match parent directory name
|
||||
|
||||
Valid: `pdf-processing`, `data-analysis`, `code-review`
|
||||
Invalid: `PDF-Processing`, `-pdf`, `pdf--processing`
|
||||
|
||||
### Description Best Practices
|
||||
|
||||
The description determines when the agent loads the skill. Be specific.
|
||||
|
||||
Good:
|
||||
|
||||
```yaml
|
||||
description: Extracts text and tables from PDF files, fills PDF forms, and merges multiple PDFs. Use when working with PDF documents.
|
||||
```
|
||||
|
||||
Poor:
|
||||
|
||||
```yaml
|
||||
description: Helps with PDFs.
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
Pi validates skills against the Agent Skills standard. Most issues produce warnings but still load the skill:
|
||||
|
||||
- Name doesn't match parent directory
|
||||
- Name exceeds 64 characters or contains invalid characters
|
||||
- Name starts/ends with hyphen or has consecutive hyphens
|
||||
- Description exceeds 1024 characters
|
||||
|
||||
Unknown frontmatter fields are ignored.
|
||||
|
||||
**Exception:** Skills with missing description are not loaded.
|
||||
|
||||
Name collisions (same name from different locations) warn and keep the first skill found.
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
brave-search/
|
||||
├── SKILL.md
|
||||
├── search.js
|
||||
└── content.js
|
||||
```
|
||||
|
||||
**SKILL.md:**
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: brave-search
|
||||
description: Web search and content extraction via Brave Search API. Use for searching documentation, facts, or any web content.
|
||||
---
|
||||
|
||||
# Brave Search
|
||||
|
||||
## Setup
|
||||
|
||||
\`\`\`bash
|
||||
cd /path/to/brave-search && npm install
|
||||
\`\`\`
|
||||
|
||||
## Search
|
||||
|
||||
\`\`\`bash
|
||||
./search.js "query" # Basic search
|
||||
./search.js "query" --content # Include page content
|
||||
\`\`\`
|
||||
|
||||
## Extract Page Content
|
||||
|
||||
\`\`\`bash
|
||||
./content.js https://example.com
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
## Skill Repositories
|
||||
|
||||
- [Anthropic Skills](https://github.com/anthropics/skills) - Document processing (docx, pdf, pptx, xlsx), web development
|
||||
- [Pi Skills](https://github.com/badlogic/pi-skills) - Web search, browser automation, Google APIs, transcription
|
||||
71
packages/coding-agent/docs/terminal-setup.md
Normal file
71
packages/coding-agent/docs/terminal-setup.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# Terminal Setup
|
||||
|
||||
Pi uses the [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) for reliable modifier key detection. Most modern terminals support this protocol, but some require configuration.
|
||||
|
||||
## Kitty, iTerm2
|
||||
|
||||
Work out of the box.
|
||||
|
||||
## Ghostty
|
||||
|
||||
Add to your Ghostty config (`~/.config/ghostty/config`):
|
||||
|
||||
```
|
||||
keybind = alt+backspace=text:\x1b\x7f
|
||||
keybind = shift+enter=text:\n
|
||||
```
|
||||
|
||||
## WezTerm
|
||||
|
||||
Create `~/.wezterm.lua`:
|
||||
|
||||
```lua
|
||||
local wezterm = require 'wezterm'
|
||||
local config = wezterm.config_builder()
|
||||
config.enable_kitty_keyboard = true
|
||||
return config
|
||||
```
|
||||
|
||||
## VS Code (Integrated Terminal)
|
||||
|
||||
`keybindings.json` locations:
|
||||
|
||||
- macOS: `~/Library/Application Support/Code/User/keybindings.json`
|
||||
- Linux: `~/.config/Code/User/keybindings.json`
|
||||
- Windows: `%APPDATA%\\Code\\User\\keybindings.json`
|
||||
|
||||
Add to `keybindings.json` to enable `Shift+Enter` for multi-line input:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "shift+enter",
|
||||
"command": "workbench.action.terminal.sendSequence",
|
||||
"args": { "text": "\u001b[13;2u" },
|
||||
"when": "terminalFocus"
|
||||
}
|
||||
```
|
||||
|
||||
## Windows Terminal
|
||||
|
||||
Add to `settings.json` (Ctrl+Shift+, or Settings → Open JSON file):
|
||||
|
||||
```json
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"command": { "action": "sendInput", "input": "\u001b[13;2u" },
|
||||
"keys": "shift+enter"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If you already have an `actions` array, add the object to it.
|
||||
|
||||
## IntelliJ IDEA (Integrated Terminal)
|
||||
|
||||
The built-in terminal has limited escape sequence support. Shift+Enter cannot be distinguished from Enter in IntelliJ's terminal.
|
||||
|
||||
If you want the hardware cursor visible, set `PI_HARDWARE_CURSOR=1` before running pi (disabled by default for compatibility).
|
||||
|
||||
Consider using a dedicated terminal emulator for the best experience.
|
||||
140
packages/coding-agent/docs/termux.md
Normal file
140
packages/coding-agent/docs/termux.md
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# Termux (Android) Setup
|
||||
|
||||
Pi runs on Android via [Termux](https://termux.dev/), a terminal emulator and Linux environment for Android.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Install [Termux](https://github.com/termux/termux-app#installation) from GitHub or F-Droid (not Google Play, that version is deprecated)
|
||||
2. Install [Termux:API](https://github.com/termux/termux-api#installation) from GitHub or F-Droid for clipboard and other device integrations
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Update packages
|
||||
pkg update && pkg upgrade
|
||||
|
||||
# Install dependencies
|
||||
pkg install nodejs termux-api git
|
||||
|
||||
# Install pi
|
||||
npm install -g @mariozechner/pi-coding-agent
|
||||
|
||||
# Create config directory
|
||||
mkdir -p ~/.pi/agent
|
||||
|
||||
# Run pi
|
||||
pi
|
||||
```
|
||||
|
||||
## Clipboard Support
|
||||
|
||||
Clipboard operations use `termux-clipboard-set` and `termux-clipboard-get` when running in Termux. The Termux:API app must be installed for these to work.
|
||||
|
||||
Image clipboard is not supported on Termux (the `ctrl+v` image paste feature will not work).
|
||||
|
||||
## Example AGENTS.md for Termux
|
||||
|
||||
Create `~/.pi/agent/AGENTS.md` to help the agent understand the Termux environment:
|
||||
|
||||
````markdown
|
||||
# Agent Environment: Termux on Android
|
||||
|
||||
## Location
|
||||
|
||||
- **OS**: Android (Termux terminal emulator)
|
||||
- **Home**: `/data/data/com.termux/files/home`
|
||||
- **Prefix**: `/data/data/com.termux/files/usr`
|
||||
- **Shared storage**: `/storage/emulated/0` (Downloads, Documents, etc.)
|
||||
|
||||
## Opening URLs
|
||||
|
||||
```bash
|
||||
termux-open-url "https://example.com"
|
||||
```
|
||||
````
|
||||
|
||||
## Opening Files
|
||||
|
||||
```bash
|
||||
termux-open file.pdf # Opens with default app
|
||||
termux-open -c image.jpg # Choose app
|
||||
```
|
||||
|
||||
## Clipboard
|
||||
|
||||
```bash
|
||||
termux-clipboard-set "text" # Copy
|
||||
termux-clipboard-get # Paste
|
||||
```
|
||||
|
||||
## Notifications
|
||||
|
||||
```bash
|
||||
termux-notification -t "Title" -c "Content"
|
||||
```
|
||||
|
||||
## Device Info
|
||||
|
||||
```bash
|
||||
termux-battery-status # Battery info
|
||||
termux-wifi-connectioninfo # WiFi info
|
||||
termux-telephony-deviceinfo # Device info
|
||||
```
|
||||
|
||||
## Sharing
|
||||
|
||||
```bash
|
||||
termux-share -a send file.txt # Share file
|
||||
```
|
||||
|
||||
## Other Useful Commands
|
||||
|
||||
```bash
|
||||
termux-toast "message" # Quick toast popup
|
||||
termux-vibrate # Vibrate device
|
||||
termux-tts-speak "hello" # Text to speech
|
||||
termux-camera-photo out.jpg # Take photo
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Termux:API app must be installed for `termux-*` commands
|
||||
- Use `pkg install termux-api` for the command-line tools
|
||||
- Storage permission needed for `/storage/emulated/0` access
|
||||
|
||||
````
|
||||
|
||||
## Limitations
|
||||
|
||||
- **No image clipboard**: Termux clipboard API only supports text
|
||||
- **No native binaries**: Some optional native dependencies (like the clipboard module) are unavailable on Android ARM64 and are skipped during installation
|
||||
- **Storage access**: To access files in `/storage/emulated/0` (Downloads, etc.), run `termux-setup-storage` once to grant permissions
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Clipboard not working
|
||||
|
||||
Ensure both apps are installed:
|
||||
1. Termux (from GitHub or F-Droid)
|
||||
2. Termux:API (from GitHub or F-Droid)
|
||||
|
||||
Then install the CLI tools:
|
||||
```bash
|
||||
pkg install termux-api
|
||||
````
|
||||
|
||||
### Permission denied for shared storage
|
||||
|
||||
Run once to grant storage permissions:
|
||||
|
||||
```bash
|
||||
termux-setup-storage
|
||||
```
|
||||
|
||||
### Node.js installation issues
|
||||
|
||||
If npm fails, try clearing the cache:
|
||||
|
||||
```bash
|
||||
npm cache clean --force
|
||||
```
|
||||
296
packages/coding-agent/docs/themes.md
Normal file
296
packages/coding-agent/docs/themes.md
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
> pi can create themes. Ask it to build one for your setup.
|
||||
|
||||
# Themes
|
||||
|
||||
Themes are JSON files that define colors for the TUI.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Locations](#locations)
|
||||
- [Selecting a Theme](#selecting-a-theme)
|
||||
- [Creating a Custom Theme](#creating-a-custom-theme)
|
||||
- [Theme Format](#theme-format)
|
||||
- [Color Tokens](#color-tokens)
|
||||
- [Color Values](#color-values)
|
||||
- [Tips](#tips)
|
||||
|
||||
## Locations
|
||||
|
||||
Pi loads themes from:
|
||||
|
||||
- Built-in: `dark`, `light`
|
||||
- Global: `~/.pi/agent/themes/*.json`
|
||||
- Project: `.pi/themes/*.json`
|
||||
- Packages: `themes/` directories or `pi.themes` entries in `package.json`
|
||||
- Settings: `themes` array with files or directories
|
||||
- CLI: `--theme <path>` (repeatable)
|
||||
|
||||
Disable discovery with `--no-themes`.
|
||||
|
||||
## Selecting a Theme
|
||||
|
||||
Select a theme via `/settings` or in `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"theme": "my-theme"
|
||||
}
|
||||
```
|
||||
|
||||
On first run, pi detects your terminal background and defaults to `dark` or `light`.
|
||||
|
||||
## Creating a Custom Theme
|
||||
|
||||
1. Create a theme file:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.pi/agent/themes
|
||||
vim ~/.pi/agent/themes/my-theme.json
|
||||
```
|
||||
|
||||
2. Define the theme with all required colors (see [Color Tokens](#color-tokens)):
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||
"name": "my-theme",
|
||||
"vars": {
|
||||
"primary": "#00aaff",
|
||||
"secondary": 242
|
||||
},
|
||||
"colors": {
|
||||
"accent": "primary",
|
||||
"border": "primary",
|
||||
"borderAccent": "#00ffff",
|
||||
"borderMuted": "secondary",
|
||||
"success": "#00ff00",
|
||||
"error": "#ff0000",
|
||||
"warning": "#ffff00",
|
||||
"muted": "secondary",
|
||||
"dim": 240,
|
||||
"text": "",
|
||||
"thinkingText": "secondary",
|
||||
"selectedBg": "#2d2d30",
|
||||
"userMessageBg": "#2d2d30",
|
||||
"userMessageText": "",
|
||||
"customMessageBg": "#2d2d30",
|
||||
"customMessageText": "",
|
||||
"customMessageLabel": "primary",
|
||||
"toolPendingBg": "#1e1e2e",
|
||||
"toolSuccessBg": "#1e2e1e",
|
||||
"toolErrorBg": "#2e1e1e",
|
||||
"toolTitle": "primary",
|
||||
"toolOutput": "",
|
||||
"mdHeading": "#ffaa00",
|
||||
"mdLink": "primary",
|
||||
"mdLinkUrl": "secondary",
|
||||
"mdCode": "#00ffff",
|
||||
"mdCodeBlock": "",
|
||||
"mdCodeBlockBorder": "secondary",
|
||||
"mdQuote": "secondary",
|
||||
"mdQuoteBorder": "secondary",
|
||||
"mdHr": "secondary",
|
||||
"mdListBullet": "#00ffff",
|
||||
"toolDiffAdded": "#00ff00",
|
||||
"toolDiffRemoved": "#ff0000",
|
||||
"toolDiffContext": "secondary",
|
||||
"syntaxComment": "secondary",
|
||||
"syntaxKeyword": "primary",
|
||||
"syntaxFunction": "#00aaff",
|
||||
"syntaxVariable": "#ffaa00",
|
||||
"syntaxString": "#00ff00",
|
||||
"syntaxNumber": "#ff00ff",
|
||||
"syntaxType": "#00aaff",
|
||||
"syntaxOperator": "primary",
|
||||
"syntaxPunctuation": "secondary",
|
||||
"thinkingOff": "secondary",
|
||||
"thinkingMinimal": "primary",
|
||||
"thinkingLow": "#00aaff",
|
||||
"thinkingMedium": "#00ffff",
|
||||
"thinkingHigh": "#ff00ff",
|
||||
"thinkingXhigh": "#ff0000",
|
||||
"bashMode": "#ffaa00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Select the theme via `/settings`.
|
||||
|
||||
**Hot reload:** When you edit the currently active custom theme file, pi reloads it automatically for immediate visual feedback.
|
||||
|
||||
## Theme Format
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||
"name": "my-theme",
|
||||
"vars": {
|
||||
"blue": "#0066cc",
|
||||
"gray": 242
|
||||
},
|
||||
"colors": {
|
||||
"accent": "blue",
|
||||
"muted": "gray",
|
||||
"text": "",
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `name` is required and must be unique.
|
||||
- `vars` is optional. Define reusable colors here, then reference them in `colors`.
|
||||
- `colors` must define all 51 required tokens.
|
||||
|
||||
The `$schema` field enables editor auto-completion and validation.
|
||||
|
||||
## Color Tokens
|
||||
|
||||
Every theme must define all 51 color tokens. There are no optional colors.
|
||||
|
||||
### Core UI (11 colors)
|
||||
|
||||
| Token | Purpose |
|
||||
| -------------- | --------------------------------------------- |
|
||||
| `accent` | Primary accent (logo, selected items, cursor) |
|
||||
| `border` | Normal borders |
|
||||
| `borderAccent` | Highlighted borders |
|
||||
| `borderMuted` | Subtle borders (editor) |
|
||||
| `success` | Success states |
|
||||
| `error` | Error states |
|
||||
| `warning` | Warning states |
|
||||
| `muted` | Secondary text |
|
||||
| `dim` | Tertiary text |
|
||||
| `text` | Default text (usually `""`) |
|
||||
| `thinkingText` | Thinking block text |
|
||||
|
||||
### Backgrounds & Content (11 colors)
|
||||
|
||||
| Token | Purpose |
|
||||
| -------------------- | ---------------------------- |
|
||||
| `selectedBg` | Selected line background |
|
||||
| `userMessageBg` | User message background |
|
||||
| `userMessageText` | User message text |
|
||||
| `customMessageBg` | Extension message background |
|
||||
| `customMessageText` | Extension message text |
|
||||
| `customMessageLabel` | Extension message label |
|
||||
| `toolPendingBg` | Tool box (pending) |
|
||||
| `toolSuccessBg` | Tool box (success) |
|
||||
| `toolErrorBg` | Tool box (error) |
|
||||
| `toolTitle` | Tool title |
|
||||
| `toolOutput` | Tool output text |
|
||||
|
||||
### Markdown (10 colors)
|
||||
|
||||
| Token | Purpose |
|
||||
| ------------------- | ------------------ |
|
||||
| `mdHeading` | Headings |
|
||||
| `mdLink` | Link text |
|
||||
| `mdLinkUrl` | Link URL |
|
||||
| `mdCode` | Inline code |
|
||||
| `mdCodeBlock` | Code block content |
|
||||
| `mdCodeBlockBorder` | Code block fences |
|
||||
| `mdQuote` | Blockquote text |
|
||||
| `mdQuoteBorder` | Blockquote border |
|
||||
| `mdHr` | Horizontal rule |
|
||||
| `mdListBullet` | List bullets |
|
||||
|
||||
### Tool Diffs (3 colors)
|
||||
|
||||
| Token | Purpose |
|
||||
| ----------------- | ------------- |
|
||||
| `toolDiffAdded` | Added lines |
|
||||
| `toolDiffRemoved` | Removed lines |
|
||||
| `toolDiffContext` | Context lines |
|
||||
|
||||
### Syntax Highlighting (9 colors)
|
||||
|
||||
| Token | Purpose |
|
||||
| ------------------- | -------------- |
|
||||
| `syntaxComment` | Comments |
|
||||
| `syntaxKeyword` | Keywords |
|
||||
| `syntaxFunction` | Function names |
|
||||
| `syntaxVariable` | Variables |
|
||||
| `syntaxString` | Strings |
|
||||
| `syntaxNumber` | Numbers |
|
||||
| `syntaxType` | Types |
|
||||
| `syntaxOperator` | Operators |
|
||||
| `syntaxPunctuation` | Punctuation |
|
||||
|
||||
### Thinking Level Borders (6 colors)
|
||||
|
||||
Editor border colors indicating thinking level (visual hierarchy from subtle to prominent):
|
||||
|
||||
| Token | Purpose |
|
||||
| ----------------- | ------------------- |
|
||||
| `thinkingOff` | Thinking off |
|
||||
| `thinkingMinimal` | Minimal thinking |
|
||||
| `thinkingLow` | Low thinking |
|
||||
| `thinkingMedium` | Medium thinking |
|
||||
| `thinkingHigh` | High thinking |
|
||||
| `thinkingXhigh` | Extra high thinking |
|
||||
|
||||
### Bash Mode (1 color)
|
||||
|
||||
| Token | Purpose |
|
||||
| ---------- | --------------------------------------- |
|
||||
| `bashMode` | Editor border in bash mode (`!` prefix) |
|
||||
|
||||
### HTML Export (optional)
|
||||
|
||||
The `export` section controls colors for `/export` HTML output. If omitted, colors are derived from `userMessageBg`.
|
||||
|
||||
```json
|
||||
{
|
||||
"export": {
|
||||
"pageBg": "#18181e",
|
||||
"cardBg": "#1e1e24",
|
||||
"infoBg": "#3c3728"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Color Values
|
||||
|
||||
Four formats are supported:
|
||||
|
||||
| Format | Example | Description |
|
||||
| --------- | ----------- | ------------------------------------- |
|
||||
| Hex | `"#ff0000"` | 6-digit hex RGB |
|
||||
| 256-color | `39` | xterm 256-color palette index (0-255) |
|
||||
| Variable | `"primary"` | Reference to a `vars` entry |
|
||||
| Default | `""` | Terminal's default color |
|
||||
|
||||
### 256-Color Palette
|
||||
|
||||
- `0-15`: Basic ANSI colors (terminal-dependent)
|
||||
- `16-231`: 6×6×6 RGB cube (`16 + 36×R + 6×G + B` where R,G,B are 0-5)
|
||||
- `232-255`: Grayscale ramp
|
||||
|
||||
### Terminal Compatibility
|
||||
|
||||
Pi uses 24-bit RGB colors. Most modern terminals support this (iTerm2, Kitty, WezTerm, Windows Terminal, VS Code). For older terminals with only 256-color support, pi falls back to the nearest approximation.
|
||||
|
||||
Check truecolor support:
|
||||
|
||||
```bash
|
||||
echo $COLORTERM # Should output "truecolor" or "24bit"
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
**Dark terminals:** Use bright, saturated colors with higher contrast.
|
||||
|
||||
**Light terminals:** Use darker, muted colors with lower contrast.
|
||||
|
||||
**Color harmony:** Start with a base palette (Nord, Gruvbox, Tokyo Night), define it in `vars`, and reference consistently.
|
||||
|
||||
**Testing:** Check your theme with different message types, tool states, markdown content, and long wrapped text.
|
||||
|
||||
**VS Code:** Set `terminal.integrated.minimumContrastRatio` to `1` for accurate colors.
|
||||
|
||||
## Examples
|
||||
|
||||
See the built-in themes:
|
||||
|
||||
- [dark.json](../src/modes/interactive/theme/dark.json)
|
||||
- [light.json](../src/modes/interactive/theme/light.json)
|
||||
229
packages/coding-agent/docs/tree.md
Normal file
229
packages/coding-agent/docs/tree.md
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
# Session Tree Navigation
|
||||
|
||||
The `/tree` command provides tree-based navigation of the session history.
|
||||
|
||||
## Overview
|
||||
|
||||
Sessions are stored as trees where each entry has an `id` and `parentId`. The "leaf" pointer tracks the current position. `/tree` lets you navigate to any point and optionally summarize the branch you're leaving.
|
||||
|
||||
### Comparison with `/fork`
|
||||
|
||||
| Feature | `/fork` | `/tree` |
|
||||
| ------- | -------------------------------------- | -------------------------------------- |
|
||||
| View | Flat list of user messages | Full tree structure |
|
||||
| Action | Extracts path to **new session file** | Changes leaf in **same session** |
|
||||
| Summary | Never | Optional (user prompted) |
|
||||
| Events | `session_before_fork` / `session_fork` | `session_before_tree` / `session_tree` |
|
||||
|
||||
## Tree UI
|
||||
|
||||
```
|
||||
├─ user: "Hello, can you help..."
|
||||
│ └─ assistant: "Of course! I can..."
|
||||
│ ├─ user: "Let's try approach A..."
|
||||
│ │ └─ assistant: "For approach A..."
|
||||
│ │ └─ [compaction: 12k tokens]
|
||||
│ │ └─ user: "That worked..." ← active
|
||||
│ └─ user: "Actually, approach B..."
|
||||
│ └─ assistant: "For approach B..."
|
||||
```
|
||||
|
||||
### Controls
|
||||
|
||||
| Key | Action |
|
||||
| ------------- | ------------------------------------------------- |
|
||||
| ↑/↓ | Navigate (depth-first order) |
|
||||
| Enter | Select node |
|
||||
| Escape/Ctrl+C | Cancel |
|
||||
| Ctrl+U | Toggle: user messages only |
|
||||
| Ctrl+O | Toggle: show all (including custom/label entries) |
|
||||
|
||||
### Display
|
||||
|
||||
- Height: half terminal height
|
||||
- Current leaf marked with `← active`
|
||||
- Labels shown inline: `[label-name]`
|
||||
- Default filter hides `label` and `custom` entries (shown in Ctrl+O mode)
|
||||
- Children sorted by timestamp (oldest first)
|
||||
|
||||
## Selection Behavior
|
||||
|
||||
### User Message or Custom Message
|
||||
|
||||
1. Leaf set to **parent** of selected node (or `null` if root)
|
||||
2. Message text placed in **editor** for re-submission
|
||||
3. User edits and submits, creating a new branch
|
||||
|
||||
### Non-User Message (assistant, compaction, etc.)
|
||||
|
||||
1. Leaf set to **selected node**
|
||||
2. Editor stays empty
|
||||
3. User continues from that point
|
||||
|
||||
### Selecting Root User Message
|
||||
|
||||
If user selects the very first message (has no parent):
|
||||
|
||||
1. Leaf reset to `null` (empty conversation)
|
||||
2. Message text placed in editor
|
||||
3. User effectively restarts from scratch
|
||||
|
||||
## Branch Summarization
|
||||
|
||||
When switching branches, user is presented with three options:
|
||||
|
||||
1. **No summary** - Switch immediately without summarizing
|
||||
2. **Summarize** - Generate a summary using the default prompt
|
||||
3. **Summarize with custom prompt** - Opens an editor to enter additional focus instructions that are appended to the default summarization prompt
|
||||
|
||||
### What Gets Summarized
|
||||
|
||||
Path from old leaf back to common ancestor with target:
|
||||
|
||||
```
|
||||
A → B → C → D → E → F ← old leaf
|
||||
↘ G → H ← target
|
||||
```
|
||||
|
||||
Abandoned path: D → E → F (summarized)
|
||||
|
||||
Summarization stops at:
|
||||
|
||||
1. Common ancestor (always)
|
||||
2. Compaction node (if encountered first)
|
||||
|
||||
### Summary Storage
|
||||
|
||||
Stored as `BranchSummaryEntry`:
|
||||
|
||||
```typescript
|
||||
interface BranchSummaryEntry {
|
||||
type: "branch_summary";
|
||||
id: string;
|
||||
parentId: string; // New leaf position
|
||||
timestamp: string;
|
||||
fromId: string; // Old leaf we abandoned
|
||||
summary: string; // LLM-generated summary
|
||||
details?: unknown; // Optional hook data
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
### AgentSession.navigateTree()
|
||||
|
||||
```typescript
|
||||
async navigateTree(
|
||||
targetId: string,
|
||||
options?: {
|
||||
summarize?: boolean;
|
||||
customInstructions?: string;
|
||||
replaceInstructions?: boolean;
|
||||
label?: string;
|
||||
}
|
||||
): Promise<{ editorText?: string; cancelled: boolean }>
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `summarize`: Whether to generate a summary of the abandoned branch
|
||||
- `customInstructions`: Custom instructions for the summarizer
|
||||
- `replaceInstructions`: If true, `customInstructions` replaces the default prompt instead of being appended
|
||||
- `label`: Label to attach to the branch summary entry (or target entry if not summarizing)
|
||||
|
||||
Flow:
|
||||
|
||||
1. Validate target, check no-op (target === current leaf)
|
||||
2. Find common ancestor between old leaf and target
|
||||
3. Collect entries to summarize (if requested)
|
||||
4. Fire `session_before_tree` event (hook can cancel or provide summary)
|
||||
5. Run default summarizer if needed
|
||||
6. Switch leaf via `branch()` or `branchWithSummary()`
|
||||
7. Update agent: `agent.replaceMessages(sessionManager.buildSessionContext().messages)`
|
||||
8. Fire `session_tree` event
|
||||
9. Notify custom tools via session event
|
||||
10. Return result with `editorText` if user message was selected
|
||||
|
||||
### SessionManager
|
||||
|
||||
- `getLeafUuid(): string | null` - Current leaf (null if empty)
|
||||
- `resetLeaf(): void` - Set leaf to null (for root user message navigation)
|
||||
- `getTree(): SessionTreeNode[]` - Full tree with children sorted by timestamp
|
||||
- `branch(id)` - Change leaf pointer
|
||||
- `branchWithSummary(id, summary)` - Change leaf and create summary entry
|
||||
|
||||
### InteractiveMode
|
||||
|
||||
`/tree` command shows `TreeSelectorComponent`, then:
|
||||
|
||||
1. Prompt for summarization
|
||||
2. Call `session.navigateTree()`
|
||||
3. Clear and re-render chat
|
||||
4. Set editor text if applicable
|
||||
|
||||
## Hook Events
|
||||
|
||||
### `session_before_tree`
|
||||
|
||||
```typescript
|
||||
interface TreePreparation {
|
||||
targetId: string;
|
||||
oldLeafId: string | null;
|
||||
commonAncestorId: string | null;
|
||||
entriesToSummarize: SessionEntry[];
|
||||
userWantsSummary: boolean;
|
||||
customInstructions?: string;
|
||||
replaceInstructions?: boolean;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface SessionBeforeTreeEvent {
|
||||
type: "session_before_tree";
|
||||
preparation: TreePreparation;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
interface SessionBeforeTreeResult {
|
||||
cancel?: boolean;
|
||||
summary?: { summary: string; details?: unknown };
|
||||
customInstructions?: string; // Override custom instructions
|
||||
replaceInstructions?: boolean; // Override replace mode
|
||||
label?: string; // Override label
|
||||
}
|
||||
```
|
||||
|
||||
Extensions can override `customInstructions`, `replaceInstructions`, and `label` by returning them from the `session_before_tree` handler.
|
||||
|
||||
### `session_tree`
|
||||
|
||||
```typescript
|
||||
interface SessionTreeEvent {
|
||||
type: "session_tree";
|
||||
newLeafId: string | null;
|
||||
oldLeafId: string | null;
|
||||
summaryEntry?: BranchSummaryEntry;
|
||||
fromHook?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Custom Summarizer
|
||||
|
||||
```typescript
|
||||
export default function (pi: HookAPI) {
|
||||
pi.on("session_before_tree", async (event, ctx) => {
|
||||
if (!event.preparation.userWantsSummary) return;
|
||||
if (event.preparation.entriesToSummarize.length === 0) return;
|
||||
|
||||
const summary = await myCustomSummarizer(
|
||||
event.preparation.entriesToSummarize,
|
||||
);
|
||||
return { summary: { summary, details: { custom: true } } };
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Summarization failure: cancels navigation, shows error
|
||||
- User abort (Escape): cancels navigation
|
||||
- Hook returns `cancel: true`: cancels navigation silently
|
||||
960
packages/coding-agent/docs/tui.md
Normal file
960
packages/coding-agent/docs/tui.md
Normal file
|
|
@ -0,0 +1,960 @@
|
|||
> pi can create TUI components. Ask it to build one for your use case.
|
||||
|
||||
# TUI Components
|
||||
|
||||
Extensions and custom tools can render custom TUI components for interactive user interfaces. This page covers the component system and available building blocks.
|
||||
|
||||
**Source:** [`@mariozechner/pi-tui`](https://github.com/badlogic/pi-mono/tree/main/packages/tui)
|
||||
|
||||
## Component Interface
|
||||
|
||||
All components implement:
|
||||
|
||||
```typescript
|
||||
interface Component {
|
||||
render(width: number): string[];
|
||||
handleInput?(data: string): void;
|
||||
wantsKeyRelease?: boolean;
|
||||
invalidate(): void;
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Description |
|
||||
| -------------------- | -------------------------------------------------------------------------------- |
|
||||
| `render(width)` | Return array of strings (one per line). Each line **must not exceed `width`**. |
|
||||
| `handleInput?(data)` | Receive keyboard input when component has focus. |
|
||||
| `wantsKeyRelease?` | If true, component receives key release events (Kitty protocol). Default: false. |
|
||||
| `invalidate()` | Clear cached render state. Called on theme changes. |
|
||||
|
||||
The TUI appends a full SGR reset and OSC 8 reset at the end of each rendered line. Styles do not carry across lines. If you emit multi-line text with styling, reapply styles per line or use `wrapTextWithAnsi()` so styles are preserved for each wrapped line.
|
||||
|
||||
## Focusable Interface (IME Support)
|
||||
|
||||
Components that display a text cursor and need IME (Input Method Editor) support should implement the `Focusable` interface:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
CURSOR_MARKER,
|
||||
type Component,
|
||||
type Focusable,
|
||||
} from "@mariozechner/pi-tui";
|
||||
|
||||
class MyInput implements Component, Focusable {
|
||||
focused: boolean = false; // Set by TUI when focus changes
|
||||
|
||||
render(width: number): string[] {
|
||||
const marker = this.focused ? CURSOR_MARKER : "";
|
||||
// Emit marker right before the fake cursor
|
||||
return [
|
||||
`> ${beforeCursor}${marker}\x1b[7m${atCursor}\x1b[27m${afterCursor}`,
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When a `Focusable` component has focus, TUI:
|
||||
|
||||
1. Sets `focused = true` on the component
|
||||
2. Scans rendered output for `CURSOR_MARKER` (a zero-width APC escape sequence)
|
||||
3. Positions the hardware terminal cursor at that location
|
||||
4. Shows the hardware cursor
|
||||
|
||||
This enables IME candidate windows to appear at the correct position for CJK input methods. The `Editor` and `Input` built-in components already implement this interface.
|
||||
|
||||
### Container Components with Embedded Inputs
|
||||
|
||||
When a container component (dialog, selector, etc.) contains an `Input` or `Editor` child, the container must implement `Focusable` and propagate the focus state to the child. Otherwise, the hardware cursor won't be positioned correctly for IME input.
|
||||
|
||||
```typescript
|
||||
import { Container, type Focusable, Input } from "@mariozechner/pi-tui";
|
||||
|
||||
class SearchDialog extends Container implements Focusable {
|
||||
private searchInput: Input;
|
||||
|
||||
// Focusable implementation - propagate to child input for IME cursor positioning
|
||||
private _focused = false;
|
||||
get focused(): boolean {
|
||||
return this._focused;
|
||||
}
|
||||
set focused(value: boolean) {
|
||||
this._focused = value;
|
||||
this.searchInput.focused = value;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.searchInput = new Input();
|
||||
this.addChild(this.searchInput);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Without this propagation, typing with an IME (Chinese, Japanese, Korean, etc.) will show the candidate window in the wrong position on screen.
|
||||
|
||||
## Using Components
|
||||
|
||||
**In extensions** via `ctx.ui.custom()`:
|
||||
|
||||
```typescript
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
const handle = ctx.ui.custom(myComponent);
|
||||
// handle.requestRender() - trigger re-render
|
||||
// handle.close() - restore normal UI
|
||||
});
|
||||
```
|
||||
|
||||
**In custom tools** via `pi.ui.custom()`:
|
||||
|
||||
```typescript
|
||||
async execute(toolCallId, params, onUpdate, ctx, signal) {
|
||||
const handle = pi.ui.custom(myComponent);
|
||||
// ...
|
||||
handle.close();
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
},
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Overlay Lifecycle
|
||||
|
||||
Overlay components are disposed when closed. Don't reuse references - create fresh instances:
|
||||
|
||||
```typescript
|
||||
// Wrong - stale reference
|
||||
let menu: MenuComponent;
|
||||
await ctx.ui.custom(
|
||||
(_, __, ___, done) => {
|
||||
menu = new MenuComponent(done);
|
||||
return menu;
|
||||
},
|
||||
{ overlay: true },
|
||||
);
|
||||
setActiveComponent(menu); // Disposed
|
||||
|
||||
// Correct - re-call to re-show
|
||||
const showMenu = () =>
|
||||
ctx.ui.custom((_, __, ___, done) => new MenuComponent(done), {
|
||||
overlay: true,
|
||||
});
|
||||
|
||||
await showMenu(); // First show
|
||||
await showMenu(); // "Back" = just call again
|
||||
```
|
||||
|
||||
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`:
|
||||
|
||||
```typescript
|
||||
import { Text, Box, Container, Spacer, Markdown } from "@mariozechner/pi-tui";
|
||||
```
|
||||
|
||||
### Text
|
||||
|
||||
Multi-line text with word wrapping.
|
||||
|
||||
```typescript
|
||||
const text = new Text(
|
||||
"Hello World", // content
|
||||
1, // paddingX (default: 1)
|
||||
1, // paddingY (default: 1)
|
||||
(s) => bgGray(s), // optional background function
|
||||
);
|
||||
text.setText("Updated");
|
||||
```
|
||||
|
||||
### Box
|
||||
|
||||
Container with padding and background color.
|
||||
|
||||
```typescript
|
||||
const box = new Box(
|
||||
1, // paddingX
|
||||
1, // paddingY
|
||||
(s) => bgGray(s), // background function
|
||||
);
|
||||
box.addChild(new Text("Content", 0, 0));
|
||||
box.setBgFn((s) => bgBlue(s));
|
||||
```
|
||||
|
||||
### Container
|
||||
|
||||
Groups child components vertically.
|
||||
|
||||
```typescript
|
||||
const container = new Container();
|
||||
container.addChild(component1);
|
||||
container.addChild(component2);
|
||||
container.removeChild(component1);
|
||||
```
|
||||
|
||||
### Spacer
|
||||
|
||||
Empty vertical space.
|
||||
|
||||
```typescript
|
||||
const spacer = new Spacer(2); // 2 empty lines
|
||||
```
|
||||
|
||||
### Markdown
|
||||
|
||||
Renders markdown with syntax highlighting.
|
||||
|
||||
```typescript
|
||||
const md = new Markdown(
|
||||
"# Title\n\nSome **bold** text",
|
||||
1, // paddingX
|
||||
1, // paddingY
|
||||
theme, // MarkdownTheme (see below)
|
||||
);
|
||||
md.setText("Updated markdown");
|
||||
```
|
||||
|
||||
### Image
|
||||
|
||||
Renders images in supported terminals (Kitty, iTerm2, Ghostty, WezTerm).
|
||||
|
||||
```typescript
|
||||
const image = new Image(
|
||||
base64Data, // base64-encoded image
|
||||
"image/png", // MIME type
|
||||
theme, // ImageTheme
|
||||
{ maxWidthCells: 80, maxHeightCells: 24 },
|
||||
);
|
||||
```
|
||||
|
||||
## Keyboard Input
|
||||
|
||||
Use `matchesKey()` for key detection:
|
||||
|
||||
```typescript
|
||||
import { matchesKey, Key } from "@mariozechner/pi-tui";
|
||||
|
||||
handleInput(data: string) {
|
||||
if (matchesKey(data, Key.up)) {
|
||||
this.selectedIndex--;
|
||||
} else if (matchesKey(data, Key.enter)) {
|
||||
this.onSelect?.(this.selectedIndex);
|
||||
} else if (matchesKey(data, Key.escape)) {
|
||||
this.onCancel?.();
|
||||
} else if (matchesKey(data, Key.ctrl("c"))) {
|
||||
// Ctrl+C
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key identifiers** (use `Key.*` for autocomplete, or string literals):
|
||||
|
||||
- Basic keys: `Key.enter`, `Key.escape`, `Key.tab`, `Key.space`, `Key.backspace`, `Key.delete`, `Key.home`, `Key.end`
|
||||
- Arrow keys: `Key.up`, `Key.down`, `Key.left`, `Key.right`
|
||||
- With modifiers: `Key.ctrl("c")`, `Key.shift("tab")`, `Key.alt("left")`, `Key.ctrlShift("p")`
|
||||
- String format also works: `"enter"`, `"ctrl+c"`, `"shift+tab"`, `"ctrl+shift+p"`
|
||||
|
||||
## Line Width
|
||||
|
||||
**Critical:** Each line from `render()` must not exceed the `width` parameter.
|
||||
|
||||
```typescript
|
||||
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
||||
|
||||
render(width: number): string[] {
|
||||
// Truncate long lines
|
||||
return [truncateToWidth(this.text, width)];
|
||||
}
|
||||
```
|
||||
|
||||
Utilities:
|
||||
|
||||
- `visibleWidth(str)` - Get display width (ignores ANSI codes)
|
||||
- `truncateToWidth(str, width, ellipsis?)` - Truncate with optional ellipsis
|
||||
- `wrapTextWithAnsi(str, width)` - Word wrap preserving ANSI codes
|
||||
|
||||
## Creating Custom Components
|
||||
|
||||
Example: Interactive selector
|
||||
|
||||
```typescript
|
||||
import {
|
||||
matchesKey,
|
||||
Key,
|
||||
truncateToWidth,
|
||||
visibleWidth,
|
||||
} from "@mariozechner/pi-tui";
|
||||
|
||||
class MySelector {
|
||||
private items: string[];
|
||||
private selected = 0;
|
||||
private cachedWidth?: number;
|
||||
private cachedLines?: string[];
|
||||
|
||||
public onSelect?: (item: string) => void;
|
||||
public onCancel?: () => void;
|
||||
|
||||
constructor(items: string[]) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
if (matchesKey(data, Key.up) && this.selected > 0) {
|
||||
this.selected--;
|
||||
this.invalidate();
|
||||
} else if (
|
||||
matchesKey(data, Key.down) &&
|
||||
this.selected < this.items.length - 1
|
||||
) {
|
||||
this.selected++;
|
||||
this.invalidate();
|
||||
} else if (matchesKey(data, Key.enter)) {
|
||||
this.onSelect?.(this.items[this.selected]);
|
||||
} else if (matchesKey(data, Key.escape)) {
|
||||
this.onCancel?.();
|
||||
}
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
if (this.cachedLines && this.cachedWidth === width) {
|
||||
return this.cachedLines;
|
||||
}
|
||||
|
||||
this.cachedLines = this.items.map((item, i) => {
|
||||
const prefix = i === this.selected ? "> " : " ";
|
||||
return truncateToWidth(prefix + item, width);
|
||||
});
|
||||
this.cachedWidth = width;
|
||||
return this.cachedLines;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
this.cachedWidth = undefined;
|
||||
this.cachedLines = undefined;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage in an extension:
|
||||
|
||||
```typescript
|
||||
pi.registerCommand("pick", {
|
||||
description: "Pick an item",
|
||||
handler: async (args, ctx) => {
|
||||
const items = ["Option A", "Option B", "Option C"];
|
||||
const selector = new MySelector(items);
|
||||
|
||||
let handle: { close: () => void; requestRender: () => void };
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
selector.onSelect = (item) => {
|
||||
ctx.ui.notify(`Selected: ${item}`, "info");
|
||||
handle.close();
|
||||
resolve();
|
||||
};
|
||||
selector.onCancel = () => {
|
||||
handle.close();
|
||||
resolve();
|
||||
};
|
||||
handle = ctx.ui.custom(selector);
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Theming
|
||||
|
||||
Components accept theme objects for styling.
|
||||
|
||||
**In `renderCall`/`renderResult`**, use the `theme` parameter:
|
||||
|
||||
```typescript
|
||||
renderResult(result, options, theme) {
|
||||
// Use theme.fg() for foreground colors
|
||||
return new Text(theme.fg("success", "Done!"), 0, 0);
|
||||
|
||||
// Use theme.bg() for background colors
|
||||
const styled = theme.bg("toolPendingBg", theme.fg("accent", "text"));
|
||||
}
|
||||
```
|
||||
|
||||
**Foreground colors** (`theme.fg(color, text)`):
|
||||
|
||||
| Category | Colors |
|
||||
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| General | `text`, `accent`, `muted`, `dim` |
|
||||
| Status | `success`, `error`, `warning` |
|
||||
| Borders | `border`, `borderAccent`, `borderMuted` |
|
||||
| Messages | `userMessageText`, `customMessageText`, `customMessageLabel` |
|
||||
| Tools | `toolTitle`, `toolOutput` |
|
||||
| Diffs | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` |
|
||||
| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` |
|
||||
| Syntax | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |
|
||||
| Thinking | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh` |
|
||||
| Modes | `bashMode` |
|
||||
|
||||
**Background colors** (`theme.bg(color, text)`):
|
||||
|
||||
`selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`
|
||||
|
||||
**For Markdown**, use `getMarkdownTheme()`:
|
||||
|
||||
```typescript
|
||||
import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
|
||||
import { Markdown } from "@mariozechner/pi-tui";
|
||||
|
||||
renderResult(result, options, theme) {
|
||||
const mdTheme = getMarkdownTheme();
|
||||
return new Markdown(result.details.markdown, 0, 0, mdTheme);
|
||||
}
|
||||
```
|
||||
|
||||
**For custom components**, define your own theme interface:
|
||||
|
||||
```typescript
|
||||
interface MyTheme {
|
||||
selected: (s: string) => string;
|
||||
normal: (s: string) => string;
|
||||
}
|
||||
```
|
||||
|
||||
## Debug logging
|
||||
|
||||
Set `PI_TUI_WRITE_LOG` to capture the raw ANSI stream written to stdout.
|
||||
|
||||
```bash
|
||||
PI_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx packages/tui/test/chat-simple.ts
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
Cache rendered output when possible:
|
||||
|
||||
```typescript
|
||||
class CachedComponent {
|
||||
private cachedWidth?: number;
|
||||
private cachedLines?: string[];
|
||||
|
||||
render(width: number): string[] {
|
||||
if (this.cachedLines && this.cachedWidth === width) {
|
||||
return this.cachedLines;
|
||||
}
|
||||
// ... compute lines ...
|
||||
this.cachedWidth = width;
|
||||
this.cachedLines = lines;
|
||||
return lines;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
this.cachedWidth = undefined;
|
||||
this.cachedLines = undefined;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Call `invalidate()` when state changes, then `handle.requestRender()` to trigger re-render.
|
||||
|
||||
## Invalidation and Theme Changes
|
||||
|
||||
When the theme changes, the TUI calls `invalidate()` on all components to clear their caches. Components must properly implement `invalidate()` to ensure theme changes take effect.
|
||||
|
||||
### The Problem
|
||||
|
||||
If a component pre-bakes theme colors into strings (via `theme.fg()`, `theme.bg()`, etc.) and caches them, the cached strings contain ANSI escape codes from the old theme. Simply clearing the render cache isn't enough if the component stores the themed content separately.
|
||||
|
||||
**Wrong approach** (theme colors won't update):
|
||||
|
||||
```typescript
|
||||
class BadComponent extends Container {
|
||||
private content: Text;
|
||||
|
||||
constructor(message: string, theme: Theme) {
|
||||
super();
|
||||
// Pre-baked theme colors stored in Text component
|
||||
this.content = new Text(theme.fg("accent", message), 1, 0);
|
||||
this.addChild(this.content);
|
||||
}
|
||||
// No invalidate override - parent's invalidate only clears
|
||||
// child render caches, not the pre-baked content
|
||||
}
|
||||
```
|
||||
|
||||
### The Solution
|
||||
|
||||
Components that build content with theme colors must rebuild that content when `invalidate()` is called:
|
||||
|
||||
```typescript
|
||||
class GoodComponent extends Container {
|
||||
private message: string;
|
||||
private content: Text;
|
||||
|
||||
constructor(message: string) {
|
||||
super();
|
||||
this.message = message;
|
||||
this.content = new Text("", 1, 0);
|
||||
this.addChild(this.content);
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
private updateDisplay(): void {
|
||||
// Rebuild content with current theme
|
||||
this.content.setText(theme.fg("accent", this.message));
|
||||
}
|
||||
|
||||
override invalidate(): void {
|
||||
super.invalidate(); // Clear child caches
|
||||
this.updateDisplay(); // Rebuild with new theme
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Rebuild on Invalidate
|
||||
|
||||
For components with complex content:
|
||||
|
||||
```typescript
|
||||
class ComplexComponent extends Container {
|
||||
private data: SomeData;
|
||||
|
||||
constructor(data: SomeData) {
|
||||
super();
|
||||
this.data = data;
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
private rebuild(): void {
|
||||
this.clear(); // Remove all children
|
||||
|
||||
// Build UI with current theme
|
||||
this.addChild(new Text(theme.fg("accent", theme.bold("Title")), 1, 0));
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
for (const item of this.data.items) {
|
||||
const color = item.active ? "success" : "muted";
|
||||
this.addChild(new Text(theme.fg(color, item.label), 1, 0));
|
||||
}
|
||||
}
|
||||
|
||||
override invalidate(): void {
|
||||
super.invalidate();
|
||||
this.rebuild();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### When This Matters
|
||||
|
||||
This pattern is needed when:
|
||||
|
||||
1. **Pre-baking theme colors** - Using `theme.fg()` or `theme.bg()` to create styled strings stored in child components
|
||||
2. **Syntax highlighting** - Using `highlightCode()` which applies theme-based syntax colors
|
||||
3. **Complex layouts** - Building child component trees that embed theme colors
|
||||
|
||||
This pattern is NOT needed when:
|
||||
|
||||
1. **Using theme callbacks** - Passing functions like `(text) => theme.fg("accent", text)` that are called during render
|
||||
2. **Simple containers** - Just grouping other components without adding themed content
|
||||
3. **Stateless render** - Computing themed output fresh in every `render()` call (no caching)
|
||||
|
||||
## Common Patterns
|
||||
|
||||
These patterns cover the most common UI needs in extensions. **Copy these patterns instead of building from scratch.**
|
||||
|
||||
### Pattern 1: Selection Dialog (SelectList)
|
||||
|
||||
For letting users pick from a list of options. Use `SelectList` from `@mariozechner/pi-tui` with `DynamicBorder` for framing.
|
||||
|
||||
```typescript
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
Container,
|
||||
type SelectItem,
|
||||
SelectList,
|
||||
Text,
|
||||
} from "@mariozechner/pi-tui";
|
||||
|
||||
pi.registerCommand("pick", {
|
||||
handler: async (_args, ctx) => {
|
||||
const items: SelectItem[] = [
|
||||
{ value: "opt1", label: "Option 1", description: "First option" },
|
||||
{ value: "opt2", label: "Option 2", description: "Second option" },
|
||||
{ value: "opt3", label: "Option 3" }, // description is optional
|
||||
];
|
||||
|
||||
const result = await ctx.ui.custom<string | null>(
|
||||
(tui, theme, _kb, done) => {
|
||||
const container = new Container();
|
||||
|
||||
// Top border
|
||||
container.addChild(
|
||||
new DynamicBorder((s: string) => theme.fg("accent", s)),
|
||||
);
|
||||
|
||||
// Title
|
||||
container.addChild(
|
||||
new Text(theme.fg("accent", theme.bold("Pick an Option")), 1, 0),
|
||||
);
|
||||
|
||||
// SelectList with theme
|
||||
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
||||
selectedPrefix: (t) => theme.fg("accent", t),
|
||||
selectedText: (t) => theme.fg("accent", t),
|
||||
description: (t) => theme.fg("muted", t),
|
||||
scrollInfo: (t) => theme.fg("dim", t),
|
||||
noMatch: (t) => theme.fg("warning", t),
|
||||
});
|
||||
selectList.onSelect = (item) => done(item.value);
|
||||
selectList.onCancel = () => done(null);
|
||||
container.addChild(selectList);
|
||||
|
||||
// Help text
|
||||
container.addChild(
|
||||
new Text(
|
||||
theme.fg("dim", "↑↓ navigate • enter select • esc cancel"),
|
||||
1,
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
// Bottom border
|
||||
container.addChild(
|
||||
new DynamicBorder((s: string) => theme.fg("accent", s)),
|
||||
);
|
||||
|
||||
return {
|
||||
render: (w) => container.render(w),
|
||||
invalidate: () => container.invalidate(),
|
||||
handleInput: (data) => {
|
||||
selectList.handleInput(data);
|
||||
tui.requestRender();
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
if (result) {
|
||||
ctx.ui.notify(`Selected: ${result}`, "info");
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Examples:** [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)
|
||||
|
||||
### Pattern 2: Async Operation with Cancel (BorderedLoader)
|
||||
|
||||
For operations that take time and should be cancellable. `BorderedLoader` shows a spinner and handles escape to cancel.
|
||||
|
||||
```typescript
|
||||
import { BorderedLoader } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
pi.registerCommand("fetch", {
|
||||
handler: async (_args, ctx) => {
|
||||
const result = await ctx.ui.custom<string | null>(
|
||||
(tui, theme, _kb, done) => {
|
||||
const loader = new BorderedLoader(tui, theme, "Fetching data...");
|
||||
loader.onAbort = () => done(null);
|
||||
|
||||
// Do async work
|
||||
fetchData(loader.signal)
|
||||
.then((data) => done(data))
|
||||
.catch(() => done(null));
|
||||
|
||||
return loader;
|
||||
},
|
||||
);
|
||||
|
||||
if (result === null) {
|
||||
ctx.ui.notify("Cancelled", "info");
|
||||
} else {
|
||||
ctx.ui.setEditorText(result);
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Examples:** [qna.ts](../examples/extensions/qna.ts), [handoff.ts](../examples/extensions/handoff.ts)
|
||||
|
||||
### Pattern 3: Settings/Toggles (SettingsList)
|
||||
|
||||
For toggling multiple settings. Use `SettingsList` from `@mariozechner/pi-tui` with `getSettingsListTheme()`.
|
||||
|
||||
```typescript
|
||||
import { getSettingsListTheme } from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
Container,
|
||||
type SettingItem,
|
||||
SettingsList,
|
||||
Text,
|
||||
} from "@mariozechner/pi-tui";
|
||||
|
||||
pi.registerCommand("settings", {
|
||||
handler: async (_args, ctx) => {
|
||||
const items: SettingItem[] = [
|
||||
{
|
||||
id: "verbose",
|
||||
label: "Verbose mode",
|
||||
currentValue: "off",
|
||||
values: ["on", "off"],
|
||||
},
|
||||
{
|
||||
id: "color",
|
||||
label: "Color output",
|
||||
currentValue: "on",
|
||||
values: ["on", "off"],
|
||||
},
|
||||
];
|
||||
|
||||
await ctx.ui.custom((_tui, theme, _kb, done) => {
|
||||
const container = new Container();
|
||||
container.addChild(
|
||||
new Text(theme.fg("accent", theme.bold("Settings")), 1, 1),
|
||||
);
|
||||
|
||||
const settingsList = new SettingsList(
|
||||
items,
|
||||
Math.min(items.length + 2, 15),
|
||||
getSettingsListTheme(),
|
||||
(id, newValue) => {
|
||||
// Handle value change
|
||||
ctx.ui.notify(`${id} = ${newValue}`, "info");
|
||||
},
|
||||
() => done(undefined), // On close
|
||||
{ enableSearch: true }, // Optional: enable fuzzy search by label
|
||||
);
|
||||
container.addChild(settingsList);
|
||||
|
||||
return {
|
||||
render: (w) => container.render(w),
|
||||
invalidate: () => container.invalidate(),
|
||||
handleInput: (data) => settingsList.handleInput?.(data),
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Examples:** [tools.ts](../examples/extensions/tools.ts)
|
||||
|
||||
### Pattern 4: Persistent Status Indicator
|
||||
|
||||
Show status in the footer that persists across renders. Good for mode indicators.
|
||||
|
||||
```typescript
|
||||
// Set status (shown in footer)
|
||||
ctx.ui.setStatus("my-ext", ctx.ui.theme.fg("accent", "● active"));
|
||||
|
||||
// Clear status
|
||||
ctx.ui.setStatus("my-ext", undefined);
|
||||
```
|
||||
|
||||
**Examples:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
|
||||
|
||||
### Pattern 5: Widgets Above/Below Editor
|
||||
|
||||
Show persistent content above or below the input editor. Good for todo lists, progress.
|
||||
|
||||
```typescript
|
||||
// Simple string array (above editor by default)
|
||||
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
|
||||
|
||||
// Render below the editor
|
||||
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], {
|
||||
placement: "belowEditor",
|
||||
});
|
||||
|
||||
// Or with theme
|
||||
ctx.ui.setWidget("my-widget", (_tui, theme) => {
|
||||
const lines = items.map((item, i) =>
|
||||
item.done
|
||||
? theme.fg("success", "✓ ") + theme.fg("muted", item.text)
|
||||
: theme.fg("dim", "○ ") + item.text,
|
||||
);
|
||||
return {
|
||||
render: () => lines,
|
||||
invalidate: () => {},
|
||||
};
|
||||
});
|
||||
|
||||
// Clear
|
||||
ctx.ui.setWidget("my-widget", undefined);
|
||||
```
|
||||
|
||||
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
||||
|
||||
### Pattern 6: Custom Footer
|
||||
|
||||
Replace the footer. `footerData` exposes data not otherwise accessible to extensions.
|
||||
|
||||
```typescript
|
||||
ctx.ui.setFooter((tui, theme, footerData) => ({
|
||||
invalidate() {},
|
||||
render(width: number): string[] {
|
||||
// footerData.getGitBranch(): string | null
|
||||
// footerData.getExtensionStatuses(): ReadonlyMap<string, string>
|
||||
return [`${ctx.model?.id} (${footerData.getGitBranch() || "no git"})`];
|
||||
},
|
||||
dispose: footerData.onBranchChange(() => tui.requestRender()), // reactive
|
||||
}));
|
||||
|
||||
ctx.ui.setFooter(undefined); // restore default
|
||||
```
|
||||
|
||||
Token stats available via `ctx.sessionManager.getBranch()` and `ctx.model`.
|
||||
|
||||
**Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts)
|
||||
|
||||
### Pattern 7: Custom Editor (vim mode, etc.)
|
||||
|
||||
Replace the main input editor with a custom implementation. Useful for modal editing (vim), different keybindings (emacs), or specialized input handling.
|
||||
|
||||
```typescript
|
||||
import { CustomEditor, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
||||
|
||||
type Mode = "normal" | "insert";
|
||||
|
||||
class VimEditor extends CustomEditor {
|
||||
private mode: Mode = "insert";
|
||||
|
||||
handleInput(data: string): void {
|
||||
// Escape: switch to normal mode, or pass through for app handling
|
||||
if (matchesKey(data, "escape")) {
|
||||
if (this.mode === "insert") {
|
||||
this.mode = "normal";
|
||||
return;
|
||||
}
|
||||
// In normal mode, escape aborts agent (handled by CustomEditor)
|
||||
super.handleInput(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert mode: pass everything to CustomEditor
|
||||
if (this.mode === "insert") {
|
||||
super.handleInput(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal mode: vim-style navigation
|
||||
switch (data) {
|
||||
case "i":
|
||||
this.mode = "insert";
|
||||
return;
|
||||
case "h":
|
||||
super.handleInput("\x1b[D");
|
||||
return; // Left
|
||||
case "j":
|
||||
super.handleInput("\x1b[B");
|
||||
return; // Down
|
||||
case "k":
|
||||
super.handleInput("\x1b[A");
|
||||
return; // Up
|
||||
case "l":
|
||||
super.handleInput("\x1b[C");
|
||||
return; // Right
|
||||
}
|
||||
// Pass unhandled keys to super (ctrl+c, etc.), but filter printable chars
|
||||
if (data.length === 1 && data.charCodeAt(0) >= 32) return;
|
||||
super.handleInput(data);
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
const lines = super.render(width);
|
||||
// Add mode indicator to bottom border (use truncateToWidth for ANSI-safe truncation)
|
||||
if (lines.length > 0) {
|
||||
const label = this.mode === "normal" ? " NORMAL " : " INSERT ";
|
||||
const lastLine = lines[lines.length - 1]!;
|
||||
// Pass "" as ellipsis to avoid adding "..." when truncating
|
||||
lines[lines.length - 1] =
|
||||
truncateToWidth(lastLine, width - label.length, "") + label;
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.on("session_start", (_event, ctx) => {
|
||||
// Factory receives theme and keybindings from the app
|
||||
ctx.ui.setEditorComponent(
|
||||
(tui, theme, keybindings) => new VimEditor(theme, keybindings),
|
||||
);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
|
||||
- **Extend `CustomEditor`** (not base `Editor`) to get app keybindings (escape to abort, ctrl+d to exit, model switching, etc.)
|
||||
- **Call `super.handleInput(data)`** for keys you don't handle
|
||||
- **Factory pattern**: `setEditorComponent` receives a factory function that gets `tui`, `theme`, and `keybindings`
|
||||
- **Pass `undefined`** to restore the default editor: `ctx.ui.setEditorComponent(undefined)`
|
||||
|
||||
**Examples:** [modal-editor.ts](../examples/extensions/modal-editor.ts)
|
||||
|
||||
## Key Rules
|
||||
|
||||
1. **Always use theme from callback** - Don't import theme directly. Use `theme` from the `ctx.ui.custom((tui, theme, keybindings, done) => ...)` callback.
|
||||
|
||||
2. **Always type DynamicBorder color param** - Write `(s: string) => theme.fg("accent", s)`, not `(s) => theme.fg("accent", s)`.
|
||||
|
||||
3. **Call tui.requestRender() after state changes** - In `handleInput`, call `tui.requestRender()` after updating state.
|
||||
|
||||
4. **Return the three-method object** - Custom components need `{ render, invalidate, handleInput }`.
|
||||
|
||||
5. **Use existing components** - `SelectList`, `SettingsList`, `BorderedLoader` cover 90% of cases. Don't rebuild them.
|
||||
|
||||
## Examples
|
||||
|
||||
- **Selection UI**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) - SelectList with DynamicBorder framing
|
||||
- **Async with cancel**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) - BorderedLoader for LLM calls
|
||||
- **Settings toggles**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - SettingsList for tool enable/disable
|
||||
- **Status indicators**: [examples/extensions/plan-mode.ts](../examples/extensions/plan-mode.ts) - setStatus and setWidget
|
||||
- **Custom footer**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - setFooter with stats
|
||||
- **Custom editor**: [examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) - Vim-like modal editing
|
||||
- **Snake game**: [examples/extensions/snake.ts](../examples/extensions/snake.ts) - Full game with keyboard input, game loop
|
||||
- **Custom tool rendering**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) - renderCall and renderResult
|
||||
17
packages/coding-agent/docs/windows.md
Normal file
17
packages/coding-agent/docs/windows.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Windows Setup
|
||||
|
||||
Pi requires a bash shell on Windows. Checked locations (in order):
|
||||
|
||||
1. Custom path from `~/.pi/agent/settings.json`
|
||||
2. Git Bash (`C:\Program Files\Git\bin\bash.exe`)
|
||||
3. `bash.exe` on PATH (Cygwin, MSYS2, WSL)
|
||||
|
||||
For most users, [Git for Windows](https://git-scm.com/download/win) is sufficient.
|
||||
|
||||
## Custom Shell Path
|
||||
|
||||
```json
|
||||
{
|
||||
"shellPath": "C:\\cygwin64\\bin\\bash.exe"
|
||||
}
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue