vastsa/PI-Desktop
Electron desktop agent running the pi engine in a Node sidecar, with every tool call gated and executed by a Rust host-core.
Overview
PI-Desktop is a desktop coding-agent app built around the pi agent engine (@earendil-works/pi-ai and pi-agent-core, pinned at 1.0.1). It is not a fork of pi’s CLI. It wraps the pi Agent in its own runtime and adds a desktop shell, a privileged Rust host process, plugins, MCP, scheduled tasks and a Plan mode. The product targets people who want a Codex-app-style GUI over a local agent with bring-your-own models.
The architecture is a strict three-process split, frozen in the project’s baseline spec. Electron main is a “thin orchestrator”. The agent loop runs in a Node sidecar, not in the renderer. A Rust binary, host-core, owns SQLite exclusively and executes every tool. The processes talk NDJSON JSON-RPC over stdio (docs/spec/00-baseline.md). The repository is also heavily governed. It has more than 340 ADRs, an AGENTS.md that sets an order of priorities for contributors, and a baseline spec with versioned decisions.
Architecture
flowchart LR
R["React renderer"] -->|"preload IPC"| M["Electron main"]
M -->|"stdio NDJSON"| S["pi agent sidecar (Node)"]
M -->|"stdio NDJSON"| H["host-core (Rust)"]
S --> RT["DesktopAgentRuntime"]
RT --> AG["pi Agent loop"]
AG --> PROV["pi-ai provider adapters"]
RT -->|"tools.execute via main"| H
H --> PERM["PermissionManager + tool budget"]
H --> TOOLS["Read/Glob/Grep/Write/Edit/Bash"]
H --> DB["SQLite"]
RT --> EXT["plugins, MCP, trusted extensions"]
PH["pi-host (headless, RACP-WS)"] --> S
| Component | Path | Role |
|---|---|---|
| Desktop shell | apps/desktop/ |
Electron main + preload + React/Tailwind renderer |
| Agent sidecar | packages/agent-runtime/src/sidecar.ts, runtime.ts |
Runs DesktopAgentRuntime around the pi Agent; tool schemas, compaction, subagents |
| Host core | crates/host-core/ |
Rust: tool execution, permissions, tool budgets, sessions, plugins, MCP registry, SQLite |
| Edit contract | crates/host-core/src/tools/hashline/ |
Line-anchored Edit with 4-hex whole-file tags |
| Shared contracts | packages/shared/ |
IPC/RPC types and error codes |
| Agent host | packages/agent-host/, host-runtime/, racp/ |
Headless host: admission, turn queue, approvals, RACP-WS server |
| Headless app | apps/pi-host/ |
pi-host CLI exposing the agent over RACP-WS |
| Plugin SDK | packages/plugin-sdk/, plugin-devkit/ |
.piplug manifest types and author tooling |
How a request flows
- Launch. Electron main starts the sidecar as its own executable with
ELECTRON_RUN_AS_NODE=1(agent-sidecar.ts). It also starts thepi-desktop-host-corebinary. The sidecar speaks stdio JSON-RPC to main, and every host call is proxied through main to the single host-core process (sidecar.ts). - Agent construction.
DesktopAgentRuntimebuilds a piAgentwith its ownstreamFn, abeforeToolCallgate andtoolExecution: "parallel". Every tool exceptTaskis declared sequential, so only a batch of pureTaskcalls actually runs concurrently (runtime.ts, L2170-L2185). - Prompt.
prompt()resets per-turn state and activates any MCP tools selected for this prompt. It compacts before sending if the incoming message would cross the threshold, and fails early withCONTEXT_TOO_LARGEif it still would not fit. It then runs extensionbefore_agent_starthooks and callsagent.prompt(...)(runtime.ts). - Turn boundaries. Between model requests,
prepareNextTurnWithoutExtensionsrecomputes the context budget. It either adds a budget reminder or runs a checkpoint compaction. If compaction fails at the hard limit, it throws instead of sending an oversized request (runtime.ts). - Tool call. Each tool’s
executeloads path-scoped instructions and, for Bash, subscribes totools.outputnotifications for streaming progress. It then calls host-core’stools.execute(runtime.ts). - Host-side checks. host-core verifies the Bash call targets the expected shell and dialect. It reads the session’s durable mode, registers a cancellation handle before the permission check (so
tools.abortcan cancel an approval wait), and then resolves the effective permission mode (rpc/mod.rs). - Decide and run.
PermissionManagerreturns allow, deny, or “ask the user”. On “ask”, a permission card is shown, and local approvals have no automatic timeout. The tool budget then admits the call, the tool runs, and the result flows back to the pi loop, which continues until the model stops.
Key components
Permission model
Risk is fixed per tool. Read/Glob/Grep are low. Write/Edit/Bash/GenerateImages are high. Plugin tools default to medium unless their manifest declares otherwise, and MCP tools are always medium because a server’s self-declared annotations are not trusted (permissions.rs). Plan and Goal modes have a hard allowlist (Read, Glob, Grep, Bash, BrowserPreview) that is checked before anything else. Outside those modes there are three permission modes. ask auto-allows only low-risk tools. accept-edits also allows Write/Edit. auto allows everything, including paths outside the workspace (permissions.rs).
Tool budgets
host-core caps concurrency: 16 tools in flight, 4 shells, 8 reads, 2 mutations (1 per session), 4 plugin calls, and a queue of 64 with a 30-second admission wait (tool_budget.rs). The sidecar also serialises Write/Edit on the same path with a PathMutex (runtime.ts).
Line-anchored Edit
Edit takes a tag and ops rather than old/new strings (runtime.ts). The tag is the low 16 bits of a SHA-256 over the normalised file, written as 4 hex digits (hashline/tag.rs). Read, Grep, Write and Edit all return it. The ops are PUT N.=M:, insert before or after a line, CUT, REM and MV. A stale tag fails the edit instead of patching the wrong text. Legacy old_string/new_string is still accepted.
Models
apiBindingForStyle maps a provider’s apiStyle to a pi-ai adapter: OpenAI Chat Completions (the default), OpenAI Responses, Anthropic Messages, the ChatGPT/Codex Responses endpoint, Pi, Google Gemini, and OpenCode Go (provider-binding.ts). The chat model catalog comes from models.dev, and thinking levels are normalised per model.
Subagents and compaction
The Task tool creates a SubagentRun with its own pi Agent, the same turn-boundary compaction, and sequential tool execution (subagent.ts). Session compaction defaults to a model-written summary. An environment variable switches it to fresh_window (runtime.ts).
Extending it
- Plugins.
.piplugzip packages with a manifest that declares a renderer entry, renderer actions, agent tools, skills, filesystem and network policy, and MCP servers (plugin-sdk/src/index.ts). Their tools appear asplugin_<id>_*and go through the same host-core permission gate. Samples live inexamples/plugins/. - MCP. User-configured stdio or HTTP servers. Tools are named
mcp_<server>_<tool>and can be selected per prompt. - Trusted extensions. These are compatible with pi-coding-agent’s extension layout, and the event list covers session, provider request/response, turn, message, tool, compaction and input events (event-capabilities.ts).
- Instructions. Path-scoped
AGENTS.md/CLAUDE.md, project memory, user skills and user-defined subagents.
Running it
- Prebuilt. Release builds cover macOS (arm64 and x64), Windows x64, and Linux (AppImage, deb, rpm).
- From source. Requires Node, pnpm 10+ and Rust. Run
pnpm install,cargo build -p host-core,pnpm build:js, thenpnpm dev. - Headless.
apps/pi-hostruns the same sidecar and host-core behind a RACP WebSocket server with device-token pairing. - Models. Bring your own key or account: API-key providers, GitHub Copilot, a ChatGPT subscription, Pi, or any OpenAI-compatible endpoint.
Strengths and caveats
- Strength: privilege separation. The model-facing code never touches the disk directly. Every tool call crosses into a Rust process that owns permissions, budgets and storage.
- Strength: precise editing. Tag-verified, line-anchored edits catch stale context instead of silently mis-applying a replacement.
- Strength: wide provider support. It supports seven wire styles through pi-ai, including subscription endpoints.
- Caveat: no OS sandbox. Bash runs as the user, with no sandbox, once permission is granted. In
automode no tool call outside Plan/Goal needs approval, including access outside the workspace. - Caveat: process and code weight. Three processes, an 8,800-line runtime file and hundreds of ADRs make the codebase hard to approach for outside contributors.
- Caveat: engine coupling. The loop semantics (steering,
finishTurn, tool execution modes) come from pinned pi packages, so pi upgrades are breaking-change events, which the repo tracks with contract tests.
Sources: code at d403c96, verified Q&A.
How it answers the Open-source coding agents questions
Each answer was drafted by a code-reading agent at commit d403c96. Its citations were checked mechanically. Compare with the other open-source coding agents →
How is the agent loop implemented?
answeredPI-Desktop has a turn-structured agent loop built on the pi-agent-core library's Agent class, not a separate Planner. The SessionRuntime class in packages/agent-runtime/src/runtime.ts wraps an Agent instance and drives the conversation turn by turn.
Turn structure — Each user message enters through SessionRuntime.prompt() (runtime.ts:8460), which appends user content, checks context budget (auto-compacting if needed), then calls this.agent.prompt(...) (runtime.ts:8555-8557). The agent streams the model response, collects tool calls, executes them sequentially via toolExecution: "sequential" (subagent.ts:296), and loops back for the next assistant turn via waitForIdle() + model .continue(). Stop conditions include: the model emits no tool call and a stop reason (stop/end_turn); the user aborts (abort(), runtime.ts:8735); a graceful stop is requested (requestGracefulStop(), runtime.ts:8751); or a silent/progress-only turn is auto-recovered once then stopped (SILENT_TURN_NUDGE, PROGRESS_TURN_NUDGE, runtime.ts:828-849).
Tool-call schema — Tools are declared to the model via JSON Schema using pi-ai's Type.Object() helpers. The core tools (Read, Write, Edit, Bash, Glob, Grep, ToolSearch, Task, TaskWait, etc.) are defined in runtime.ts:3154-3263 with schema parameters. The Edit tool uses a line-anchored format with tag, ops (PUT/CUT/REM/MV), and optional legacy old_string/new_string. Bash accepts command + optional timeout.
Sub-agents — The Task tool (SUBAGENT_TOOL_NAME, subagent.ts:83) spawns a SubagentRun class that creates its own Agent instance inside the same sidecar process (subagent.ts:278-298). Delegates have their own system prompt, tool set, and model (possibly pinned). They share the host connection so all permission checks go through host-core. The parent only sees the final report; intermediate messages are filtered out of model context (session-context.ts:33-41). TaskWait converges on running delegates, TaskList reports status, TaskStop stops them. Delegates are bounded: max captures, max tokens, and keep the parent from overflowing.
DesktopAgentRuntime, not SessionRuntime, and the session agent uses toolExecution: "parallel" with every tool except Task marked sequential (runtime.ts L2170-L2179); the sequential setting cited is the subagent's.How is repository context gathered and kept within the context window?
answeredContext is gathered from session history and managed via compaction. The SessionRuntime maintains a fullEntries array of Entry objects (message, compaction, branch_summary, custom) — the append-only transcript. On each turn, buildSessionContext() in session-context.ts:94-105 projects these entries into { messages: AgentMessage[] } for the model, slicing from the latest compaction entry forward (buildContextEntries(), session-context.ts:43-51).
Compaction — When the context budget is near its limit (automaticCompactionThresholdFor()), prepareNextTurn() (runtime.ts:6560-6609) triggers compaction before the next provider request. This runs a pi-based summary: earlier conversation is summarized into a compact text block, the retained tail (latest user messages, pending tool results) is preserved, and a checkpoint is persisted. The strategy defaults to summary (spends a model request to generate a structured summary) but can be set to fresh_window via env var (runtime.ts:887-894). Fallback paths exist when summary generation fails (COMPACTION_FALLBACK_MARKER, runtime.ts:701-704).
Repo maps and search — The system prompt sections include a runtime section (mode prompt, tool list, shell guidance, scratch dir) and tool activation state (system-transcript.ts:36-65). The model calls tools like Grep (regex search), Glob (file pattern matching), and Read (bounded file windows with line numbers) to gather context at runtime — there is no offline embedding or vector index.
Long sessions — Context compaction is the primary mechanism for keeping sessions within the window. The compaction entry replaces old messages with a summary plus a retained tail of recent messages. Delegation helps too: subagent runs produce a compact report rather than their full transcript. The context_budget system section reminds the model of remaining tokens (runtime.ts:938-958).
How are code edits applied?
answeredEdits use a line-anchored, tag-verified contract (ADR 0087). The Edit tool (runtime.ts:3215-3241) takes path, tag (4-hex fingerprint of whole file from the latest Read/Write/Edit/Grep), and ops — operation strings with +-prefixed body rows. Operations are: PUT N.=M: (replace lines N–M), PUT <N: / PUT >N: / PUT >$: (insert before/after/append), CUT N.=M (delete), REM (delete file), MV DEST (rename). Each successful write returns a new tag for the next edit. The legacy mode (old_string/new_string) is also still available (runtime.ts:3230-3241) but the line-anchored format is the primary contract.
Validation and recovery — Edits are validated at the host-core level (crates/host-core/src/tools/mod.rs). Error codes include EDIT_TAG_MISMATCH, EDIT_TAG_UNKNOWN, EDIT_LINES_UNSEEN, EDIT_PARSE_FAILED, EDIT_RANGE_INVALID, EDIT_NO_CHANGE (packages/shared/src/errors.ts:152-163). The RECOVERABLE_MUTATION_ERROR_CODES set (runtime.ts:403-407) defines which errors get one free retry: tag mismatch, unknown tag, and unseen lines. Each error code has specific advice sent in the tool result (mutationTerminationAdvice(), runtime.ts:409-430). After a failed Edit, the guidance says to re-read the live file and regenerate with fresh tag and narrower anchors.
Concurrency and safety — Host-core enforces tool budgets: max 2 in-flight mutations globally, max 1 per session, max 16 total in-flight tools (crates/host-core/src/tool_budget.rs:7-13). PATH_MUTATING_TOOLS in the runtime also uses a PathMutex to prevent concurrent Write/Edit on the same path (runtime.ts:747).
Write tool — Write is simpler: path + content, create or overwrite. No diffing, no line anchoring. Both Write and Edit go through the same host-core mutation path and the same permission framework.
How are shell commands and file writes kept safe?
answeredShell commands and file writes go through a multi-layer safety system.
Permission modes — The PermissionManager in crates/host-core/src/permissions.rs evaluates every tool call against an effective permission mode. Tools are classified by risk: Read/Glob/Grep = Low, Write/Edit/Bash = High, plugins = Medium (or declared), MCP = Medium (permissions.rs:120-137). The permission mode can be auto (auto-allow low-risk, prompt for high-risk), accept-edits (auto-allow Write/Edit only), or stricter modes that require user approval. Plan/Goal modes have an explicit allowlist (plan_mode_allows(), permissions.rs:144-148) that only admits Read, Glob, Grep, Bash, and BrowserPreview — never Write, Edit, plugins, or Task.
Tool budgets — crates/host-core/src/tool_budget.rs enforces concurrency caps: max 16 in-flight tools globally, 4 shell, 8 reads, 2 mutations, 4 plugins, 1 mutation per session, and a queue depth of 64. This prevents resource exhaustion and serializes dangerous operations.
Bash execution — Commands run through a resolved shell (POSIX, PowerShell, cmd) selected by the user (crates/host-core/src/tools/shell.rs). The CommandShellOption travels with the session. The runtime enforces a timeout clamp (1–21,600 seconds, runtime.ts:1242-1250) and detects patch commands (isPatchCommand(), runtime.ts:1123-1134) to redirect the model to use Edit/Write instead. External-path Bash calls require explicit permission (runtime.ts:3097-3098).
Network policy — Host-core mirrors the networkPolicy settings for LAN/enforcement decisions (network_policy.rs:1-51), supporting both strict mode (no insecure LAN endpoints) and relaxed mode.
Checkpoint and audit — The compaction system creates checkpoints before each model request, providing rollback points. The transcript is append-only. File mutations are serialized per-path. Plugin permissions are sandboxed via capabilities derived from the manifest.
auto permission mode allows every tool (including outside-workspace paths) without a prompt; ask auto-allows only low-risk tools and accept-edits additionally allows Write/Edit (crates/host-core/src/permissions.rs L231-L256).Which models are supported and how are they called?
answeredPI-Desktop supports multiple model providers through the pi-ai library, with a universal provider model (ADR 0012).
Wire protocols — provider-binding.ts:129-176 maps user-configured apiStyle values to pi-ai wire adapters: chat_completions (OpenAI-compatible), responses (OpenAI Responses), anthropic_messages (Claude), openai_codex_responses (ChatGPT/Codex), pi_messages (Pi service), google_generative_ai (Gemini), and opencode_go (OpenCode Go API). If no explicit style is set, it defaults to OpenAI Chat Completions (provider-binding.ts:170-175).
Model resolution — Providers are stored with apiStyle, baseUrl, modelId, apiKey, and optional modelConfig with per-model overrides. The apiBindingForProviderModel() function (provider-binding.ts:194-196) resolves the correct wire protocol considering both provider-level and model-level API styles. Vendor accounts (GitHub Copilot, Pi, custom) are handled with special auth paths including OAuth token refresh (provider-binding.ts:76).
Thinking/reasoning — Models can be configured with thinking levels via SessionThinkingLevel / SubagentThinkingLevel. The thinking-level.ts module clamps and normalizes these. Claude models with adaptive thinking vs. budget-token thinking are handled differently (provider-binding.ts:298+). DeepSeek Flash gets special tool-declaration optimizations (fixed-tool-declarations.ts:29-42).
Cost tracking — Usage is tracked via MessageUsage objects accumulating tokens and costs per turn, surfaced through request-usage.ts. The provider-retry.ts module handles rate limits (up to 5 retries) and transient errors (up to 3 retries) at the provider level.
Subagent models — Delegations can pin their own model via SubagentDefinition. Electron main resolves credentials; subagent model keys must be explicitly opted-in for Task.model overrides via subagentModelKeys (runtime.ts:1037-1039). Missing or failed model bindings fall through a fallback chain.
How can it be extended and customised?
answeredPI-Desktop has four extension systems.
1. Plugins — Full plugin system with a PluginManifest (packages/plugin-sdk/src/index.ts:48-78+) declaring id, version, renderer entry, agentTools (tools the model can call), skills, fsPolicy, netPolicy, and mcpServers. Plugins are installed via the marketplace or local .piplug files. Host-core manages plugin lifecycle — install, resolve, validate, permission derivation, provider reconciliation — in crates/host-core/src/plugins.rs. Plugin tools are prefixed plugin_<id>_ and go through the same host-core permission path. The examples/plugins/ directory contains sample plugins. Plugin development CLI is in packages/plugin-devkit/.
2. MCP (Model Context Protocol) — User-configured MCP servers (apps/desktop/electron/main/user-mcp.ts) connect to external services via stdio or HTTP. Their tools are exposed as mcp_<serverId>_<tool> names. The runtime supports OAuth-based auth, connection caching per workspace, and timeout configuration. The McpCallRegistry tracks active calls. MCP tool selection can be per-prompt via mcpServerIds/mcpToolNames (mcp-tool-selection.ts:20-48). Plugins can also declare MCP servers in their manifest, which are resolved via the plugin-MCP bridge.
3. Trusted Extensions — Full-featured extension API (packages/agent-runtime/src/extensions/runner.ts) exposing lifecycle hooks (before_agent_start, before_provider_request, after_provider_response, agent_settled, etc.), custom tools, commands, UI requests, diagnostics, and model configuration. The ExtensionAPI object is built per session over a TrustedExtensionBridge (agent-sidecar.ts:82-89). Currently supported events are listed in event-capabilities.ts.
4. Instruction files — Per-project AGENTS.md and CLAUDE.md files (the repo itself is a showcase). The session runtime resolves path-scoped instructions from these files (project-instructions.ts), creates prompt sections from them, and includes them in the system prompt. Project memory is also supported via projectMemoryPrompt (project-memory-prompt.ts).