Skip to content

Latest commit

 

History

History
220 lines (153 loc) · 20.2 KB

File metadata and controls

220 lines (153 loc) · 20.2 KB

Pylon

Pylon is an open source GUI for coding agents, forked from T3 Code. A Node WebSocket server wraps provider CLIs (Codex, Claude Code, Cursor, Grok, OpenCode) and serves web, desktop, and mobile clients.

You can think of Pylon as a "bring-your-own-subscription" alternative to apps like Claude Desktop, Codex App, Cursor Glass, and Conductor.

Compatibility names

Pylon still retains upstream compatibility identifiers such as .t3, T3CODE_HOME, t3.json, npx t3, @t3tools/*, com.t3tools.*, and some T3-named source files. Treat those as implementation details, not product copy. Do not rename compatibility identifiers during branding or UI work unless the developer explicitly expands the scope and the migration is handled across every client and connection mode.

The desktop product identity is deliberately independent from T3 Code so both apps can be installed and run at the same time. Preserve these Pylon-owned boundaries when adopting upstream desktop work:

  • macOS/Windows application ID: com.pylon.code (com.pylon.code.dev.* for local development);
  • renderer protocols: pylon-code:// and pylon-code-dev://;
  • runtime home: ~/.pylon-code unless explicitly overridden;
  • Electron profiles: pylon-code and pylon-code-dev;
  • Linux executable/registration: pylon, pylon-code.desktop, and pylon-code WM class;
  • packaged app and artifacts: Pylon (Alpha) / Pylon (Nightly) and Pylon-*.

T3-named environment variables may still be passed to the bundled compatibility server. They are not permission to point Pylon at T3's default runtime or Electron data directories.

Fork direction and source control

Pylon is a long-lived independent product, not a temporary reskin or a patch queue intended to collapse back into T3 Code. Build in Pylon's direction while preserving the upstream qualities described below.

  • The canonical repository is the public pylon-code/pylon repository. Its writable remote is origin, and its default product branch is pylon. It moved from rynfar/pylon to the pylon-code organization; GitHub redirects the old path, but use the organization path.
  • Base Pylon work on pylon or a task branch created from it. Do not treat the inherited main branch as Pylon's product branch.
  • t3code-upstream (pingdotgg/t3code) and t3code-fork (rynfar/t3code) are reference remotes. They are intentionally fetch-only. Never push to them or re-enable their push URLs.
  • Upstream changes are opt-in. Fetch and inspect upstream commits, then cherry-pick or selectively merge only changes that benefit Pylon. Do not hard-reset, wholesale rebase, or replace Pylon with an upstream branch.
  • Use the review-t3-upstream skill whenever the user asks what changed in T3, whether Pylon should update, or wants to evaluate or adopt upstream work. The skill owns the durable review ledger in .agents/upstream-review.md; never bypass its human decision gate.
  • Resolve upstream conflicts Pylon-first. Preserve Pylon branding, agent guidance, and later Pylon-specific product decisions unless the developer explicitly chooses otherwise.
  • An inherited compatibility name is not permission to restore visible T3 branding. Keep product identity and runtime compatibility separate.
  • Before committing or publishing, verify the current branch and remotes. If the checkout is not rooted in the Pylon repository or a push would target a T3 remote, stop and correct it before proceeding.

What makes Pylon special?

Pylon inherits a product used by more than 100,000 people. Preserve the qualities that made the upstream project successful while giving the fork a coherent Pylon identity.

1. Open at the core

Pylon stays open at the core. Share the roadmap, the reasoning, and the code. The upstream project's strong fork culture is part of what made Pylon possible.

2. Performance without compromise

Lots of apps have gotten bogged down with bad tech decisions and "slop". Pylon inherits a fast foundation and must keep it fast. Regularly consider websocket payload size, CSS animations that cause GPU spikes, and expensive list rendering.

3. Remote ready

The inherited websocket layer (npx t3) enables core remote features. Whether users connect over their local network, Tailscale, or the bundled tunnel solution, new features need a deliberate remote-support decision.

4. Multi-surface

Pylon has three key app surfaces: web, desktop, and mobile.

Web has hosted and local modes. The inherited hosted origin is app.t3.codes, while npx t3 hosts the web app locally. Both modes need support where reasonable until Pylon infrastructure deliberately changes them.

Desktop is the main surface most users install first. It is an Electron app that bundles the server runner and can host remote connections from the hosted web client or mobile app.

Mobile is a React Native app for iOS and Android. It connects to a compatible Pylon environment to control work remotely.

Inherited product principles

I like ambitious ideas, simple systems, and software that feels obvious. Do not preserve complexity just because it already exists. Do not introduce machinery because it looks architecturally impressive. Understand the real constraint, then fight for the smallest model that makes the correct behavior unsurprising.

Channel both "measure twice, cut once" and "yagni". Fight scope creep. Try to honor the dev's intent in both a minimal and realistic fashion.

The rest of this document is meant to help you navigate the codebase and make changes effectively. Think of these instructions less as "hard rules", more as "good defaults". The developer's preferences should be able to override anything here.

Most Pylon contributions may come from Pylon itself, often controlled remotely. Be careful when accessing data, stopping dev servers, or taking actions that could damage the environment the contributor is actively using.

A small glossary

We need to be on the same page with terminology. When communicating, use this language:

  • you means the agent reading this file and changing Pylon.
  • we, us, and maintainers mean the people maintaining Pylon. Use upstream maintainers when specifically referring to T3 Code's maintainers.
  • user means the person using Pylon to direct coding agents.
  • agent means the coding agent a user runs inside Pylon. Depending on context, that may also include you.
  • provider means the agent runtime or harness Pylon talks to, such as Codex, Claude, Cursor, or OpenCode.
  • client means the web, desktop, or mobile UI.
  • environment means one running Pylon server and the machine, filesystem, provider credentials, and state it owns.
  • project means an environment-local workspace record rooted at a directory.
  • thread means the durable conversation and work history for a project.
  • turn means one user-to-agent cycle, including follow-up work such as checkpointing.
  • runtime home means the base data directory. It defaults to ~/.pylon-code on every launch path, while the override names stay T3-compatible (--base-dir, T3CODE_HOME); runtime state normally lives below its userdata directory.

The three ways to hurt yourself

  1. Killing by pattern. Never pkill -f, pgrep | kill, or kill a PID you found by matching a name, path, or worktree string. Your own agent process has this worktree's path in its argv, and this machine runs several other dev servers at once. Kill only a PID you captured at spawn, or the owner of your port from ss -H -ltnp after confirming /proc/<pid>/cwd is your worktree.
  2. Writing to the live install. ~/.pylon-code/userdata is Pylon's real database and ~/.t3/userdata is T3 Code's, and both may be in use while you work. Reading them read-only is fine; copy only from ~/.pylon-code (see Test data). Never start a server against either, never open either read-write, never clean either up.
  3. Baking in origins. Never set VITE_HTTP_URL or VITE_WS_URL for dev. Dev is single-origin and Vite proxies /api, /ws, /oauth, and /.well-known. Setting them bakes localhost into the bundle and silently breaks every remote browser.

Hit every surface

The most common defect in this repo is a change that works on the path you tested and is missing everywhere else. Before calling frontend work done, walk this list and say which entries applied:

  • Entry points. A behavior reachable from the chat view is usually also reachable from Settings, the command palette, and a keybinding. Fixing one is not fixing the feature.
  • Clients. Web, desktop (wraps web, adds Electron shell/IPC), and mobile (React Native, separate navigation). Shared logic lives in packages/client-runtime
  • Providers. Codex, Claude, Cursor, Grok, and OpenCode each have an adapter. Provider-shaped features need a decision per adapter, even if the decision is "not supported here".
  • Contracts. Anything crossing the wire is typed in packages/contracts. Change the schema and the server, web, mobile, and desktop all follow.
  • Reverse states. If you added a way in, add the way out and the way to see it. Snooze needs unsnooze. Close needs reopen. A one-way door is a bug.
  • Connection modes. Local, remote/relay, and tunnel behave differently. Multi-device and multi-environment cases are real.
  • Docs. docs/ splits by audience. Behavior changes that a user would notice belong in docs/user/ (shipped-product voice, no repo tooling or source paths); architecture and contributor changes in docs/internals/; runbooks in docs/operations/; new vocabulary in docs/internals/glossary.md.

Dev servers

  • vp i installs. Worktrees get this from the compatibility-named t3.json setup script; if module resolution looks broken, it probably did not run.
  • vp run dev starts server and web. In a worktree, state defaults to that worktree's gitignored .t3, which deliberately outranks an ambient T3CODE_HOME so you cannot land on shared state by accident. An explicit --home-dir still wins.
  • Ports derive from the worktree path and are stable across restarts, but read the real ones from the [dev-runner] line since occupied ports shift.
  • vp run dev needs a TTY. Launched from a plain non-interactive background shell it prints its first two lines and exits 0 without ever binding a port, which reads exactly like a fast successful start. Run it under script -q /dev/null … or nohup … & (there is no setsid on macOS), then confirm the port is actually listening before you trust it.
  • Sharing over the tailnet is three steps: run vp run dev --share in the background, wait for the pairingUrl: line in its output, then hand that full URL to the user with its pairing token intact. Do not wire up tailscale serve by hand for this, and do not open the complete pairing URL yourself.
  • The web app requires pairing. For an explicit human handoff, provide the pairing URL rather than the bare origin — a URL without its token is useless to whoever you gave it to. Treat the token as a secret everywhere else: never commit it, capture it in screenshots, or reuse it. If a token got consumed, mint a fresh one with node apps/server/src/bin.ts pair; it carries standard scopes, while the startup URL carries admin scopes (needed for Settings → Connections management).
  • Stop what you started, by the PID you tracked. See rule 1.

Test data

An empty database is a bad test. Seed your worktree's .t3 with a copy of real data instead of pointing at live state:

  • Copy from ~/.pylon-code/userdata — Pylon's own runtime home, and the only realistic fixture that matches this repo's schema. Worktree state still lives at <worktree>/.t3/userdata, which is a compatibility-named path, not a second source of data.

  • Do not seed from ~/.t3/userdata. Despite the compatibility-named path, that is T3 Code's database, and it carries upstream's migration numbering. Pylon renumbered 037–040 (ProjectionThreadsPinned, ProjectionThreadsContinuedFrom, ProjectionTurnsKeysetIndex, ProjectionThreadsPinOrderKey) and retired id 36, so a Pylon server started against a copy of T3's database records those ids as already applied and then dies on the first query with no such column: continued_from_thread_id. Reading it is still fine; seeding from it is not.

  • Snapshot the database with VACUUM INTO, which is safe even while a server has the source open and yields one consistent file:

    mkdir -p .t3/userdata
    rm -f .t3/userdata/state.sqlite*  # VACUUM INTO refuses to overwrite
    bun -e "new (require('bun:sqlite').Database)(process.env.HOME + '/.pylon-code/userdata/state.sqlite', { readonly: true }).run(\"VACUUM INTO '.t3/userdata/state.sqlite'\")"

    A plain cp is only safe when no server has the source open, and must bring the -wal and -shm siblings along. A live file copy is a corrupt copy.

  • Bring secrets and settings.json only if the flow under test needs them.

  • Copy in, never symlink. Data flows one way: into your sandbox, never back out.

Verifying

  • Smallest proof that the change works. vp test run <files> for the tests you touched, targeted lint and typecheck for the scope you changed.
  • Do not run repo-wide checks. No vp check, no vp run -r test, no vp run -r typecheck unless I ask. CI owns the full suite.
  • Read verification output; do not trust the exit code alone. A vp run -F filter matching no package exits 0 having checked nothing. The server package is named t3, not @t3tools/server-F @t3tools/server typecheck is a silent no-op that reads as a pass.
  • Backend behavior changes ship with focused tests for that behavior.
  • The server is event-sourced and its async flows emit typed receipts. Wait on receipts and worker drains, never on sleeps or polling. A test that needs a timeout to pass is wrong.
  • Upon request, user-visible frontend changes should get one integrated pass in a real client: test-pylon-app for web, test-pylon-mobile for mobile. The primary agent does this once after integrating. Subagents do not launch their own dev servers. Ask permission before doing computer use or spinning up browsers.
  • Use the ship-pylon-mobile skill for getting a build onto a physical iPhone, or when an installed iOS app is broken or stale. It owns the EAS build, OTA, and rollback paths, and the fingerprint check that keeps a JS update from landing on a binary that cannot run it.

Landing changes

Every change lands the same way, and you do not need to be asked each time. Do not commit directly to pylon, and do not fast-forward a task branch onto it.

  1. Branch from the latest pylon. git fetch origin pylon, then branch from origin/pylon — never from a stale local ref, which may sit well behind. Name the branch for the work: fix/<topic>, feat/<topic>, docs/<topic>, or upstream/<yyyy-mm-dd>-<topic> for adopted upstream work.
  2. Build and verify on the branch, per Verifying above.
  3. Open a PR against pylon. Push the branch and open it. This workflow is the standing ask, so opening one needs no separate request.
  4. Merge once checks are green and the developer approves. Delete the branch afterwards.

pylon only ever moves by merging a PR. If you are about to run git push origin pylon, stop — closing that path is the point of this section.

A branch checked out in a worktree is pinned there and cannot advance elsewhere, so keep pylon checked out in the main repository and give worktrees their own task branches. A worktree left sitting on pylon silently holds the branch back while work lands from elsewhere.

Pull requests

  • Conventional commit titles, plain language: fix(web): new threads no longer spike CPU.
  • Body: the problem in a sentence or two, then how you fixed it. End with the model and harness that did the work.
  • Rebase onto the latest pylon branch before opening. Stale branches conflict and burn a review round. Never rebase a Pylon branch directly onto a T3 remote.
  • UI changes need before/after images. Motion or timing needs a short video.
  • Upload PR evidence to GitHub. Never commit PR-only screenshots or assets such as .github/pr-assets/.
  • One concern per PR. If the description says "also", split it.
  • When babysitting: poll checks and comments newer than the last push, verify each bot finding against the source, fix real ones, dismiss false positives with a written reason. Stay quiet when nothing is new. Stop when the bots are green on the latest commit.

Plans and work artifacts

  • Do not commit implementation plans, research notes, or agent scratch files. Keep temporary working material outside the worktree. .plans/ is gitignored only as a safety net for legacy tooling.
  • Track active maintainer work in the GitHub issue or project item that owns it. External proposals follow CONTRIBUTING.md and belong in Ideas discussions.
  • Put durable architecture, constraints, and decisions in docs/internals/. Update those docs when the product changes so agents find current facts instead of abandoned intentions.
  • A merged PR is the implementation record. Close or update its tracking item when the work lands; do not preserve a second checklist in the repository.

Durable project facts

.agents/durable-facts.jsonl is the append-only fact log for coding agents. It is an index of stable project knowledge, not a replacement for canonical code and documentation.

  • Never edit, delete, or reorder an existing line. Append new records at the end of the file.
  • Each line is one JSON object with a unique id, UTC recorded_at, focused scope, concise current fact, full source_commits, repository-relative source_paths verified at the current head, and a supersedes array.
  • Record non-obvious, durable product behavior, architecture, compatibility boundaries, and maintainer constraints. Do not record plans, task status, debugging chronology, guesses, or short-lived implementation details.
  • When a fact changes, update its canonical code or documentation and append a complete replacement whose supersedes array names the old record IDs. Readers must prefer the replacement; history stays intact.

How it works

Clients send typed WebSocket requests. The server turns them into commands, a pure decider turns commands into persisted events, and a projector derives the read model the UI renders. Provider CLIs run as subprocesses; per-provider adapters translate their native protocols into orchestration events. Side effects run in queue-backed reactors that emit receipts when milestones land. Each turn ends with a checkpoint, a hidden git ref, so the app can diff and restore.

Full glossary with file links: docs/internals/glossary.md

Where code lives

  • apps/server - WebSocket, orchestration, providers, checkpointing. Effect-heavy: read .repos/effect-smol/LLMS.md before writing Effect code.
  • apps/web - React/Vite UI. apps/desktop wraps it, apps/mobile is React Native, apps/marketing is the site.
  • packages/contracts - Effect/Schema contracts plus small derived helpers. No heavy runtime logic.
  • packages/shared - shared runtime utils, subpath exports, no barrel.
  • packages/client-runtime - client code shared by web and mobile.
  • .repos/ - vendored read-only references. Prefer their patterns over invented ones. Never edit or import from them. Sync with vpr sync:repos when bumping the matching dependency.

Taste

  • Complexity belongs at the adapter boundary. Orchestration stays pure, UI stays dumb.
  • Inferred types over annotations. any is the enemy.
  • Comments describe how a thing is used, and move when the code moves. To be used mostly to describe functions, not to annotate every line of behavior.
  • Our users drive agents all day and notice a dropped frame, a lying spinner, and a stale label.
  • Continuous animation must be scoped to a transient state and gated. Nothing may repaint at rest: an animation runs only while the state it reports is live, and stops for reduce-motion, backgrounding, and unfocused screens. thread-work-log.tsx's shimmer is the reference. An unbounded animation that survives all three gates pegs the GPU on high-refresh displays.
  • If a rule here fights the task in front of you, say so loudly and get a human sign-off before breaking it.

Additional tips

  • Don't verify with browsers or computer use unless the user explicitly agrees or requests it.
  • Security is important, but should not be over-indexed on, especially for dev mode/maintainer-only features.