Skip to content

Latest commit

 

History

History
182 lines (148 loc) · 11.7 KB

ARCHITECTURE.md

File metadata and controls

182 lines (148 loc) · 11.7 KB

Architecture

🗂️ The repository layout

Every top-level folder is one role, and the boundaries between them are what keeps upstream merges cheap and the product layers replaceable:

Path Role
picode-source/ Layer 1 — the editor. PiCode's own source tree: the VS Code-derived editor with the PiCode changes and identity committed in its history. Upstream merges land here, and only here.
distribution/ Layer 4 — the product as data. The product delta (the single home of identity and version), first-run defaults, the pinned pi runtime and the brand assets.
dev/ The build engine: the five-phase build, staging, the update feed, the pi runtime installer.
builder/ The C#/WinUI 3 front-end that drives the same pipeline with one button.
upstream/ The pin file: the VS Code version the tree descends from.
docs/ Internal papers: this document, distribution, CI, decisions, the task board.
assets/ Brand images of the repository itself.
updates/ The static update feeds, one tree per channel (layout in updates/README.md).
cloud/ The PiCode Account and cloud-sync API (ADR-017).
.github/workflows/ CI and release pipelines (CI.md).

Nothing of ours lives inside picode-source/ except code the editor itself runs: the PiCode changes to the editor, and beside them the durable agent (picode-source/durable, its chat bridge in picode-source/durable-bridge and its ACP proof client in picode-source/durable-acp-client), which the editor carries and spawns as its own program. The identity, defaults and release data stay outside the tree, applied onto it — that is why bringing a newer VS Code is a merge against picode-source/ and nothing else has to move.

🧱 The layered model

These are the architecture layers. The build layers in DISTRIBUTION.md §2 and the design layers in DISENO.md slice the same product differently.

┌──────────────────────────────────────────────────────────────────┐
│ 4. Experience layer   the product delta, first-run defaults,     │
│                       the portable profile, the update feed      │
├──────────────────────────────────────────────────────────────────┤
│ 3. Agent layer        the native chat, modified in the core,     │
│                       speaking to pi (in-process SDK + CLI)      │
├──────────────────────────────────────────────────────────────────┤
│ 2. Extension layer    the agent runtime (pi), the                │
│                       trimmed built-in extensions, Open VSX      │
├──────────────────────────────────────────────────────────────────┤
│ 1. Editor layer       the owned source tree in picode-source/    │
└──────────────────────────────────────────────────────────────────┘

Layer 1 — Editor: the owned source tree

picode-source/ is not a vendored archive: it is PiCode's source, versioned in this repository. The packer collects every extension under picode-source/extensions/, the identity is applied to picode-source/product.json before compilation, and the checksums are computed over the result — so a behaviour change is written in TypeScript, in the tree itself, never patched onto a minified bundle.

The build is dev/build.sh in five phases: prepare (source check, identity, dependencies), the connector, compile, pack, stage. builder/ drives the same pipeline through a GUI. CI (.github/workflows/ci.yml) builds on push; the release workflow reads the version from distribution/product-delta.json before tagging.

The lightweight cut (2026-09-29) is a standing property of this layer, not a one-off: the product ships what an agent-driven code editor needs, nothing else. Ten extension folders were removed (the four test-only extensions, mermaid preview, the notebook pair, Microsoft/GitHub authentication and the GitHub extension), ESLint was removed from the toolchain, and regenerable artefacts stay out of the tree. Removing a folder under extensions/ removes it from the product — the packer has no exclusion list.

Two properties of the layer shape everything above it:

  • Product keys can be deleted, not just overridden. That is what makes the removal of Copilot and the telemetry endpoints real rather than cosmetic.
  • resources/app/extensions/ is the built-in scan path. Built-ins need no install step and no compiled-in extension list.

Layer 2 — Extensions

  • The agent runtime: pi is pinned in distribution/runtime.json and installed into the pack by dev/pi-runtime.sh. It is an executable the agent layer talks to, decoupled from the editor so it updates independently. Which one runs is the owner's choice (picode.pi.runtime).
  • The built-in extensions: the trimmed set the source tree ships (language colorizers for every language, git, TypeScript, JSON/HTML/CSS language features, markdown preview without mermaid, emmet, themes, the picode connector).
  • Anything else the owner installs comes from Open VSX — the Microsoft marketplace is not licensed for third-party builds.

Layer 3 — Agent: the native chat in the core, pi driven by the built-in connector

