Skip to content

uzihaq/pretcli

Repository files navigation

PretCLI

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 │
└───────────────────────────────────┴──────────────┘

Quickstart

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:5173

Open http://localhost:5173. Click + to create a session — pick Claude Code, Codex, or Terminal, choose a directory, and go.

Run components separately

npm run daemon   # just the node-pty daemon
npm run ui       # just the vite dev server

Production build

npm run build    # → dist/
npm run preview  # serve dist/

How it works

  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 ────────────┘

1. Daemon (daemon/server.cjs)

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

2. Parser (src/parsers/)

Plugin architecture with three parsers checked in order:

  • codexParser (codex.ts) — detects prompt, blocks, OpenAI Codex banner
  • 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.

3. React app (src/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:

  1. Detects / re-detects the parser for the session
  2. Runs parser.parse(raw)Block[]
  3. Applies the frozen-prefix / live-tail jitter defense (Claude Code only)
  4. Runs parser.workingState(raw) → updates sidebar timer/status
  5. Runs parser.extractSidebarFindings(raw, blocks) → updates sidebar

4. View modes

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

5. File explorer

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.

Session management

  • 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.

Project layout

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

Accessing from other devices (Tailscale / LAN)

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.

REST API

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

Full endpoint reference

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

Deploying to a VPS

The daemon binds to 0.0.0.0:3001 by default. To expose it behind a reverse proxy:

  1. Build the frontend: npm run builddist/
  2. Serve dist/ with nginx / caddy / any static file server
  3. 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;
}
  1. Client auto-detects the WebSocket URL from the page hostname. If the page is served from your-domain.com, the client connects to wss://your-domain.com:3001. No code changes needed.
  2. Add authentication — the daemon has no auth. Put auth in the reverse proxy (basic auth, JWT, client cert) before the WebSocket upgrade.
  3. 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=3001

Sessions persist to ~/.pretcli/sessions.json and are restored on daemon restart. Claude Code sessions resume via --resume.

Integrating with other tools

Adding a new CLI tool parser

  1. Create src/parsers/myTool.ts implementing the ToolParser interface
  2. Register it in src/parsers/detect.ts (before terminalParser)
  3. Add a ToolIcon variant in src/components/ToolIcon.tsx
  4. Add the tool to TOOLS in NewSessionModal.tsx and SessionTool types
  5. Add the tool name to the daemon's createSession allowed list
  6. Add a buildLaunchCommand case in daemon/server.cjs

The parser only needs to implement 5 methods — see types.ts for the interface and terminal.ts for the minimal example.

Using the daemon API directly

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.

Session persistence

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).

Limitations

  • 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

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors