The control room for your coding CLIs.

Every session in one window, with the files, the diff and the terminal beside it.

InstallGitHub (opens in a new tab)macOS · Linux · Windows
A working simulation of the plxr window with three coding sessions side by side. The sessions are made up and nothing leaves this page.
the sync worker gives up after the first timeout. retry the push with exponential backoff, five attempts at most
I'll start with how the worker calls the remote.
Read internal/sync/worker.go
148 lines
flush() calls Push once and returns whatever comes back. I'll wrap that call in a loop that doubles the wait, caps it at five seconds and stops as soon as the context is cancelled.
Update internal/sync/worker.go
14 added, 1 removed
func (w *Worker) flush(ctx context.Context, batch []Item) error {
return w.remote.Push(ctx, batch)
delay := 200 * time.Millisecond
var err error
for attempt := 1; attempt <= 5; attempt++ {
if err = w.remote.Push(ctx, batch); err == nil {
the search page shows an empty box when nothing matches. give it a proper empty state
Read app/search/Results.tsx
41 lines
Results renders the list whatever it holds, so no hits means an empty list. I'll return a short message instead, with the query in it.
Update app/search/Results.tsx
8 added, 1 removed
export function Results({ hits, query }: Props) {
return <ul className="results">{hits.map(renderHit)}</ul>;
if (hits.length === 0) {
return (
<p className="results-empty">
Nothing matches “{query}”. Try fewer words.
</p>
);
}
return <ul className="results">{hits.map(renderHit)}</ul>;
}
Run this command?
$ pnpm vitest run app/search
tables break when a cell has an escaped pipe in it. fix the parser
I'll write a failing test first, so the fix has something to prove.
Update tests/test_tables.py
3 added
def test_escaped_pipe_stays_in_cell():
row = split_row(r"| a \| b | c |")
assert row == ["a | b", "c"]
Ran 1 shell command
$ python -m pytest tests/test_tables.py -q
F.......................
1 failed, 23 passed in 0.31s
It fails as expected: split_row cuts at every pipe. I'll split only where a pipe is not escaped, then unescape the cells.
Update quillmark/tables.py
2 added, 1 removed
def split_row(line: str) -> list[str]:
cells = line.strip().strip("|").split("|")
cells = re.split(r"(?<!\\)\|", line.strip().strip("|"))
cells = [c.replace(r"\|", "|") for c in cells]
return [c.strip() for c in cells]
Ran 1 shell command
$ python -m pytest tests/test_tables.py -q
........................
24 passed in 0.29s
All 24 pass. An escaped pipe now stays inside its cell.
Worked for 22s
sync-retry6 sessions · 1 needs you · 1 waiting · 1 working · 2 orphaned · 1 ended#1·#211%·52%#30%·49%#43%·98%335.3M4 active
K search commandsBBJ the edgesE switch session

Simulated: the sessions are made up and nothing leaves this page.

The board

Every session as a tile, in the order that matters: the ones that want an answer first, then the working ones, then the quiet ones.

Each tile shows its last lines, its folder and its state. A session has exactly one state, and that state has the same name wherever it appears.

2of 6 want an answer. They stand at the front of the board.
sessions>
  1. retry uploads with backoffharbor-sync · retry-backoff
    Bash(pnpm test src/upload)Do you want to proceed?› 1. Yes 2. No
    needs you212.4kClaude Code
  2. rate limit per tokentide-api · main
    ● Added a sliding-window limiter. 12 files changed, checks green.Open a pull request?
    waiting · open a pull request?96.8kCodex CLI
  3. migrate invoice tablesledger · invoices
    Applying edit to db/0042_invoices.sqlTokens: 8.1k sent, 1.2k receivedCommitting a3f9e1c
    working · applying an edit41.3kaider
  4. rewrite the setup guidefieldnotes · docs
    Generatingdocs/setup.md +64 −22docs/index.md +3 −1
    working · generating58.9kGemini CLI
  5. atlas-web
    Local: http://localhost:5173/ready in 412 mspage reload src/App.tsx
    running
  6. prune test fixturesharbor-sync · main
    Removed 31 unused fixtures.Build · 0 errors$
    ended · 14:0233.1kopencode
6 sessions · 1 needs you · 1 waiting · 2 working · 1 running · 1 ended01/06

The order, wherever sessions are listed

  1. 1needs youstuck on a question
  2. 2waitingwaiting for input
  3. 3workingat work
  4. 4runningalive, reports nothing
  5. 5haltedhalted by you
  6. 6orphanedplxr stopped unexpectedly
  7. 7endedover, with the time it ended

A permission comes before a plain question, and the session that has waited longest comes before the others. Within a state the order they came in is kept, so the board does not reshuffle while nothing changes.

Real terminals

Every session is a PTY that keeps running when the window closes.

The daemon owns the terminals. The window is a client that shows them, and there may be several windows at once. Close it and try.

04:12harbor-sync has been running, with a window or without one.
  • 2 MiB of scrollback per session, kept by the daemon. Older output falls off the back.

  • The stream also runs into a recording on disk, which the search reads and the player plays back with a scrubber.

  • From any other terminal, plxr attach <which> joins a session. Ctrl-Q twice lets go.

  • Unix PTYs on macOS and Linux, ConPTY on Windows, through one layer.

  • If plxr itself stops unexpectedly, the tile says orphaned and a click picks the conversation back up.

1 window attached.
retry uploads with backoffharbor-sync · retry-backoff
› run the upload tests and fix what fails● Running the upload suite. Ran 1 shell command ✓ upload/chunk.test.ts 14 passed ✗ upload/retry.test.ts 2 failed● The retry loop never waits between attempts. Update src/upload/retry.ts
working · 04:12 · 19.2k tokens
plxr daemon127.0.0.1 · 3 PTYs · 1 window
  • harbor-syncclaude5.6kB
  • tide-apicodex16.9kB
  • atlas-webpnpm dev6.6kB

The tools around it

Projects, file tree, what has changed (with the diff), review, search, inbox, usage against your plan's limits, open ports, archive, notes.

11Each stands on the edge you put it on: left, right, or in the bottom section, which has a left and a right half of its own. Drag an icon to another edge, or focus it and press Alt with an arrow key.
Filesproject
Inboxglobal
2
drop a tool here
drop a tool here
Move Changes to

Changes · looks at the project being worked in. What has changed in the working tree, staged and not, with the diff.

Templates and agents

Start a session the way you always start it: the folder, the CLI, the account.

A CLI that is not on the PATH your login shell has is greyed out with “not found” before you click, and an account close to the end of one of its windows is marked while the choice still costs nothing.

From a shell it is plxr new [path] [-- command …], which starts Claude Code in the current folder when you name nothing.

new sessionN
directory
~/work/quarry
what to start
permissions
account
~/work/quarry $ claude
nothing started yet
~/.plxr/templates/morning.json

A template remembers a set of folders and what runs in them.

The same folders under the same accounts every morning become one click. Whatever is open right now can be saved as the set.

harbor-sync·quarry·saltbox·
~/.plxr/agents/mycli.json

A new CLI is one JSON file in ~/.plxr/agents.

blocked recognises a decision waiting in the visible text, working a spinner. After idle_seconds without output the session counts as idle_status. Claude Code reports its own state once plxr setup-hook has hooked into its events; every other CLI is read from the screen.

  • Claude Code
  • Codex CLI
  • aider
  • Gemini CLI
  • opencode
  • Cursor CLI

Four skins

The tube, Windows 95, pen and paper, pixels. They are not colour schemes: each one draws the whole window its own way, with its own icon set.

Point at the screen to look closer
CRT. Shown in green. The CRT theme plxr ships is amber.

Keyboard first

K for everything, 19 for the views, B B J for the edges, N for a new session, W to close a panel.

Press keys to find them on the map. Keys your browser uses, like W, still do what they do in the browser.

Panels and windows

Navigation

Terminal and editor

Other

Every shortcut lives in one table: the handlers read it, the list under ? prints it, and in the settings each row can be bound to another key.

On Linux and Windows the chords are Ctrl chords and is Alt. While a terminal has the keyboard, a Ctrl chord or an F-key belongs to the program running in it.

Yours, locally

The daemon listens on 127.0.0.1 on a random port behind a token in ~/.plxr/daemon.json. Nothing leaves the machine.

  • daemon.json is written with mode 0600, because it holds the token.

  • Every request under /api and /ws needs the token. Without it the answer is 403.

  • One daemon at a time: a lock file decides which process gets to be it.

  • PLXR_HOME moves the whole home, so a build of your own runs next to the installed one without touching it.

  • Reaching plxr from another machine is a switch under Settings, off until you turn it on. Then the token, or a pairing code that works once for ten minutes, stands between the network and the daemon.

~/.plxr/daemon.json-rw-------
{ "port": 52731, "token": "3f9ac1d07be24e5a96c0f18d2b7e43a1c95d60e8f2a4b71c", "pid": 48213, "since": 1790150400000}
new port, new token
  • the windowGET /api/sessions · X-Plxr-Token 3f9ac1…b71c200
  • any other processGET /api/sessions · no token403
  • another machineby default nothing listens beyond 127.0.0.1·

Install

One command. It works out which system you are on, takes the newest release and puts it where that system keeps programs.

Nothing else is touched: no package manager, no service, no admin rights.

Once it runs it keeps itself up to date: a new version shows as a band at the top of the window, and one click installs it.

plxr 0.98.0 is out — you are on 0.97.0.
install.sh
curl -fsSL https://raw.githubusercontent.com/nock-404/plxr/main/install.sh | sh
The script prints: plxr v0.98.0, plxr-macos-arm64.zip. installed: /Applications/plxr.app. start it: open -a plxr.

What the script prints.

goes to/Applications/plxr.app
archiveplxr-macos-arm64.ziparm64 · 11.6 MB · sha256 59724a9f…76890872

Building it yourself

needsGo 1.23+Node 20+

bundle-macos.sh makes the .app; the Linux package is built by CI on a tag (.github/workflows/linux.yml), Windows cross-compiles from macOS.

The source on GitHub (opens in a new tab)

plxrin the repository
./build.sh

# the frontend, then the binary

./try.sh

# start this build in a home of its own, next to the installed one

./check.sh

# every gate: the static ones, the tests, and the window checks

What it is made of

A Go daemon owns the sessions, the archive and the settings and serves an HTTP/WS API plus the interface; a Wails v3 window loads that interface; the interface itself is Next.js, statically exported, laid out with dockview. BUILD.md (opens in a new tab) is the log of what is done and what is not.

this machine · 127.0.0.1
coding CLIsone PTY each
Claude CodeworkingCodexwaitingaiderworking

Every session is a PTY that keeps running when the window closes.

plxr daemonGo

Owns the sessions, the archive and the settings.

/api/*/ws/*/

127.0.0.1 on a random port, behind a token in ~/.plxr/daemon.json

windowWails v3

loads 127.0.0.1:PORT/?token=…

the interface

Next.js, statically exported, laid out with dockview

How plxr is put together: coding CLIs run in PTYs owned by the plxr daemon, written in Go. The daemon serves an HTTP and WebSocket API and the interface on 127.0.0.1. A Wails v3 window loads that interface.

github.com/aymanbagabas/go-ptyv0.2.3github.com/gorilla/websocketv1.5.3github.com/wailsapp/wails/v3v3.0.0-beta.16golang.org/x/sysv0.47.0golang.org/x/termv0.45.0
next16.3.3react19.2.8dockview-react8.3.1@xterm/xterm^6.0.0@xterm/addon-webgl^0.19.0codemirror^6.0.2

Every set of icons and every library that ships with plxr is listed under Settings → Licences, with its licence in full.