Star on GitHub

Docs

How Leglas is built

Under the hood, Leglas is one Node process between your browser and your dev server, and an agent working through it. Here's how the pieces fit together and what happens to a change request after you press send. If you want more of the details behind Leglas you can find that here, if you're interested in contributing take a look at the CONTRIBUTING.md file and if you're just looking for the easiest way to use Leglas, .

Ten boxes and the arrows between them: the browser loads the interface, a change request goes through the API and the queue, a Chromium on the machine takes a screenshot of it, the agent runner passes it to an agent, the agent edits the project, a file change reaches the WebSocket, which nudges the browser, and every other path goes through the proxy to the dev server

What is happening under the hood. One Node process sits between your browser and your dev server. The agent, and the Chromium that takes the screenshots, run on the same machine but outside it.

ELI5: One program between your browser and the app you are building

The same ten boxes with plain labels: your browser opens the Leglas page, your change is taken in and a browser on this computer takes a screenshot, your change waits its turn, the agent edits your project's files, Leglas tells your browser to look again and everything else passes through to your running app

Leglas is one program that sits between your browser and the app you are building. You type a change, Leglas takes a screenshot, an agent edits the code and your browser is told to look again. It never runs your app itself, which is why it works with any framework.

One process in front of your dev server

npx leglas starts one Node process. It listens on a local port, 4100 unless that is taken, and answers four kinds of request:

browser ── http://localhost:4100
             /leglas            the interface, served as built static files
             /leglas/api/...    the JSON API behind the interface and the commands
             /leglas/files/...  plain HTML files a config mounts
             everything else    your dev server, through a proxy

The app arrives through the proxy, so each preview is an iframe on the same origin as the interface. That is why a relative preview URL needs no CORS setup and why Leglas works with any framework: it never imports or runs the app, it only forwards HTTP to it.