The first agent surface was a standalone extension (extensions/picode-pi-chat); it was retired on 2026-09-27 and its replacement is the native chat modified in the core (src/vs/workbench/contrib/chat/** carries the PiCode changes: the model selector, the removal of the Copilot paywall, sessions). The editor's multi-agent host (src/vs/platform/agentHost/) exists in the tree, but PiCode's own bridge to it (node/pi/) is prepared and not wired yet (see HARNESS.md, Part 1 §5). Meanwhile picode-source/extensions/picode — the connector, compiled by phase 2 — contributes the providers and models surface.

Who runs the chat (ADR-020). The chat's agent is pi, in process: agent.ts registers the @pi chat participant, which owns the model, the agent profile and the tools, and streams the answer into the panel. durable is the backend, not a harness: pi reaches it through its own tools (the durable_send / durable_list / durable_read bridge in picode-source/durable-bridge, plus MCP), and durable keeps in its own SQLite the conversations it is handed — every session, per ADR-021. The durable chatSessions contribution stays declared but disabled ("when": "false"): a chatSessions type is a session provider, and the CHAT panel runs agents — selecting it made the panel call invokeAgent('durable'), which has no implementation, and the chat answered No activated agent with id "durable". The panel's harness is pi's own, and Local is its session.

pi is reached two ways. The in-process SDK owns the chat's session: piSdk.ts loads @earendil-works/pi-coding-agent at runtime (createAgentSessionServices / createAgentSessionFromServices) inside the connector's process, so the chat, its tools and its event stream are all in-process; the same SDK serves providers, models, login and packages. The CLI of the active runtime covers what the SDK does not expose — package management (pi install, remove, update, list).

The editor travels with every turn. editor-context.ts reads the folder (or the area), the open file, the selection, its errors and warnings, and the message's own references, and context.ts writes them into the prompt. Both the in-process chat and the durable turn go through it, so the agent answers about the screen the owner is looking at (ADR-019).

Until October 2026 the integration used pi --mode rpc as a child process (ADR-003, with ADR-005/006 for its framing); ADR-014 supersedes all three. The RPC framing below is kept as history — the payloads are still pi's, and the connector now consumes them as typed SDK events.

Events (agent → host): agent_start, agent_end, agent_settled, turn_start/turn_end, message_start/message_end, message_update, tool_execution_*, queue_update, compaction_*, auto_retry_*, extension_error.

message_update is delta-only: the client assembles live text from contentIndex + delta and treats message_end as the authoritative final state. Usage is per message, never a total: tokens and cost arrive on each assistant message, the panel sums them from message_end records, and the context size is the last turn's figure rather than a sum, because a window's fullness is not cumulative. set_model takes both halves of the reference (provider + modelId); a bare model field is rejected (ADR-007), and the reasoning picker offers what the runtime reports rather than the full enum.

🗄️ Superseded — the RPC contract (ADR-003, superseded by ADR-014)

Framing (strict). Records are delimited by LF (\n) only. Unicode line separators (U+2028, U+2029) are legal inside JSON strings, so Node's readline was not protocol-compliant and was not used: the client split the byte stream itself.

Commands (host → agent): prompt (with streamingBehavior when busy), abort, new_session, get_state, get_commands, get_available_models/set_model/ cycle_model, set_thinking_level/cycle_thinking_level/ get_available_thinking_levels.

Layer 4 — Experience

distribution/product-delta.json carries the identity and version — one home, applied by the build so the compiled editor and the released one cannot disagree. distribution/settings.json are the first-run defaults, copied only when the user has no settings of their own. The profile is portable and disposable. The updater reads the static feed produced by dev/update-feed.mjs (layout in updates/README.md) from the build's own channel branch, sealed into product.json by dev/build.sh; each channel reads only its own feed (ADR-015).

🧪 Testing

The connector (extensions/picode) keeps hermetic suites for the parts that can be wrong without an editor: the runtime resolver, the pi list parser, the usage arithmetic, the context block. The editor's own behaviour is verified by reading what it writes — the log directory it chose, the extension activation line, the output channel — because a passing unit test says nothing about whether the editor loads the result.

🌐 Language

All user-facing copy is English: the editor, the chat, notifications, command titles, setting descriptions and the labels shown for pi's tools. Code, comments, commit messages and diagnostic output stay English as well. The conversation with the owner is Spanish, and so are the records that are his — AGENTS.md (his own words) and the papers in docs/. Other languages are reached later through language packs, a separate layer over the shipped text, never a translation of the sources in place (ADR-012).

🚫 Non-goals

  • Patching a minified core: changes are made in the source tree and compiled.
  • A custom language server for pi.
  • Implementing our own agent loop: pi owns agent behaviour, PiCode owns the surface around it.
  • Shipping weight without a user-facing reason: the lightweight cut is doctrine, and a folder under extensions/ needs a justification to exist.