A web UI for managing and watching AI coding sessions. Spawns Claude Code, Codex, or plain terminal processes via a node-pty daemon, parses the terminal output into styled blocks, and renders them as a clean chat-like interface with a live status sidebar.
It is not a replacement for Claude Code or Codex — it wraps the real CLI tools in a prettier interface with session management, file browsing, and multi-tool support.
┌───────────────────────────────────┬──────────────┐
│ 🟠 claude-1 🟢 codex-1 ⬛ sh + │ ● Working │
├───────────────────────────────────┤ 7m 35s │
│ ┃ you │ 1.9k tokens │
│ ┃ fix the dark mode bug │ │
│ │ Current │
│ 🔍 searched 1 pattern │ fix the … │
│ │ │
│ ┃ Two problems found: │ Files │
│ ┃ 1. Hardcoded … │ parser.ts M │
│ ┃ 2. Insecure … │ ansi.ts + │
│ │ │
│ ┌─ EXPLORE 22 tools 31s ┐ │ Session │
│ └───────────────────────┘ │ Total: 23m │
├───────────────────────────────────┤ Turns: 6 │
│ Send a message… ↑ │ Tokens: 78k │
└───────────────────────────────────┴──────────────┘
Prerequisites: Node 20+, npm. No tmux required.
git clone https://github.com/uzihaq/pretcli.git
cd pretcli
npm install
npm run dev
# → daemon on ws://127.0.0.1:3001
# → vite on http://localhost:5173Open http://localhost:5173. Click + to create a session — pick Claude
Code, Codex, or Terminal, choose a directory, and go.
npm run daemon # just the node-pty daemon
npm run ui # just the vite dev servernpm run build # → dist/
npm run preview # serve dist/ node-pty (spawns claude / codex / shell)
│
│ raw PTY bytes
▼
@xterm/headless (virtual terminal emulator)
│
│ serialize screen + scrollback
▼
daemon/server.cjs ── WebSocket ──► React app
▲ │
│ pty.write() │
└─── keyboard input ────────────┘
A single Node.js process that manages all sessions. For each session:
- node-pty spawns the tool process (Claude Code, Codex, or a shell)
- @xterm/headless consumes the raw PTY output and maintains a virtual screen buffer (the same job a real terminal emulator does)
- @xterm/addon-serialize dumps the current screen + scrollback as ANSI-preserving text on every output event (debounced to 16ms)
- The snapshot is pushed to subscribed clients via WebSocket in real time
Also serves files via HTTP (GET /file/<path>) for drag-to-download, and
provides list_dir, read_file, write_file, file_info, and
read_file_binary actions for the file explorer.
Session persistence: session metadata + the last ~500KB of output is
saved to ~/.pretcli/sessions.json every 5 seconds. On daemon restart,
sessions are recreated:
- Claude Code:
claude --resume <sessionId>(picks up the conversation) - Codex: restarts fresh in the same cwd
- Terminal: restarts fresh shell in the same cwd
- Buffer replay: clients see previous output immediately on reconnect
Plugin architecture with three parsers checked in order:
- codexParser (
codex.ts) — detects›prompt,•blocks,OpenAI Codexbanner - claudeCodeParser (
claudeCode.ts) — detects▐▛███▜▌banner,⏺❯markers - terminalParser (
terminal.ts) — always matches (fallback)
Each parser implements the ToolParser interface (defined in types.ts):
detect(), parse(), workingState(), extractSidebarFindings(),
pollInterval(). Parsers are stateless — they see one snapshot and return
transient findings. All stateful display logic (latching, frozen-prefix,
own-clock timer, file accumulation) lives in App.tsx.
Connects to the daemon via WebSocket on mount. Subscribes to all sessions for background status tracking. When the active session's snapshot arrives:
- Detects / re-detects the parser for the session
- Runs
parser.parse(raw)→Block[] - Applies the frozen-prefix / live-tail jitter defense (Claude Code only)
- Runs
parser.workingState(raw)→ updates sidebar timer/status - Runs
parser.extractSidebarFindings(raw, blocks)→ updates sidebar
Each session can cycle through view modes (header toggle button):
- Pretty (default for Claude Code / Codex) — parsed blocks with formatted messages, tool cards, thinking indicators, progress checklists
- Terminal (default for terminal sessions) — raw xterm.js rendering
- Explorer — full-screen file browser with grid/list views, breadcrumb navigation, file preview split, drag-in/drag-out
Available in the sidebar (terminal sessions) and as a full-screen view:
- Sidebar explorer: tree view, click to expand folders, double-click to cd, click files to preview. File info panel at the bottom.
- Full-screen explorer: grid view (file-type icons) or list view (name + size), breadcrumb path bar, bottom-half preview split. Drag files out to download, drop files in to upload.
- Tabs at the top for each active session. Per-tab tool icons (Claude coral square, Codex teal mark, terminal glyph).
- + button opens a modal: pick tool, choose directory (with browseable folder picker), set skip-permissions, optionally name the session.
- × button on each tab to close (kills the PTY process).
- Double-click a tab name to rename inline.
- Status dots: pulsing coral = working, solid green = finished (background tab), no dot = idle.
pretcli/
├── README.md
├── docs/
│ ├── ARCHITECTURE.md ← system design deep-dive
│ ├── DEVELOPMENT.md ← how to extend / modify
│ └── TROUBLESHOOTING.md ← common failures + fixes
├── daemon/
│ └── server.cjs ← node-pty + WebSocket daemon
├── scripts/
│ └── fix-spawn-helper.cjs ← postinstall: chmod node-pty binary
├── hooks/
│ ├── README.md ← notification hook setup
│ └── notify-ui.sh ← Claude Code Stop hook
├── src/
│ ├── App.tsx ← root: WebSocket handlers, state, layout
│ ├── main.tsx
│ ├── styles.css ← theme vars + all custom CSS
│ ├── lib/
│ │ ├── daemon.ts ← WebSocket client for daemon
│ │ ├── ansi.ts ← stripAnsi + ansiToHtml
│ │ ├── parser.ts ← Claude Code raw text → Block[]
│ │ ├── contentRender.ts ← ANSI → marked → linkify pipeline
│ │ ├── filePaths.ts ← linkifyFilePaths → vscode://file/
│ │ └── theme.ts ← dark/light hook
│ ├── parsers/
│ │ ├── types.ts ← ToolParser interface
│ │ ├── detect.ts ← detection cascade
│ │ ├── claudeCode.ts ← Claude Code parser
│ │ ├── codex.ts ← Codex parser
│ │ └── terminal.ts ← terminal fallback
│ └── components/
│ ├── ChatArea.tsx ← scroll area + terminal split
│ ├── InputBar.tsx ← textarea + send (agent sessions)
│ ├── SessionTabs.tsx ← header: tabs, rename, view toggle
│ ├── StatusSidebar.tsx ← right panel (status / files / explorer)
│ ├── NewSessionModal.tsx ← session creation modal
│ ├── FileExplorer.tsx ← sidebar file browser
│ ├── FullFileExplorer.tsx ← full-screen file browser
│ ├── FilePreview.tsx ← code / markdown / image preview
│ ├── ToolIcon.tsx ← SVG icons per tool
│ └── blocks/ ← one component per Block.type
└── package.json
The daemon and vite dev server both bind to 0.0.0.0 by default, so they're
accessible from any device on the same network. With Tailscale:
npm run dev
# → http://<tailscale-ip>:5173 (UI)
# → ws://<tailscale-ip>:3001 (WebSocket)
# → http://<tailscale-ip>:3001 (REST API)The client auto-detects the correct WebSocket host from the page URL — no configuration needed. Open the Tailscale IP on your phone and it just works.
The daemon serves a stateless HTTP API alongside the WebSocket, designed for scripting, agents, and one-shot queries. All responses are JSON.
# List sessions
curl http://localhost:3001/api/sessions
# Create a Claude Code session
curl -X POST http://localhost:3001/api/sessions \
-H 'Content-Type: application/json' \
-d '{"tool":"claude-code","directory":"~/my-project","skipPermissions":true}'
# Send input to a session
curl -X POST http://localhost:3001/api/sessions/my-session/input \
-H 'Content-Type: application/json' \
-d '{"data":"fix the bug\r"}'
# Read current screen output
curl http://localhost:3001/api/sessions/my-session/buffer
# Kill a session
curl -X POST http://localhost:3001/api/sessions/my-session/kill
# Rename a session
curl -X POST http://localhost:3001/api/sessions/my-session/rename \
-H 'Content-Type: application/json' \
-d '{"newName":"new-name"}'
# Browse files
curl "http://localhost:3001/api/dir?path=~/my-project"
# Read a file
curl "http://localhost:3001/api/file/read?path=~/my-project/package.json"
# File metadata
curl "http://localhost:3001/api/file/info?path=~/my-project/package.json"
# Write a file
curl -X POST http://localhost:3001/api/file/write \
-H 'Content-Type: application/json' \
-d '{"path":"~/my-project/test.txt","content":"hello world"}'
# Home directory
curl http://localhost:3001/api/home| Method | Path | Body | Description |
|---|---|---|---|
| GET | /api/sessions |
— | List all sessions |
| POST | /api/sessions |
{tool, directory, skipPermissions?, name?} |
Create session |
| GET | /api/sessions/:name/buffer |
— | Current screen output |
| POST | /api/sessions/:name/input |
{data} |
Write to PTY |
| POST | /api/sessions/:name/kill |
— | Kill session |
| POST | /api/sessions/:name/rename |
{newName} |
Rename session |
| GET | /api/dir?path=... |
— | List directory |
| GET | /api/file/read?path=... |
— | Read file (100KB) |
| GET | /api/file/info?path=... |
— | File metadata |
| POST | /api/file/write |
{path, content} or {path, base64} |
Write file |
| GET | /api/home |
— | Home directory |
| GET | /health |
— | Liveness check |
| GET | /file/<path> |
— | Direct file download |
The daemon binds to 0.0.0.0:3001 by default. To expose it behind a reverse proxy:
- Build the frontend:
npm run build→dist/ - Serve
dist/with nginx / caddy / any static file server - Run the daemon behind a reverse proxy that upgrades WebSocket:
# nginx example
location /ws {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /file/ {
proxy_pass http://127.0.0.1:3001;
}- Client auto-detects the WebSocket URL from the page hostname. If the
page is served from
your-domain.com, the client connects towss://your-domain.com:3001. No code changes needed. - Add authentication — the daemon has no auth. Put auth in the reverse proxy (basic auth, JWT, client cert) before the WebSocket upgrade.
- Keep the daemon alive — use systemd, pm2, or a supervisor:
# systemd unit example
[Service]
ExecStart=/usr/bin/node /opt/pretcli/daemon/server.cjs
Restart=always
Environment=DAEMON_PORT=3001Sessions persist to ~/.pretcli/sessions.json and are restored on daemon
restart. Claude Code sessions resume via --resume.
- Create
src/parsers/myTool.tsimplementing theToolParserinterface - Register it in
src/parsers/detect.ts(beforeterminalParser) - Add a
ToolIconvariant insrc/components/ToolIcon.tsx - Add the tool to
TOOLSinNewSessionModal.tsxandSessionTooltypes - Add the tool name to the daemon's
createSessionallowed list - Add a
buildLaunchCommandcase indaemon/server.cjs
The parser only needs to implement 5 methods — see types.ts for the
interface and terminal.ts for the minimal example.
The daemon exposes a WebSocket API on port 3001. Connect and send JSON:
const ws = new WebSocket('ws://127.0.0.1:3001');
// Create a session
ws.send(JSON.stringify({
type: 'new_session',
tool: 'claude-code', // or 'codex', 'terminal'
directory: '~/my-project',
skipPermissions: true,
name: 'my-session' // optional
}));
// Subscribe to output
ws.send(JSON.stringify({ type: 'subscribe', session: 'my-session' }));
// Send input
ws.send(JSON.stringify({ type: 'input', session: 'my-session', data: 'hello\r' }));
// List files
ws.send(JSON.stringify({ type: 'list_dir', path: '~/my-project' }));
// Read a file
ws.send(JSON.stringify({ type: 'read_file', path: '~/my-project/package.json' }));Full message reference: list_sessions, new_session, subscribe,
unsubscribe, input, resize, kill_session, rename_session, home,
notifications, list_dir, read_file, read_file_binary, file_info,
write_file.
Sessions survive daemon restarts. The daemon writes ~/.pretcli/sessions.json
every 5 seconds with session metadata + the last ~500KB of screen output.
On restart:
- Claude Code: resumes via
claude --resume <sessionId>(conversation pickup) - Codex / Terminal: restarts fresh in the same cwd
- Buffer replay: clients see previous output immediately on reconnect
The persistence file is also written on clean shutdown (SIGTERM/SIGINT).
- macOS / Linux only (node-pty requires a POSIX system)
- The daemon binds to
0.0.0.0— add auth via a reverse proxy for production deployments - The parser is built against current Claude Code / Codex output markers. If the tools change their terminal format, the parser needs updates