The server pushes to the interface over one WebSocket, which it implements itself. A push is a nudge that names what moved (the config, the requests, the dev server's health, a share, an update, a set being built), and the interface fetches that again. Nothing else holds a connection open.

A POST to the API is accepted only from this machine, and when the browser sends an Origin it has to match the address the server was reached by. That keeps a page in another tab from queueing work for an agent.

The packages

PackageOn npmWhat it is
packages/servernoThe HTTP server: config loading, the proxy, the API, the request queue, the agent runner, screenshots, branch previews, sharing, the update check and building a set of directions with Claude or Codex
packages/shellnoThe interface: a React application built by Vite into static files
packages/clileglasThe binary. Parses the command line, starts the server and holds every command an agent runs
packages/mcpleglas-mcpA stdio MCP server that exposes those same commands as tools

They depend on each other in one direction:

leglas-mcp ──► leglas ──┬──► @leglas/server
                        └──► @leglas/shell, as built files

Only two packages reach npm. When packages/cli builds, the bundler inlines @leglas/server and copies the built interface into dist/shell, so leglas installs as one package with no runtime dependencies. The Claude Agent SDK is the one exception: it stays an optional dependency because it finds its platform binary at run time.

What leglas and leglas-mcp export is recorded in api-surface.txt at the root. pnpm api:update rewrites it from the built type declarations, and a patch release is refused when it has moved.

Inside the server

packages/server/src keeps what answers requests at the top and groups the rest by area:

FolderWhat is in it
agents/Finding the agents on the machine, the three ways to reach one, the runner that drains the queue and the reasons a run can end
requests/The queue, the notes left on a preview and the images that ride with a request
capture/Finding and driving a browser over the DevTools protocol, the screenshot and its crops, hydration evidence, and text on the page that overlaps or is cut off
config/Finding and loading leglas.config.ts, and what is local to one machine: added directions and renames
branches/Deciding whether a direction needs its own branch, and the worktree, install and dev server when it does
share/The share itself and the tunnel it borrows
generation/Building a set of directions with Claude or Codex: each agent's restricted runs, the plan, the builds, the render check that calls one ready, the slots in the switch file and the record of each set

At the top, server.ts is the HTTP server and every API route, proxy.ts forwards to the dev server, live.ts is the WebSocket, server-info.ts writes server.json, update.ts is the update check, log.ts composes the design log entries leglas keep writes, and index.ts is what the package exports.

Inside the interface

packages/shell/src follows the same rule: the frame of the application at the top, one folder per feature under it. A feature's component, its logic, its calls to the API and its hook sit together, and the logic is the part with tests.

FolderWhat is in it
share/The share panel, what a share is on this side and the calls that start and change one
update/The update chip and its panel
generation/Building a set of directions: the brief's controls, the card that follows a set, the cover over a direction not ready yet, and the calls and poll behind them
annotate/Notes pinned to a spot on a preview: the layer, the anchor that finds the spot again and the notes API
references/Images attached to a request: the strip, what is admitted and the upload
rail/The rail: its header, a row and the pieces of one, the search field, tag colours and the dialog that confirms a delete
composer/The controls around the field a change is typed into: the mode chip, the attach and annotate buttons, the send
stage/What sits beside the rail: one preview pane, and the tools the floating widget opens
lineage/Which direction came from which: the tree, the lines in the gutter, the crumbs and the trail
agents/Picking an agent and its effort, what state a request is in and why one failed, the status card above the composer, connecting an MCP host
preview/What the stage has to know about a frame: its identity and when it is ready, the duplicate scan, the dev server's health, the second pane of a comparison, framework overlays
ui/The shared components, tips, toasts, the orb, the clipboard and the floating widget's drag rules
net/The fetch wrapper, the WebSocket client and the poll that cannot outrun itself

At the top, App.tsx reads the config and decides between the interface, a notice and a share that has ended. Shell.tsx is the rail and the stage, and HelpOverlay.tsx is the list of keys it opens. Shell.tsx holds the state the rail and the stage share and hands each piece what it needs, so a piece can be read without it. useShellState.ts is the behaviour under them: selection, search, rename and remove, the keyboard, resizing and which panes are mounted. reference.ts is the text a copied direction carries, which has nothing to do with the images in references/. types.ts, prefs.ts, keymap.ts and naming.ts are shared by all of it.

Where state lives

Leglas has no database and no account. The committed leglas.config.ts is the only description of a project that teammates share. Everything else is local to one machine, in a .leglas/ directory inside the project. The main entries:

PathWhat it holds
previews.jsonDirections added from the interface or by leglas add
renames.jsonWhat a direction is called on this machine when that differs from the config
requests.jsonThe queue of change requests
annotations.jsonNotes left on a preview that have not been sent yet
captures/ and references/Screenshots taken for a request, and images a person attached to one
variants/Switchers scaffolded by leglas new
generations/A record of each set of directions Leglas built, with pictures; the newest 100
worktrees/Checkouts for directions that live on their own branch
watch.jsonThe agent command chosen for this project
server.jsonWhere the running server is, so a command in another process can find it

The server watches the config and these files and nudges the interface when one changes, which is how leglas add in a terminal shows up in an open rail.

The path of a change request

  1. Someone types into the composer, or drops notes on a preview. The interface posts to /leglas/api/request.
  2. The server takes a fresh screenshot of the direction. It drives a Chromium already on the machine over the DevTools protocol, and crops around every note from that one page load. It also reads the console for a hydration error, so the prompt can say the app rebuilt the page.
  3. The request goes into the queue with its images, its notes and whether it forks a new direction or edits this one.
  4. The runner takes it when an agent has been picked in the interface. There are three ways to reach an agent: a Claude Agent SDK session, the Codex app server, and a command template for any other CLI. All three look the same to the rest of the runner. leglas watch is this loop in a terminal, and an MCP host that is working the queue gets the same right of way.
  5. The agent edits the project. When a run ends badly the runner names why from a fixed set of reasons, never from captured output, because a provider log can carry a prompt.
  6. The queue file changes, the server nudges, and the rail shows the new direction.

Directions on their own branch

Most directions live in the running app and switch instantly. leglas classify sends one to its own branch only when it cannot sit in the same server, for example when it changes dependencies. Such a direction gets a git worktree under .leglas/worktrees/, its own install, its own dev server and a proxy of its own in front of that. None of it starts until someone opens the direction, and one left idle is stopped again.

Sharing

A share starts a second listener on the loopback interface and points a tunnel at it. Requests that arrive there are marked remote: they get the interface in a read-only form, a config trimmed to what the share lists and a health verdict, and every other API route answers 403. How far a viewer can reach into the dev server is decided by path on the server, so it holds against anything a page's own JavaScript could try.

The MCP server

leglas-mcp calls the functions the CLI calls and returns the same envelope, so a behaviour fixed in packages/cli is fixed for both. What it adds is finding the project: a plugin host starts it in the plugin's own directory, so it asks the host for its workspace roots before it trusts the working directory, and refuses when nothing names a project. On a host that speaks channels it also pushes each request into the open session.

Around the packages

PathWhat it is
site/Builds the homepage, the changelog page and these pages. It runs on Vercel with no install step, so it imports nothing from npm
test/Tests about the repository: the plugin manifests, the publish workflow, what the CLI tells people to type
evals/A benchmark cut from this repository's own fixes. Inside a task only instruction.md and task.toml are written by hand. evals/build.ts rewrites the rest, most of it copied from other commits
skills/leglas/The skill an agent loads. With plugin.json and mcp.json at the root it makes the repository an Agent Plugin