===== @noahsaso/pi-remote META =====
name = '@noahsaso/pi-remote'
version = '0.3.1'
description = 'Remote terminal access for pi via WebSocket and browser, with Tailscale integration'
repository.url = 'git+https://github.com/noahsaso/pi-remote.git'
homepage = 'https://github.com/noahsaso/pi-remote#readme'
keywords = [
  'pi-package',
  'pi',
  'remote',
  'terminal',
  'pty',
  'websocket',
  'tailscale'
]
----- README -----
# @noahsaso/pi-remote

Remote terminal access for pi via WebSocket with Tailscale integration. Connect to your pi session from mobile browsers over LAN or your tailnet.

## Features

- **PTY-based remote access** - Wraps pi in a pseudo-terminal for optimal performance
- **WebSocket bridge** - Real-time bidirectional terminal I/O with auto-reconnect and output replay buffer
- **Mobile-first** - Touch scroll with momentum, virtual keybar (arrows, Ctrl+C, etc.)
- **Token authentication** - All connections require token auth (HTTP, WebSocket, API)
- **Auth modal** - Browser prompts for token if missing/invalid, with error feedback on retry
- **QR code** - Scan to connect instantly from mobile
- **`/remote` command** - Restart pi in remote mode from within a running session
- **Tailscale integration** - Automatically serves over HTTPS on your tailnet with a unique session subpath
- **Discovery service** - Lists all active remote sessions at `/pi/` with a card UI; auto-starts on first session, auto-shuts down on last
- **TUI widget** - Shows Tailscale URL, LAN URL, token, and all-sessions link in a bordered card above the editor
- **Session ended overlay** - Browser shows a modal when the remote session exits
- **Scroll-to-bottom button** - Appears in the header when scrolled up
- **Styled error pages** - Dark themed 403/404 pages matching the app style

## Screenshots

### Browser — Remote access modal

### TUI — Widget with Tailscale, LAN, and Token
![TUI widget](https://raw.githubusercontent.com/noahsaso/pi-remote/main/packages/remote/docs/tui-widget.png)

## Usage as a pi Extension

This package is primarily used as a [pi coding agent](https://github.com/badlogic/pi-mono) extension. The extension registers a `/remote` command that lets you switch your current pi session into remote access mode without losing context.

### Global (all projects)

Add to `~/.pi/agent/settings.json`:

```json
{
  "extensions": [
    "~/path/to/pi-remote/packages/remote"
  ]
}
```

Or install from npm:

```bash
pi install npm:@noahsaso/pi-remote
```

### Project-local

Add to `.pi/settings.json` in your project root:

```json
{
  "extensions": [
    "~/path/to/pi-remote/packages/remote"
  ]
}
```

### Temporary (one-off)

```bash
pi -e ~/path/to/pi-remote/packages/remote/extension/index.ts
```

### Using `/remote` inside pi

Once the extension is loaded, run `/remote` in pi:

```
/remote
```

This will:
1. Save your current session
2. Shut down the current pi process
3. Restart pi wrapped in a PTY with a WebSocket server
4. Display a QR code and URL — scan from mobile or open in a browser

When running inside a remote session, a persistent widget above the editor shows the remote URL.

---

## Usage as a CLI Tool

You can also launch remote mode directly from the terminal without a running pi session:

```bash
npm install -g @noahsaso/pi-remote
pi-remote
```

### Options

```bash
# Specify a custom pi binary path
pi-remote --pi-path /path/to/pi

# Pass extra arguments to pi
pi-remote -- --continue

# Custom port (default: 7009)
PORT=8080 pi-remote
```

---

## Architecture

```
                        ┌──────────────────┐
                        │  Discovery Svc   │  port 7008
                        │  /pi/ (session   │  Tailscale: /pi/
                        │   list + links)  │
                        └──────────────────┘
                               ▲
              register/deregister via localhost API
                               │
┌─────────────┐         ┌─────────────┐
│   Browser   │  WS     │ HTTP Server │  port 7009+
│  (xterm.js) │────────▶│   + WS      │  Tailscale: /pi/{id}/
└─────────────┘         └──────┬──────┘
                               │
                        ┌──────┴──────┐
                        │     PTY     │
                        │   (pi CLI)  │
                        └─────────────┘
```

### Source Layout

| Path | Description |
|------|-------------|
| `extension/index.ts` | Pi extension entry — registers `/remote` command and bordered TUI widget (Tailscale, LAN, Token, All sessions) |
| `src/cli.ts` | `pi-remote` binary entry point |
| `src/discovery.ts` | Discovery service — session registry, card-based web UI, auto-shutdown |
| `src/discovery-main.ts` | Entry point for the detached discovery process |
| `src/tailscale.ts` | Shared Tailscale helpers (find binary, hostname, serve, serve off) |
| `src/pty.ts` | PTY management (node-pty), stdin/stdout attachment |
| `src/ws.ts` | WebSocket bridge, mobile-priority resize logic |
| `src/server.ts` | HTTP server with token auth, `/api/local-url` endpoint, styled error pages |
| `web/` | Browser frontend (xterm.js, touch scroll, virtual keybar, auth modal, session ended overlay) |

---

## API

### `startRemote(options)`

```typescript
import { startRemote } from "@noahsaso/pi-remote";

const cleanup = await startRemote({
  piPath: "/usr/local/bin/pi",  // optional, auto-detected
  args: ["--continue"],          // optional, passed to pi
  cwd: process.cwd(),            // optional
  env: process.env,              // optional
});

// Stop the server and kill the PTY process
cleanup();
```

### Environment Variables

| Variable | Description |
|----------|-------------|
| `PORT` | HTTP server port (default: `7009`) |
| `PI_REMOTE_URL` | Set automatically when running inside `pi-remote`; the extension uses this to show the URL widget |
| `PI_REMOTE_TAILSCALE_URL` | Set automatically when Tailscale serve is active; shown in the TUI widget |
| `PI_REMOTE_DISCOVERY_URL` | Set automatically when the discovery service is running; links to the all-sessions page |

---

## Tailscale Integration

When [Tailscale](https://tailscale.com) is installed and running, pi-remote automatically:

===== tau-mirror META =====
name = 'tau-mirror'
version = '1.0.7'
description = 'Web UI that mirrors your Pi terminal session in the browser'
repository.url = 'git+https://github.com/deflating/tau.git'
homepage = 'https://github.com/deflating/tau#readme'
keywords = [ 'pi', 'pi-package', 'coding-agent', 'web-ui', 'mirror' ]
----- README -----
# Tau

A web UI that mirrors your [Pi](https://github.com/badlogic/pi-mono) terminal session in the browser. No separate server — it runs as a Pi extension inside your existing process.

![Tau dark mode](docs/images/dark.png)

![Tau terracotta theme](docs/images/terracotta.png)

![Settings](docs/images/settings.png)

![Commands](docs/images/commands.png)

## What it does

Tau connects to your running Pi TUI and gives you a second view in the browser. Same session, same messages, same tools — just a different screen. Type in the terminal or the browser, both stay in sync.

- **Live mirroring** — streams messages, tool calls, and thinking blocks in real-time
- **Works on any device** — open it on your phone, tablet, or another monitor
- **Session browser** — view history from any past session
- **No extra process** — the Pi extension *is* the server

## Install

```bash
pi install npm:tau-mirror
```

Or from git:

```bash
pi install git:github.com/mattkennelly/tau
```

## Usage

1. Start Pi normally in your terminal
2. Open the URL shown in the status bar (default: `http://localhost:3001`)
3. That's it

Type `/qr` in the terminal to show a QR code and scan it to access via your phone.

## Features

### Chat
- Full markdown rendering with syntax-highlighted code blocks
- Streaming responses with typing indicator
- Image attachments (paste, drag & drop, or button)
- Copy any message with one click
- Inline diff viewer for edit tool calls (red/green lines)
- Scroll-to-bottom button with new message indicator
- Message queuing — type while the agent is working, messages queue and auto-send

### Session Management
- Browse all past sessions grouped by project
- Full-text search across all session history with highlighted snippets
- Sorted by last modified (most recent first)
- Live session marked with a green dot
- Historical sessions are read-only
- Inline session rename
- Favourite sessions, tags, and filtering

### Model & Thinking
- Model picker with search/filter and keyboard support
- Thinking level toggle (off/low/medium/high)
- Token usage percentage with context window visualiser
- Cost tracking per session

### Voice Input
- Mic button in the input area using Web Speech API (on-device dictation)
- Live transcription into the textarea
- Pulses red while recording

### File Browser
- Right sidebar with lazy-loaded file tree
- Navigate directories, open files natively
- Drag files onto the input to insert their path

### Compaction
- Manual context compaction with status display
- Auto-compaction support

### PWA
- Installable as a standalone app on iOS, Android, and macOS
- Custom app icons
- Service worker with network-first caching

## Configuration

Environment variables (set before starting Pi):

| Variable | Default | Description |
|----------|---------|-------------|
| `TAU_MIRROR_PORT` | `3001` | Server port |
| `TAU_STATIC_DIR` | *(bundled)* | Override static files path |

## How it works

Tau is a [Pi extension](https://github.com/badlogic/pi-mono#extensions) that starts an HTTP + WebSocket server inside the Pi process. The extension subscribes to all Pi events and forwards them to connected browser clients. Commands from the browser are executed via the extension API against the same agent session.

```
┌─────────────┐     ┌──────────────────────────────┐     ┌─────────────┐
│  Pi TUI     │     │  Pi Process                  │     │  Browser    │
│  (terminal) │◄───►│                              │◄───►│  (Tau)      │
│             │     │  tau extension               │     │             │
└─────────────┘     │    ↳ HTTP + WS on :3001      │     └─────────────┘
                    └──────────────────────────────┘
```

There's no separate server to run. The extension auto-loads when Pi starts and shuts down when Pi exits.

## Development

Clone and point the extension at the local static files:

```bash
git clone https://github.com/mattkennelly/tau.git
cd tau
TAU_STATIC_DIR=$(pwd)/public pi
```

Edit the files in `public/` — refresh the browser to see changes.

## License

MIT

===== pi-tau-mux-server META =====
name = 'pi-tau-mux-server'
version = '1.0.8'
description = 'Standalone Tau server - aggregates multiple pi coding agent instances into one web UI with Tailscale support'
repository.url = 'git+https://github.com/dwainm/pi-tau-mux-server.git'
homepage = 'https://github.com/dwainm/pi-tau-mux-server#readme'
keywords = [
  'pi',
  'pi-coding-agent',
  'tau',
  'mux',
  'aggregator',
  'web-ui',
  'tailscale'
]
----- README -----
# Pi Tau Mux Server

Standalone server that aggregates multiple [pi coding agent](https://github.com/mariozechner/pi-coding-agent) instances into one unified web UI.

## Architecture

```
┌─────────────┐                    ┌──────────────────────┐                    ┌─────────────┐
│  Pi TUI     │                    │  pi-tau-mux-server   │                    │  Browser    │
│  (terminal) │    WebSocket /pi    │  (standalone daemon) │    WebSocket /ws   │  (Tau UI)  │
│             │◄───────────────────►│                      │◄──────────────────►│             │
└─────────────┘                    │  Aggregates all      │                    └─────────────┘
                                   │  Pi instances        │
┌─────────────┐                    │                      │                    ┌─────────────┐
│  Pi TUI     │                    │  Serves web UI       │                    │  Phone      │
│  (another)  │◄───────────────────►│  Scans sessions      │◄──────────────────►│  (QR scan)  │
└─────────────┘                    └──────────────────────┘                    └─────────────┘
```

**This server** = standalone daemon (web UI, session browser, Tailscale support)
**[pi-tau-mux](https://github.com/dwainm/pi-tau-mux)** = Pi extension (lightweight client that connects here)

## Features

- **Session Browser** — View all Pi sessions across projects with active/stale/ended status
- **Live Mirror** — Connect multiple Pi instances, stream messages in real-time
- **Tailscale Support** — Auto-detects Tailscale IP and MagicDNS for remote access
- **QR Codes** — Scan from phone/tablet to access remotely
- **Status Tracking** — Active (< 3 days), stale (< 30 days), ended detection
- **Clean Names** — Shows `basename-paneId` instead of encoded paths

## Install

```bash
npm install -g pi-tau-mux-server
```

Or run without installing:

```bash
npx pi-tau-mux-server
```

## Usage

Start the server:

```bash
pi-tau-mux-server
```

With custom port:

```bash
TAU_PORT=3001 pi-tau-mux-server
```

Open the URL shown in the output (e.g., `http://localhost:3001`).

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `TAU_PORT` | 3001 | Server port |

## API Endpoints

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/health` | GET | Server health + Tailscale info |
| `/api/sessions` | GET | List all sessions grouped by project |
| `/api/sessions/:dir/:file` | GET | Load session file contents (JSONL) |
| `/api/sessions/switch` | POST | No-op (acknowledges session switch) |
| `/api/qr` | GET | QR code page for mobile access |

## WebSocket Endpoints

| Path | Description |
|------|-------------|
| `/ws` | Browser clients (web UI) |
| `/pi` | Pi client connections ([pi-tau-mux](https://github.com/dwainm/pi-tau-mux) extension) |

## Tailscale

If Tailscale is running, the server auto-detects it:

1. Scans network interfaces for `100.x.x.x` IPs
2. Falls back to `tailscale status --json` CLI if available (for MagicDNS hostname)
3. Uses Tailscale IP/hostname for QR codes

This lets you scan the QR code from any device on your tailnet — no port forwarding needed.

## Session Status

| Status | Description |
|--------|-------------|
| `active` | Modified within last 3 days |
| `stale` | Modified within last 30 days |
| `ended` | Has `session_end` marker or older than 30 days |

## How It Works

1. **Server starts** — HTTP server on configured port, WebSocket endpoints ready
2. **Pi instances connect** — The [pi-tau-mux](https://github.com/dwainm/pi-tau-mux) extension registers each Pi instance
3. **Events stream** — Messages, tool calls, thinking blocks forwarded in real-time
4. **Sessions scanned** — Server reads `~/.pi/agent/sessions` for all historical sessions
5. **Browser connects** — Web UI shows live sessions + session browser

## Related

- **[pi-tau-mux](https://github.com/dwainm/pi-tau-mux)** — The Pi extension (client) that connects to this server
- **[pi-coding-agent](https://github.com/mariozechner/pi-coding-agent)** — The Pi coding agent
- **[Tau (original)](https://github.com/deflating/tau)** — Original implementation (single-instance server)

## Why the Split?

The original Tau ran an HTTP server inside each Pi process. This mux architecture:

- **One server** for all Pi instances — no port conflicts
- **One port** to expose via Tailscale — easy remote access
- **Session aggregation** — see all sessions from one UI
- **Lower overhead** — server runs once, not per Pi instance

## License

MIT

===== pi-interactive-shell META =====
name = 'pi-interactive-shell'
version = '0.13.0'
description = 'Run AI coding agents in pi TUI overlays with interactive, hands-free, and dispatch supervision'
repository.url = 'git+https://github.com/nicobailon/pi-interactive-shell.git'
homepage = 'https://github.com/nicobailon/pi-interactive-shell#readme'
keywords = [
  'pi-package',      'pi',
  'pi-coding-agent', 'extension',
  'interactive',     'shell',
  'terminal',        'tui',
  'subagent',        'claude',
  'gemini',          'codex'
]
----- README -----

# Pi Interactive Shell

An extension for [Pi coding agent](https://github.com/badlogic/pi-mono/) that lets Pi autonomously run interactive CLIs in an observable TUI overlay. Pi controls the subprocess while you watch - take over anytime.

https://github.com/user-attachments/assets/76f56ecd-fc12-4d92-a01e-e6ae9ba65ff4

```typescript
interactive_shell({ command: 'vim config.yaml' })
```

Important: the `interactive_shell({...})` snippets in this README are tool calls made by Pi (or extension/prompt authors). End users do not type these directly into chat. As a user, ask Pi to run something (for example: "run this in dispatch mode") or use `/spawn`, `/attach`, and `/dismiss` commands.

## Why

Some tasks need interactive CLIs - editors, REPLs, database shells, long-running processes. Pi can launch them in an overlay where:

- **User watches** - See exactly what's happening in real-time
- **User takes over** - Type anything to gain control
- **Agent monitors** - Query status, send input, decide when done

Works with any CLI: `vim`, `htop`, `psql`, `ssh`, `docker logs -f`, `npm run dev`, `git rebase -i`, etc.

## Install

```bash
pi install npm:pi-interactive-shell
```

The `interactive-shell` skill is automatically symlinked to `~/.pi/agent/skills/interactive-shell/`.

**Requires:** Node.js. PTY support uses `zigpty` prebuilt binaries (no `node-gyp` toolchain required on supported platforms).

## Modes

| Mode | Agent waits? | How output reaches agent | Best for |
|---|---|---|---|
| **Interactive** (default) | Yes — blocks until exit | Tool return value | Editors, REPLs, SSH — when you need the result now |
| **Hands-free** | No | Poll with `sessionId` | Dev servers, builds — when you want to watch progress and send follow-up commands |
| **Dispatch** | No | Notification on completion via `triggerTurn` | Delegating tasks to subagents — fire and forget |
| **Monitor** | No | Notification on structured monitor trigger events | Watchers, logs, tests, and state checks — wake only when something specific happens |

**Interactive** — The overlay opens, user controls the session, agent waits for it to close. Use for editors (`vim`), database shells (`psql`), or any task where the agent needs the final result immediately.

**Hands-free** — The overlay opens but returns immediately. The agent polls periodically with `sessionId` to check status and get new output. Good for long-running builds or dev servers where you want to react mid-flight (send input, check logs, kill when ready).

**Dispatch** — Returns immediately. No polling. The agent gets woken up via `triggerTurn` only when the session completes (natural exit, timeout, quiet detection, or user kill). The notification includes a tail of the output. This is the default for delegating work to subagents. Add `background: true` to skip the overlay entirely.

**Monitor** — Returns immediately. No polling, no completion notification. The agent gets woken up when a configured monitor trigger emits an event. Supports stream triggers, poll-diff checks, first-class file watching, optional cooldowns, persistence controls, detector commands, and event history queries. Runs headless; attach to inspect if needed.

## Quick Start

The examples below show agent-side tool calls. They are not chat commands for end users.

### Structured Spawn

For Pi, Codex, Claude, and Cursor, the agent can use structured spawn params instead of building command strings by hand:

```typescript
// User says: "Spawn pi so I can edit files interactively"
interactive_shell({ spawn: { agent: "pi" }, mode: "interactive" })

// User says: "Delegate this refactor to codex and notify me when it's done"
interactive_shell({ spawn: { agent: "codex" }, mode: "dispatch" })

// User says: "Ask cursor to review the diffs in dispatch mode"
interactive_shell({ spawn: { agent: "cursor", prompt: "Review the diffs" }, mode: "dispatch" })

// User says: "Ask claude to review the diffs in dispatch mode"
interactive_shell({ spawn: { agent: "claude", prompt: "Review the diffs" }, mode: "dispatch" })

// User says: "Start claude in a worktree for hands-free monitoring"
interactive_shell({ spawn: { agent: "claude", worktree: true }, mode: "hands-free" })

// User says: "Fork my current pi session" (Pi-only)
interactive_shell({ spawn: { mode: "fork" }, mode: "interactive" })
```

Structured `spawn` uses the same resolver and config defaults as the user-facing `/spawn` command. Raw `command` is still supported for arbitrary CLIs and custom launch strings.

For Codex image or design work, Codex can invoke `gpt-image-2` directly from the prompt. Natural language is usually enough, and `$imagegen` forces the image-generation tool when you need it. Attach references with `-i` for edits and iterations. See the bundled `codex-cli` skill for concrete examples. For Cursor CLI-specific command references, see the optional `examples/skills/cursor-cli` skill. Cursor structured spawn defaults to `--model composer-2-fast`, which explicitly selects Cursor's Composer 2 Fast model.

### Interactive

```typescript
// User says: "Open package.json in vim"
interactive_shell({ command: 'vim package.json' })

// User says: "Connect to the postgres database"
interactive_shell({ command: 'psql -d mydb' })

// User says: "SSH into the server"
interactive_shell({ command: 'ssh user@server' })
```

The agent's turn is blocked until the overlay closes. User controls the session directly.

### Hands-Free

```typescript
// Start a long-running process
interactive_shell({
  command: 'npm run dev',
  mode: "hands-free",
  reason: "Dev server"
})
// → { sessionId: "calm-reef", status: "running" }

// User says: "Check on the dev server status"
interactive_shell({ sessionId: "calm-reef" })
// → { status: "running", output: "Server ready on :3000", runtime: 45000 }

// Send input when needed
interactive_shell({ sessionId: "calm-reef", input: "/run review", submit: true })
interactive_shell({ sessionId: "calm-reef", inputKeys: ["ctrl+c"] })

// Kill when done
interactive_shell({ sessionId: "calm-reef", kill: true })
// → { status: "killed", output: "..." }
```

The overlay opens for the user to watch. The agent checks in periodically. User can type anything to take over control. After taking over a monitored hands-free or dispatch session, press `Ctrl+G` to return control to the agent.

### Dispatch

```typescript
// User says: "Delegate refactoring the auth module to pi and notify me when done"
interactive_shell({
  command: 'pi "Refactor the auth module"',
  mode: "dispatch",
  reason: "Auth refactor"
})
// → Returns immediately: { sessionId: "calm-reef" }
// → Agent ends turn or does other work.
```

When the session completes, the agent receives a compact notification on a new turn:

```
Session calm-reef completed successfully (5m 23s). 847 lines of output.

Step 9 of 10
Step 10 of 10
All tasks completed.

Attach to review full output: interactive_shell({ attach: "calm-reef" })
```

The notification includes a brief tail (last 5 lines) and a reattach instruction. The PTY is preserved for 5 minutes so the agent can attach to review full scrollback.

Dispatch defaults `autoExitOnQuiet: true` — the session gets a 15s startup grace period, then is killed after output goes silent (8s by default), which signals completion for task-oriented subagents. Tune the grace period with `handsFree: { gracePeriod: 60000 }` or opt out entirely with `handsFree: { autoExitOnQuiet: false }`.

The overlay still shows for the user, who can Ctrl+T to transfer output, Ctrl+B to background, take over by typing, or Ctrl+Q for more options. `Ctrl+G` only becomes meaningful after the user has taken over a monitored hands-free or dispatch session.

### Background Dispatch (Headless)

```typescript
// No overlay — runs completely invisibly
interactive_shell({
  command: 'pi "Fix all lint errors"',
  mode: "dispatch",
  background: true
})
// → { sessionId: "calm-reef" }
// → User can /attach calm-reef to peek
// → Agent notified on completion, same as regular dispatch
```

Multiple headless dispatches can run concurrently alongside a single interactive overlay. This is how you parallelize subagent work — fire off three background dispatches and process results as each completion notification arrives.

### Monitor (Event-Driven)

These examples are **agent tool calls**. End users should ask in natural language (for example: "watch my tests and alert me on failures"), and Pi should invoke `interactive_shell` with the monitor config.

Wake the agent when monitor triggers emit events — no polling and no waiting for process completion.

```typescript

===== openclaude-web META =====
name = 'openclaude-web'
version = '0.2.0'
description = 'Local web UI for openclaude / Claude Code — claude.ai/code-style chat in your browser, runs on your phone or laptop. Spawns openclaude or claude as a subprocess and streams events over WebSocket.'
repository.url = 'git+https://github.com/sealjeal/openclaude-web.git'
homepage = 'https://github.com/sealjeal/openclaude-web#readme'
keywords = [
  'openclaude',
  'claude-code',
  'claude',
  'web-ui',
  'chat-ui',
  'termux',
  'ai-agent',
  'local-ui'
]
----- README -----
# openclaude-web

A local web UI for [openclaude](https://www.npmjs.com/package/@gitlawb/openclaude) and [Claude Code](https://www.npmjs.com/package/@anthropic-ai/claude-code), inspired by claude.ai/code. Runs entirely on your machine (Termux on Android, Linux, macOS) — opens in your browser, talks to your local agent over WebSocket.

```
┌─────────── Browser (anywhere on LAN) ────────────┐
│  React SPA — sidebar, conversation, tool cards   │
└───────────────────────┬──────────────────────────┘
                        │  WebSocket
┌───────────────────────▼──────────────────────────┐
│  Local Node server (Fastify + ws)                 │
│  Spawns openclaude / claude subprocesses          │
│  …or relays a live TUI session via /web-control   │
└──────────────────────────────────────────────────┘
```

## Quick start

```bash
npm install -g openclaude-web      # auto-patches your openclaude bundle
openclaude-web                     # starts the server on :3009
```

Then in another terminal:

```bash
openclaude --resume <some-session>
> /web-control                      # the slash command
```

The TUI emits a clickable URL like `http://127.0.0.1:3009/?session=…`. Open it in any browser — you get a live two-way mirror of that exact session. Type from either side; both UIs update.

That's the whole flow: **install → run server → /web-control → open URL.** No config, no auth, no cloud.

## What you get

- **Live mirror.** Whatever shows up in the TUI shows up in the browser, and vice versa. Type from your phone's browser, watch the TUI on your laptop respond, or the other way around.
- **Permission prompts in the browser.** When the AI tries to run something that needs permission (Bash, file write, etc.), the prompt pops up in both places — answer in either, both dismiss.
- **Mode picker** above the composer (Ask / Accept edits / Plan / Auto / Bypass) — same shift-tab modes the TUI has.
- **Stop button.** Hitting stop in the browser interrupts the running tool / model call in the TUI exactly like Esc twice.
- **`/web-control` again** while connected opens the cloud-style menu (Disconnect / Show QR / Continue) — pick Disconnect and the chat shows `> /web-control \n ⎿ Web disconnected.`, same shape as cloud's `/remote-control`.
- **Indicator pill** at the bottom right of the TUI shows live bridge status. Press Enter on it to open the status dialog (`d` to disconnect, space for QR).
- **Sidebar** of all your past openclaude + claude sessions, grouped by date, searchable.
- **Click any sidebar session** → resumes via `--resume`, replays full JSONL history, then streams live.
- **Streaming events** render claude.ai/code-style: peachy user bubbles, markdown-with-syntax-highlight assistant text, one-line tool rows that expand on click, real word-level edit diffs, TodoWrite checklists, terminal-style Bash blocks, cost + duration after each turn.
- **PATH-mode attachments** for non-Anthropic backends — uploads land on disk and the TUI sees a path, bypassing the per-message image cap.
- **`?session=<id>` deep link** — when /web-control prints a URL, that URL takes the browser straight to that session. Strict mirror-only mode: if the TUI is gone, the page shows an offline banner instead of silently spawning a sidecar agent.

## Install / run

```bash
npm install -g openclaude-web      # patches openclaude bundle on install
openclaude-web                     # listen on http://0.0.0.0:3009
openclaude-web --port 8080         # custom port
openclaude-web --host 127.0.0.1    # bind only loopback
openclaude-web --help
```

The bin auto-patches the `@gitlawb/openclaude` bundle on first start (idempotent, with a backup at `cli.mjs.ocw-bak`). If you install openclaude AFTER openclaude-web, the patches apply on the next `openclaude-web` start.

```bash
openclaude-web patch-openclaude            # manually re-patch
openclaude-web patch-openclaude --check    # report patch status (exit 0 if patched)
openclaude-web patch-openclaude --unpatch  # restore the original bundle
```

## How the bridge works

The TUI's `useReplBridge` React hook (originally for cloud `/remote-control`) opens a WebSocket. We patch the bundle to point that WebSocket at `ws://127.0.0.1:3009` instead of `wss://api.anthropic.com`, and our Fastify server runs a relay that fans events between the TUI publisher and any subscribed browser tabs. When you type in the browser, we package it as a `{type:"event", data:{type:"user", message:…}}` envelope and `sendToPublisher`s it to the TUI which calls `enqueue()`. Going through openclaude's own `extractInboundMessageFields` means image content arrays (with `normalizeImageBlocks`) round-trip cleanly. Outbound, every entry the TUI adds to its `messages` array is forwarded as `{type:"event", data:<event>}` for the browser to render.

Permission prompts, mode toggles, stop, and the dialog menu all use the same wire-up — small custom envelope types (`_bridge_permission_request`, `_bridge_mode_changed`, `_bridge_interrupt`, etc.) flow through the same WebSocket relay. Every patch is idempotent and the patcher keeps a versioned marker in the bundle so it auto-upgrades cleanly across openclaude-web releases.

## Requirements

- Node.js ≥ 20
- Either:
  - [`openclaude`](https://www.npmjs.com/package/@gitlawb/openclaude) installed (any session in `~/.openclaude/projects/`)
  - And/or [Claude Code](https://www.npmjs.com/package/@anthropic-ai/claude-code) installed (sessions in `~/.claude/projects/`)
- Authentication for whichever agent you use (run it once interactively to log in)

## Environment overrides

| Var                       | Default                                  | What                                                                            |
|---------------------------|------------------------------------------|---------------------------------------------------------------------------------|
| `PORT`                    | `3009`                                   | Server listen port                                                              |
| `HOST`                    | `0.0.0.0`                                | Server bind host                                                                |
| `OPENCLAUDE_CLI`          | `which openclaude`                       | Override the openclaude binary path                                              |
| `CLAUDE_BIN`              | `which claude`                           | Override the claude binary path                                                  |
| `OPENCLAUDE_BRIDGE_URL`   | `ws://127.0.0.1:3009`                    | WS URL the TUI's `/web-control` connects to                                      |
| `OPENCLAUDE_NO_BRIDGE`    | unset                                    | Set to `1` in spawned subprocesses to force-disable bridge auto-attach           |
| `OPENCLAUDE_WEB_UPLOADS_DIR` | `/sdcard/Download/openclaude-web-uploads` (Termux) or `~/openclaude-web-uploads` | Where browser-uploaded attachments are stored on disk for PATH-mode             |

## License

MIT


x/package/docs/plans/2026-03-10-discovery-service.md:414:async function deregisterSession(sessionId: string): Promise<void> {
x/package/docs/plans/2026-03-10-discovery-service.md:415:	await fetchDiscovery(`/api/sessions/${sessionId}`, { method: "DELETE" });
x/package/docs/plans/2026-03-10-discovery-service.md:421:Replace the token generation and Tailscale setup. The session still does its own Tailscale serve for `/pi/{sessionId}/`, but gets the token from the discovery service.
x/package/docs/plans/2026-03-10-discovery-service.md:428:- The session still sets up its own Tailscale route at `/pi/{sessionId}/`
x/package/docs/plans/2026-03-10-discovery-service.md:454:git commit -m "feat: integrate discovery service into session startup/shutdown"
x/package/docs/plans/2026-03-10-discovery-service.md:471:	contentLines.push("  \x1b[1;32mAll sessions:\x1b[0m " + discoveryUrl);
x/package/docs/plans/2026-03-10-discovery-service.md:509:1. Start first session: `/remote` → verify discovery service spawns, widget shows all-sessions URL
x/package/docs/plans/2026-03-10-discovery-service.md:510:2. Start second session in another terminal: verify it gets the same token, registers
x/package/docs/plans/2026-03-10-discovery-service.md:511:3. Open discovery URL in browser → verify card layout shows both sessions
x/package/docs/plans/2026-03-10-discovery-service.md:512:4. Click a session card → verify it opens the terminal
x/package/docs/plans/2026-03-10-discovery-service.md:513:5. Exit one session → verify its card disappears from discovery
x/package/docs/plans/2026-03-10-discovery-service.md:514:6. Exit last session → verify discovery service shuts down and Tailscale route is cleaned up
x/package/package.json:4:	"description": "Remote terminal access for pi via WebSocket and browser, with Tailscale integration",

===== tau-mirror SIGNALS =====
x/package/extensions/mirror-server.ts:5: * allowing a browser to connect and mirror the TUI session in real-time.
x/package/extensions/mirror-server.ts:7: * - Forwards all Pi events to connected browser clients
x/package/extensions/mirror-server.ts:8: * - Accepts commands from the browser and executes them via the extension API
x/package/extensions/mirror-server.ts:56:const SESSIONS_DIR = path.join(process.env.HOME || "~", ".pi/agent/sessions");
x/package/extensions/mirror-server.ts:60:function registerInstance(port: number, sessionFile: string, cwd: string) {
x/package/extensions/mirror-server.ts:62:  const info = { port, pid: process.pid, sessionFile, cwd, startedAt: new Date().toISOString() };
x/package/extensions/mirror-server.ts:66:function updateInstanceSession(sessionFile: string) {
x/package/extensions/mirror-server.ts:71:    info.sessionFile = sessionFile;
x/package/extensions/mirror-server.ts:80:function getRunningInstances(): Array<{ port: number; pid: number; sessionFile: string; cwd: string }> {
x/package/extensions/mirror-server.ts:123:  // Pending RPC-style requests from browser (id -> resolver)
x/package/extensions/mirror-server.ts:148:  let tailscaleUrl = "";
x/package/extensions/mirror-server.ts:154:    description: "Open Tau web UI in browser",
x/package/extensions/mirror-server.ts:175:      // Open in default browser
x/package/extensions/mirror-server.ts:198:      // Forward event to all connected browser clients
x/package/extensions/mirror-server.ts:204:  // Also capture context from session events
x/package/extensions/mirror-server.ts:210:  pi.on("session_start", async (_event, ctx) => {
x/package/extensions/mirror-server.ts:215:    // Update instance registry with new session file
x/package/extensions/mirror-server.ts:216:    updateInstanceSession(ctx.sessionManager.getSessionFile() || "");
x/package/extensions/mirror-server.ts:241:    const sessionName = pi.getSessionName();
x/package/extensions/mirror-server.ts:242:    if (sessionName && sessionName !== "New Session" && sessionName !== "Untitled") {
x/package/extensions/mirror-server.ts:253:      broadcast({ type: "event", event: { type: "session_name", name: title } });
x/package/extensions/mirror-server.ts:309:    // Get session entries for message history
x/package/extensions/mirror-server.ts:310:    const entries = ctx.sessionManager.getEntries();
x/package/extensions/mirror-server.ts:315:    const sessionName = pi.getSessionName();
x/package/extensions/mirror-server.ts:316:    const sessionFile = ctx.sessionManager.getSessionFile();
x/package/extensions/mirror-server.ts:326:      sessionName,
x/package/extensions/mirror-server.ts:327:      sessionFile,
x/package/extensions/mirror-server.ts:334:  // Handle commands from browser clients
x/package/extensions/mirror-server.ts:431:            sessionFile: ctx.sessionManager.getSessionFile(),
x/package/extensions/mirror-server.ts:432:            sessionName: pi.getSessionName(),
x/package/extensions/mirror-server.ts:444:          const entries = ctx.sessionManager.getEntries();
x/package/extensions/mirror-server.ts:525:        case "get_session_stats": {
x/package/extensions/mirror-server.ts:527:            sendTo(ws, error("get_session_stats", "No context available"));
x/package/extensions/mirror-server.ts:531:          const entries = ctx.sessionManager.getEntries();
x/package/extensions/mirror-server.ts:540:          sendTo(ws, success("get_session_stats", {
x/package/extensions/mirror-server.ts:541:            sessionFile: ctx.sessionManager.getSessionFile(),
x/package/extensions/mirror-server.ts:551:        case "set_session_name": {
x/package/extensions/mirror-server.ts:554:            sendTo(ws, error("set_session_name", "Name cannot be empty"));
x/package/extensions/mirror-server.ts:558:          sendTo(ws, success("set_session_name"));
x/package/extensions/mirror-server.ts:593:            const sessionFile = ctx.sessionManager.getSessionFile();
x/package/extensions/mirror-server.ts:594:            if (!sessionFile) throw new Error("No session file to export");
x/package/extensions/mirror-server.ts:597:              ? `"${sessionFile}" "${command.outputPath}"`
x/package/extensions/mirror-server.ts:598:              : `"${sessionFile}"`;
x/package/extensions/mirror-server.ts:601:            const result = output.trim().split("\n").pop() || sessionFile.replace(".jsonl", ".html");
x/package/extensions/mirror-server.ts:674:  // API routes (sessions list, etc.)
x/package/extensions/mirror-server.ts:695:      if (tailscaleUrl) qrPromises.push(QRCode.toDataURL(tailscaleUrl, { width: 256, margin: 2 }));
x/package/extensions/mirror-server.ts:697:        const tsSection = tailscaleUrl && dataUrls[1]
x/package/extensions/mirror-server.ts:698:          ? `<p style="margin-top:24px;color:rgba(255,255,255,0.3);font-size:11px">TAILSCALE</p><img src="${dataUrls[1]}" width="256" height="256" alt="Tailscale QR"><a href="${tailscaleUrl}">${tailscaleUrl}</a>`
x/package/extensions/mirror-server.ts:715:      res.end(JSON.stringify({ status: "ok", mode: "mirror", mirrorUrl, tailscaleUrl: tailscaleUrl || undefined }));
x/package/extensions/mirror-server.ts:725:    if (urlPath === "/api/sessions" && req.method === "GET") {
x/package/extensions/mirror-server.ts:730:    // Full-text search across sessions
x/package/extensions/mirror-server.ts:738:    // File browser: list directory
x/package/extensions/mirror-server.ts:747:            const entries = latestCtx.sessionManager.getEntries();
x/package/extensions/mirror-server.ts:748:            const sessionEntry = entries.find((e: any) => e.type === "session");
x/package/extensions/mirror-server.ts:749:            if (sessionEntry?.cwd) dirPath = sessionEntry.cwd;
x/package/extensions/mirror-server.ts:760:    // File browser: open file natively
x/package/extensions/mirror-server.ts:786:    // Session file endpoint: /api/sessions/:dirName/:file
x/package/extensions/mirror-server.ts:787:    const sessionMatch = urlPath.match(/^\/api\/sessions\/([^/]+)\/([^/]+)$/);
x/package/extensions/mirror-server.ts:788:    if (sessionMatch && req.method === "GET") {
x/package/extensions/mirror-server.ts:789:      serveSessionFile(res, sessionMatch[1], sessionMatch[2]);
x/package/extensions/mirror-server.ts:819:    // Session switch — in mirror mode, this is a no-op (session is controlled by TUI)
x/package/extensions/mirror-server.ts:820:    if (urlPath === "/api/sessions/switch" && req.method === "POST") {
x/package/extensions/mirror-server.ts:837:      // Get tmux pane PIDs
x/package/extensions/mirror-server.ts:838:      const paneOutput = execSync("tmux list-panes -a -F '#{pane_pid}' 2>/dev/null", { encoding: "utf8" });
x/package/extensions/mirror-server.ts:839:      const tmuxFiles = new Set<string>();
x/package/extensions/mirror-server.ts:843:          // Find Pi (node) processes that are children of tmux shells
x/package/extensions/mirror-server.ts:850:              if (match) tmuxFiles.add(match[0]);
x/package/extensions/mirror-server.ts:855:      return tmuxFiles;
x/package/extensions/mirror-server.ts:869:      const tmuxFiles = getTmuxSessionFiles();
x/package/extensions/mirror-server.ts:881:        const sessions: any[] = [];
x/package/extensions/mirror-server.ts:889:              const isTmux = tmuxFiles.has(filePath);
x/package/extensions/mirror-server.ts:890:              sessions.push({ ...parsed, file, filePath, mtime: stat.mtimeMs, ...(isTmux && { tmux: true }) });
x/package/extensions/mirror-server.ts:895:        sessions.sort((a, b) => b.mtime - a.mtime);
x/package/extensions/mirror-server.ts:897:        if (sessions.length > 0) {
x/package/extensions/mirror-server.ts:898:          projects.push({ path: decodedPath, dirName: dir.name, sessions });
x/package/extensions/mirror-server.ts:903:        const aTime = a.sessions[0]?.mtime || 0;
x/package/extensions/mirror-server.ts:904:        const bTime = b.sessions[0]?.mtime || 0;
x/package/extensions/mirror-server.ts:958:  // Parse session file header
x/package/extensions/mirror-server.ts:966:    let sessionName: string | null = null;
x/package/extensions/mirror-server.ts:976:        if (entry.type === "session") header = entry;
x/package/extensions/mirror-server.ts:977:        else if (entry.type === "session_info" && entry.name) sessionName = entry.name;
x/package/extensions/mirror-server.ts:1003:      name: sessionName,
x/package/extensions/mirror-server.ts:1010:  // File browser
x/package/extensions/mirror-server.ts:1104:            let sessionId = "";
x/package/extensions/mirror-server.ts:1105:            let sessionName = "";
x/package/extensions/mirror-server.ts:1106:            let sessionTimestamp = "";
x/package/extensions/mirror-server.ts:1115:                if (entry.type === "session") {
x/package/extensions/mirror-server.ts:1116:                  sessionId = entry.id;
x/package/extensions/mirror-server.ts:1117:                  sessionTimestamp = entry.timestamp || "";
x/package/extensions/mirror-server.ts:1119:                if (entry.type === "session_info" && entry.name) {
x/package/extensions/mirror-server.ts:1120:                  sessionName = entry.name;
x/package/extensions/mirror-server.ts:1146:                    if (matches.length >= 3) break; // max 3 matches per session
x/package/extensions/mirror-server.ts:1159:                sessionId,
x/package/extensions/mirror-server.ts:1160:                sessionName,
x/package/extensions/mirror-server.ts:1161:                sessionTimestamp,
x/package/extensions/mirror-server.ts:1181:  pi.on("session_start", async (_event, ctx) => {
x/package/extensions/mirror-server.ts:1228:        console.log("[Mirror] Browser client disconnected");
x/package/extensions/mirror-server.ts:1238:    // Heartbeat keeps mobile/Tailscale sessions alive and removes stale clients.
x/package/extensions/mirror-server.ts:1303:      let tailscaleIp = "";
x/package/extensions/mirror-server.ts:1307:            tailscaleIp = net.address;
x/package/extensions/mirror-server.ts:1311:        if (tailscaleIp) break;
x/package/extensions/mirror-server.ts:1315:      tailscaleUrl = tailscaleIp ? `http://${tailscaleIp}:${port}` : "";
x/package/extensions/mirror-server.ts:1316:      console.log(`[Mirror] Tau mirror server running on ${mirrorUrl}${tailscaleUrl ? `  •  Tailscale: ${tailscaleUrl}` : ""}`);
x/package/extensions/mirror-server.ts:1317:      ctx.ui.setStatus("mirror", `Mirror: ${localIp}:${port}${tailscaleIp ? ` • TS: ${tailscaleIp}:${port}` : ""}`);
x/package/extensions/mirror-server.ts:1320:      const sessionFile = ctx.sessionManager.getSessionFile() || "";
x/package/extensions/mirror-server.ts:1321:      registerInstance(port, sessionFile, ctx.cwd || process.cwd());
x/package/extensions/mirror-server.ts:1323:      ctx.ui.notify(`Tau mirror: ${mirrorUrl}${tailscaleUrl ? `  •  Tailscale: ${tailscaleUrl}` : ""}  •  /qr for QR code`, "info");
x/package/extensions/mirror-server.ts:1332:  pi.on("session_shutdown", async () => {
x/package/extensions/imessage-bridge.ts:309:  pi.on("session_start", async (_event, ctx) => {
x/package/extensions/imessage-bridge.ts:335:  pi.on("session_shutdown", async () => {
x/package/extras/com.tau.pi-daemon.plist:9:        <string>/opt/homebrew/bin/tmux</string>
x/package/extras/com.tau.pi-daemon.plist:10:        <string>new-session</string>
x/package/README.md:3:A web UI that mirrors your [Pi](https://github.com/badlogic/pi-mono) terminal session in the browser. No separate server — it runs as a Pi extension inside your existing process.
x/package/README.md:15:Tau connects to your running Pi TUI and gives you a second view in the browser. Same session, same messages, same tools — just a different screen. Type in the terminal or the browser, both stay in sync.
x/package/README.md:19:- **Session browser** — view history from any past session
x/package/README.md:36:1. Start Pi normally in your terminal
x/package/README.md:40:Type `/qr` in the terminal to show a QR code and scan it to access via your phone.
x/package/README.md:54:- Browse all past sessions grouped by project
x/package/README.md:55:- Full-text search across all session history with highlighted snippets
x/package/README.md:57:- Live session marked with a green dot

===== pi-tau-mux-server SIGNALS =====
x/package/README.md:10:│  (terminal) │    WebSocket /pi    │  (standalone daemon) │    WebSocket /ws   │  (Tau UI)  │
x/package/README.md:16:│  (another)  │◄───────────────────►│  Scans sessions      │◄──────────────────►│  (QR scan)  │
x/package/README.md:20:**This server** = standalone daemon (web UI, session browser, Tailscale support)
x/package/README.md:25:- **Session Browser** — View all Pi sessions across projects with active/stale/ended status
x/package/README.md:71:| `/api/sessions` | GET | List all sessions grouped by project |
x/package/README.md:72:| `/api/sessions/:dir/:file` | GET | Load session file contents (JSONL) |
x/package/README.md:73:| `/api/sessions/switch` | POST | No-op (acknowledges session switch) |
x/package/README.md:88:2. Falls back to `tailscale status --json` CLI if available (for MagicDNS hostname)
x/package/README.md:99:| `ended` | Has `session_end` marker or older than 30 days |
x/package/README.md:106:4. **Sessions scanned** — Server reads `~/.pi/agent/sessions` for all historical sessions
x/package/README.md:107:5. **Browser connects** — Web UI shows live sessions + session browser
x/package/README.md:121:- **Session aggregation** — see all sessions from one UI
x/package/package.json:41:    "tailscale"
x/package/src/server.js:5: * - Serves web UI for browser clients
x/package/src/server.js:7: * - Maintains session list by scanning ~/.pi/agent/sessions
x/package/src/server.js:21:const SESSIONS_DIR = path.join(HOME, '.pi/agent/sessions');
x/package/src/server.js:49:const browserClients = new Set();
x/package/src/server.js:50:const piClients = new Map(); // sessionId -> ws
x/package/src/server.js:70:let tailscaleInfo = null;
x/package/src/server.js:75:    const status = execSync('tailscale status --json 2>/dev/null', { encoding: 'utf8', timeout: 2000 });
x/package/src/server.js:81:        const tailscaleIp = self.TailscaleIPs?.[0] || null;
x/package/src/server.js:84:        if (tailscaleIp || hostname) {
x/package/src/server.js:86:            ip: tailscaleIp,
x/package/src/server.js:114:  if (tailscaleInfo?.hostname) {
x/package/src/server.js:115:    return `http://${tailscaleInfo.hostname}:${port}`;
x/package/src/server.js:118:  if (tailscaleInfo?.ip) {
x/package/src/server.js:119:    return `http://${tailscaleInfo.ip}:${port}`;
x/package/src/server.js:129:function hasEndMarker(sessionFile) {
x/package/src/server.js:131:    const stats = fs.statSync(sessionFile);
x/package/src/server.js:135:    const fd = fs.openSync(sessionFile, 'r');
x/package/src/server.js:138:    return buffer.toString('utf8').includes('"type":"session_end"');
x/package/src/server.js:144:function getSessionStatus(sessionFile) {
x/package/src/server.js:145:  if (!fs.existsSync(sessionFile)) return 'ended';
x/package/src/server.js:148:    const stat = fs.statSync(sessionFile);
x/package/src/server.js:151:    if (hasEndMarker(sessionFile)) return 'ended';
x/package/src/server.js:213:  let sessionName = null;
x/package/src/server.js:223:      if (entry.type === 'session') header = entry;
x/package/src/server.js:224:      else if (entry.type === 'session_info' && entry.name) sessionName = entry.name;
x/package/src/server.js:250:    name: sessionName,
x/package/src/server.js:272:    const sessionsByCwd = new Map();
x/package/src/server.js:288:            const session = {
x/package/src/server.js:300:            allSessions.push(session);
x/package/src/server.js:302:            if (!sessionsByCwd.has(cwd)) {
x/package/src/server.js:303:              sessionsByCwd.set(cwd, []);
x/package/src/server.js:305:            sessionsByCwd.get(cwd).push(session);
x/package/src/server.js:317:      const sessions = allSessions.filter(s => {
x/package/src/server.js:322:      if (sessions.length === 0) continue;
x/package/src/server.js:324:      sessions.sort((a, b) => b.mtime - a.mtime);
x/package/src/server.js:327:      const allSessionsInThisDir = sessionsByCwd.get(cwd) || [];
x/package/src/server.js:338:        sessions,
x/package/src/server.js:412:      browserClients: browserClients.size,
x/package/src/server.js:414:      tailscale: tailscaleInfo
x/package/src/server.js:425:  if (cleanPath === '/api/sessions' && req.method === 'GET') {
x/package/src/server.js:442:  // Switch session (no-op in mux mode - just acknowledge)
x/package/src/server.js:443:  if (cleanPath === '/api/sessions/switch' && req.method === 'POST') {
x/package/src/server.js:449:  // Load session file contents: /api/sessions/:dirName/:fileName
x/package/src/server.js:450:  const sessionMatch = urlPath.match(/^\/api\/sessions\/([^/]+)\/(.+)$/);
x/package/src/server.js:451:  if (sessionMatch && req.method === 'GET') {
x/package/src/server.js:452:    const dirName = sessionMatch[1];
x/package/src/server.js:453:    const fileName = sessionMatch[2];
x/package/src/server.js:454:    const sessionFile = path.join(SESSIONS_DIR, dirName, fileName);
x/package/src/server.js:456:    if (!fs.existsSync(sessionFile)) {
x/package/src/server.js:464:      const content = fs.readFileSync(sessionFile, 'utf8');
x/package/src/server.js:475:      res.end(JSON.stringify({ error: 'Failed to read session file' }));
x/package/src/server.js:492:  browserClients.add(ws);
x/package/src/server.js:498:    console.log('[Tau Mux] Browser client disconnected');
x/package/src/server.js:499:    browserClients.delete(ws);
x/package/src/server.js:503:    browserClients.delete(ws);
x/package/src/server.js:526:    console.log('[Tau Mux] Pi client disconnected');
x/package/src/server.js:528:    for (const [sessionId, client] of piClients.entries()) {
x/package/src/server.js:530:        piClients.delete(sessionId);
x/package/src/server.js:554:      piClients.set(msg.sessionId, ws);
x/package/src/server.js:555:      console.log(`[Tau Mux] Registered session: ${msg.sessionId}`);
x/package/src/server.js:561:          tailscale: tailscaleInfo ? (tailscaleInfo.ip ? `http://${tailscaleInfo.ip}:${PORT}` : null) : null,
x/package/src/server.js:562:          magicDns: tailscaleInfo?.hostname ? `http://${tailscaleInfo.hostname}:${PORT}` : null
x/package/src/server.js:565:      broadcastToBrowsers({ type: 'pi_registered', sessionId: msg.sessionId });
x/package/src/server.js:569:      piClients.delete(msg.sessionId);
x/package/src/server.js:570:      console.log(`[Tau Mux] Unregistered session: ${msg.sessionId}`);
x/package/src/server.js:571:      broadcastToBrowsers({ type: 'pi_unregistered', sessionId: msg.sessionId });
x/package/src/server.js:575:      // Forward events to browser clients
x/package/src/server.js:580:      // Update state for this session
x/package/src/server.js:581:      broadcastToBrowsers({ type: 'state_update', sessionId: msg.sessionId, state: msg.state });
x/package/src/server.js:588:  for (const client of browserClients) {
x/package/src/server.js:600:  for (const client of browserClients) {
x/package/src/server.js:602:      browserClients.delete(client);
x/package/src/server.js:608:      browserClients.delete(client);
x/package/src/server.js:650:tailscaleInfo = detectTailscale();
x/package/src/server.js:657:  if (tailscaleInfo) {
x/package/src/server.js:658:    console.log(`[Tau Mux] 🐺 Tailscale detected (${tailscaleInfo.tailnet || 'tailnet'})`);
x/package/src/server.js:659:    if (tailscaleInfo.hostname) {
x/package/src/server.js:660:      console.log(`[Tau Mux] MagicDNS: http://${tailscaleInfo.hostname}:${PORT}`);
x/package/src/server.js:662:    if (tailscaleInfo.ip) {
x/package/src/server.js:663:      console.log(`[Tau Mux] Tailscale IP: http://${tailscaleInfo.ip}:${PORT}`);
x/package/src/server.js:669:  console.log(`[Tau Mux] WebSocket endpoints: /ws (browser), /pi (pi clients)`);
x/package/public/websocket-client.js:10:    this.reconnectAttempts = 0;
x/package/public/websocket-client.js:12:    this.reconnectDelay = 1000;
x/package/public/websocket-client.js:15:    this.reconnectTimer = null;
x/package/public/websocket-client.js:26:    if (this.reconnectTimer) {
x/package/public/websocket-client.js:27:      clearTimeout(this.reconnectTimer);
x/package/public/websocket-client.js:28:      this.reconnectTimer = null;
x/package/public/websocket-client.js:30:    // Close only fully stale sockets before reconnecting
x/package/public/websocket-client.js:38:      this.reconnectAttempts = 0;
x/package/public/websocket-client.js:60:      this.dispatchEvent(new CustomEvent('disconnected'));
x/package/public/websocket-client.js:68:  disconnect() {
x/package/public/websocket-client.js:71:    if (this.reconnectTimer) {
x/package/public/websocket-client.js:72:      clearTimeout(this.reconnectTimer);
x/package/public/websocket-client.js:73:      this.reconnectTimer = null;
x/package/public/websocket-client.js:80:  // Force reconnect — resets attempt counter and connects fresh
x/package/public/websocket-client.js:82:    this.reconnectAttempts = 0;
x/package/public/websocket-client.js:86:      try { this.ws.close(1000, 'force reconnect'); } catch (e) {}
x/package/public/websocket-client.js:88:    if (this.reconnectTimer) {
x/package/public/websocket-client.js:89:      clearTimeout(this.reconnectTimer);
x/package/public/websocket-client.js:90:      this.reconnectTimer = null;
x/package/public/websocket-client.js:96:    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
x/package/public/websocket-client.js:97:      console.error('[WS] Max reconnection attempts reached');
x/package/public/websocket-client.js:98:      this.dispatchEvent(new CustomEvent('reconnectFailed'));
x/package/public/websocket-client.js:102:    this.reconnectAttempts++;
x/package/public/websocket-client.js:103:    const delay = Math.min(this.maxReconnectDelay, this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1));
x/package/public/websocket-client.js:105:    console.log(`[WS] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
x/package/public/websocket-client.js:107:    this.reconnectTimer = setTimeout(() => {

===== pi-interactive-shell SIGNALS =====
x/package/types.ts:12:	sessionId?: string;
x/package/types.ts:40:	status: "running" | "user-takeover" | "exited" | "killed" | "agent-resumed";
x/package/types.ts:41:	sessionId: string;
x/package/types.ts:82:	persistence?: {
x/package/types.ts:97:	sessionId: string;
x/package/types.ts:105:	stream: "pty";
x/package/types.ts:111:	sessionId: string;
x/package/types.ts:121:	terminalReason?: MonitorTerminalReason;
x/package/types.ts:126:/** Options for starting or reattaching an interactive shell session. */
x/package/types.ts:132:	/** Original session start time in ms since epoch, preserved across background/reattach transitions. */
x/package/types.ts:143:	sessionId?: string; // Pre-generated sessionId for non-blocking modes
x/package/types.ts:155:	// When true, unregister active session on completion (blocking tool call path).
x/package/types.ts:158:	// Existing PTY session (for attach flow -- skip creating a new PTY)
x/package/types.ts:159:	existingSession?: import("./pty-session.js").PtyTerminalSession;
x/package/tool-schema.ts:13:- interactive (default): User supervises and controls the session
x/package/tool-schema.ts:31:When mode="hands-free", the tool returns IMMEDIATELY with a sessionId.
x/package/tool-schema.ts:35:1. Start session: interactive_shell({ command: 'pi "Fix bugs"', mode: "hands-free" })
x/package/tool-schema.ts:36:   -> Returns immediately with sessionId
x/package/tool-schema.ts:37:2. Check status/output: interactive_shell({ sessionId: "calm-reef" })
x/package/tool-schema.ts:39:3. When task is done: interactive_shell({ sessionId: "calm-reef", kill: true })
x/package/tool-schema.ts:40:   -> Kills session and returns final output
x/package/tool-schema.ts:48:- interactive_shell({ sessionId: "calm-reef" }) - get status + rendered terminal output (default: 20 lines, 5KB)
x/package/tool-schema.ts:49:- interactive_shell({ sessionId: "calm-reef", outputLines: 50 }) - get more lines (max: 200)
x/package/tool-schema.ts:50:- interactive_shell({ sessionId: "calm-reef", outputMaxChars: 20000 }) - get more content (max: 50KB)
x/package/tool-schema.ts:51:- interactive_shell({ sessionId: "calm-reef", outputOffset: 0, outputLines: 50 }) - pagination (lines 0-49)
x/package/tool-schema.ts:52:- interactive_shell({ sessionId: "calm-reef", incremental: true }) - get next N unseen lines (server tracks position)
x/package/tool-schema.ts:53:- interactive_shell({ sessionId: "calm-reef", drain: true }) - only NEW output since last query (raw stream)
x/package/tool-schema.ts:54:- interactive_shell({ sessionId: "calm-reef", kill: true }) - end session
x/package/tool-schema.ts:55:- interactive_shell({ sessionId: "calm-reef", input: "..." }) - send input
x/package/tool-schema.ts:70:- interactive_shell({ sessionId: "calm-reef", input: "/help", submit: true }) - type text and press Enter
x/package/tool-schema.ts:71:- interactive_shell({ sessionId: "calm-reef", inputKeys: ["ctrl+c"] }) - named keys
x/package/tool-schema.ts:72:- interactive_shell({ sessionId: "calm-reef", inputKeys: ["up", "up", "enter"] }) - multiple keys
x/package/tool-schema.ts:73:- interactive_shell({ sessionId: "calm-reef", inputHex: ["0x1b", "0x5b", "0x41"] }) - raw escape sequences
x/package/tool-schema.ts:74:- interactive_shell({ sessionId: "calm-reef", inputPaste: "multiline\\ntext" }) - bracketed paste (prevents auto-execution)
x/package/tool-schema.ts:85:When mode="dispatch", the tool returns IMMEDIATELY with a sessionId.
x/package/tool-schema.ts:86:You do NOT need to poll. You'll be notified automatically when the session completes.
x/package/tool-schema.ts:89:1. Start session: interactive_shell({ command: 'pi "Fix bugs"', mode: "dispatch" })
x/package/tool-schema.ts:90:   -> Returns immediately with sessionId
x/package/tool-schema.ts:92:3. When complete, you receive a notification with the session output
x/package/tool-schema.ts:95:You can still query with sessionId if needed, but it's not required.
x/package/tool-schema.ts:98:Start a session without any overlay. Process runs headlessly, agent notified on completion:
x/package/tool-schema.ts:110:- interactive_shell({ sessionId: "calm-reef", background: true })
x/package/tool-schema.ts:113:Open an overlay for a background session:
x/package/tool-schema.ts:123:- interactive_shell({ dismissBackground: "calm-reef" }) - dismiss specific session
x/package/tool-schema.ts:127:Structured \`spawn\` also supports a \`prompt\` field for Pi, Codex, Claude, and Cursor using their native startup prompt forms.
x/package/tool-schema.ts:132:- interactive_shell({ spawn: { agent: "codex" }, mode: "dispatch" })
x/package/tool-schema.ts:133:- interactive_shell({ spawn: { agent: "cursor", prompt: "Review the diffs" }, mode: "dispatch" })
x/package/tool-schema.ts:134:- interactive_shell({ spawn: { agent: "claude", prompt: "Review the diffs" }, mode: "dispatch" })
x/package/tool-schema.ts:135:- interactive_shell({ spawn: { mode: "fork" } }) // pi-only fork of the current persisted session
x/package/tool-schema.ts:143:			description: "The raw CLI command to run (e.g., 'pi \"Fix the bug\"'). Use this for arbitrary CLIs. Mutually exclusive with 'spawn'.",
x/package/tool-schema.ts:146:	spawn: Type.Optional(
x/package/tool-schema.ts:154:				description: "Spawn agent to launch. Defaults to the configured spawn.defaultAgent.",
x/package/tool-schema.ts:160:				description: "Spawn mode. 'fork' is only supported for pi and requires a persisted current session.",
x/package/tool-schema.ts:163:				description: "Launch in a separate git worktree. Defaults to spawn.worktree from config.",
x/package/tool-schema.ts:169:			description: "Structured spawn request for pi, codex, claude, or cursor. Use this instead of building the command string manually when you want the extension's spawn defaults, Pi-only fork behavior, worktree support, or native startup prompts.",
x/package/tool-schema.ts:172:	sessionId: Type.Optional(
x/package/tool-schema.ts:174:			description: "Session ID to interact with an existing hands-free session",
x/package/tool-schema.ts:179:			description: "Kill the session (requires sessionId). Use when task appears complete.",
x/package/tool-schema.ts:210:				Type.Number({ description: "Change max update interval for existing session (ms)" }),
x/package/tool-schema.ts:213:				Type.Number({ description: "Change quiet threshold for existing session (ms)" }),
x/package/tool-schema.ts:218:		Type.String({ description: "Raw text to send to the session (requires sessionId). This only types the text; it does not submit it. Use submit=true or inputKeys:['enter'] when you want to press Enter." }),
x/package/tool-schema.ts:221:		Type.Boolean({ description: "Press Enter after sending any input. Prefer this when submitting slash commands or prompts to editor-based TUIs like pi. (requires sessionId)" }),
x/package/tool-schema.ts:225:			description: "Named keys with modifier support: up, down, enter, ctrl+c, alt+x, shift+tab, ctrl+alt+delete, etc. (requires sessionId)",
x/package/tool-schema.ts:230:			description: "Hex bytes to send as raw escape sequences (e.g., ['0x1b', '0x5b', '0x41'] for ESC[A). (requires sessionId)",
x/package/tool-schema.ts:235:			description: "Text to paste with bracketed paste mode - prevents shells from auto-executing multiline input. (requires sessionId)",
x/package/tool-schema.ts:245:			description: "Optional session name (used for session IDs)",
x/package/tool-schema.ts:302:			persistence: Type.Optional(Type.Object({
x/package/tool-schema.ts:320:			description: "Run without overlay (with mode='dispatch' or mode='monitor') or dismiss existing overlay (with sessionId). Process runs in background, user can /attach.",
x/package/tool-schema.ts:325:			description: "Background session ID to reattach. Opens overlay with the specified mode.",
x/package/tool-schema.ts:330:			description: "List all background sessions.",
x/package/tool-schema.ts:335:			description: "Dismiss background sessions. true = all, string = specific session ID. Kills running sessions, removes exited ones.",
x/package/tool-schema.ts:340:			description: "Query monitor lifecycle/state summary. Requires monitorSessionId or sessionId.",
x/package/tool-schema.ts:345:			description: "Query structured monitor event history instead of session output. Requires monitorSessionId or sessionId.",
x/package/tool-schema.ts:350:			description: "Target monitor session for monitorStatus/monitorEvents queries.",
x/package/tool-schema.ts:387:				Type.Number({ description: "Startup grace period before autoExitOnQuiet can kill the session (default: 15000ms)" }),
x/package/tool-schema.ts:397:					description: "Auto-kill session when output stops (after quietThreshold). Defaults to false. Set to true for fire-and-forget single-task delegations.",
x/package/tool-schema.ts:428:	spawn?: { agent?: "pi" | "codex" | "claude" | "cursor"; mode?: "fresh" | "fork"; worktree?: boolean; prompt?: string };
x/package/tool-schema.ts:429:	sessionId?: string;
x/package/tool-schema.ts:458:		persistence?: { stopAfterFirstEvent?: boolean; maxEvents?: number };
x/package/spawn.ts:64:				return { ok: false, error: `Duplicate spawn agent: ${token.value}` };
x/package/spawn.ts:71:				return { ok: false, error: `Duplicate spawn mode: ${token.value}` };
x/package/spawn.ts:77:			return { ok: false, error: `Unknown /spawn argument: ${token.value}` };
x/package/spawn.ts:80:			return { ok: false, error: `Unknown /spawn argument: ${token.value}` };
x/package/spawn.ts:88:			error: "Prompt text must be quoted as a single argument, for example /spawn claude \"review the diffs\" --dispatch.",
x/package/spawn.ts:96:			error: "Prompt-bearing /spawn requires --hands-free or --dispatch.",
x/package/spawn.ts:102:			error: "Monitored /spawn requires a quoted prompt, for example /spawn claude \"review the diffs\" --dispatch.",
x/package/spawn.ts:121:	| { ok: true; spawn: ResolvedSpawn }
x/package/spawn.ts:123:	const agent = request?.agent ?? config.spawn.defaultAgent;
x/package/spawn.ts:125:	const worktree = request?.worktree ?? config.spawn.worktree;
x/package/spawn.ts:129:		return { ok: false, error: "Spawn prompt cannot be empty." };
x/package/spawn.ts:133:		return { ok: false, error: `Cannot fork ${agent}. Fork is only supported for pi sessions.` };
x/package/spawn.ts:140:			return { ok: false, error: "Cannot fork the current session because it is not persisted (likely --no-session mode)." };
x/package/spawn.ts:155:	const executable = config.spawn.commands[agent];
x/package/spawn.ts:156:	const args = [...config.spawn.defaultArgs[agent]];
x/package/spawn.ts:157:	let reason = `spawn ${agent} (${mode === "fork" ? "fork current session" : "fresh session"})`;
x/package/spawn.ts:171:		spawn: {
x/package/spawn.ts:195:	const baseDir = config.spawn.worktreeBaseDir
x/package/spawn.ts:196:		? resolve(repoRoot.stdout, config.spawn.worktreeBaseDir)
x/package/spawn.ts:306:		return { ok: false, error: "Unterminated quote in /spawn arguments." };
x/package/session-query.ts:2:import type { OutputOptions, OutputResult } from "./session-manager.js";
x/package/session-query.ts:4:import type { PtyTerminalSession } from "./pty-session.js";
x/package/session-query.ts:6:/** Mutable query bookkeeping kept per active session. */
x/package/session-query.ts:25:	session: PtyTerminalSession,
x/package/session-query.ts:42:		return getIncrementalOutput(session, state, requestedLines, requestedMaxChars);
x/package/session-query.ts:46:		return buildTruncatedOutput(session.getRawStream({ sinceLast: true, stripAnsi: true }), requestedMaxChars, true);
x/package/session-query.ts:50:		return getOffsetOutput(session, opts.offset, requestedLines, requestedMaxChars);
x/package/session-query.ts:53:	const tailResult = session.getTailLines({
x/package/session-query.ts:90:	session: PtyTerminalSession,
x/package/session-query.ts:95:	const result = session.getLogSlice({
x/package/session-query.ts:112:	session: PtyTerminalSession,
x/package/session-query.ts:117:	const result = session.getLogSlice({
x/package/session-manager.ts:1:import { PtyTerminalSession } from "./pty-session.js";
x/package/session-manager.ts:8:	session: PtyTerminalSession;
x/package/session-manager.ts:60:// Human-readable session slug generation
x/package/session-manager.ts:136:	private sessions = new Map<string, BackgroundSession>();
x/package/session-manager.ts:157:	registerActive(session: ActiveSession): void {
x/package/session-manager.ts:158:		this.activeSessions.set(session.id, session);
x/package/session-manager.ts:163:		// Only release the ID if explicitly requested (when session fully terminates)
x/package/session-manager.ts:164:		// This prevents ID reuse while session is still running after takeover
x/package/session-manager.ts:175:		const session = this.activeSessions.get(id);

===== openclaude-web SIGNALS =====
x/package/scripts/postinstall.mjs:41:  const { spawnSync } = await import("node:child_process");
x/package/scripts/postinstall.mjs:44:    const r = spawnSync(process.execPath, [patchScript], { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" });
x/package/scripts/patch-openclaude.mjs:14: * display for openclaude TUI sessions only — the official `claude` binary
x/package/scripts/patch-openclaude.mjs:138:  // ── Patch 2 (v3): bump the resume-transcript safety cap ──────────────
x/package/scripts/patch-openclaude.mjs:139:  // Openclaude refuses to resume a session whose reconstructed transcript
x/package/scripts/patch-openclaude.mjs:140:  // exceeds 8 MiB ("Reconstructed transcript is too large to resume safely").
x/package/scripts/patch-openclaude.mjs:141:  // For users who keep long-running development sessions (with many image
x/package/scripts/patch-openclaude.mjs:179:  // ── Patch 9 (v16): inject /web-connect and /web-disconnect slash commands ──
x/package/scripts/patch-openclaude.mjs:180:  // The user wants explicit, per-session opt-in for the bridge: run `openclaude`
x/package/scripts/patch-openclaude.mjs:182:  // session with openclaude-web. `/web-disconnect` reverses it. The commands
x/package/scripts/patch-openclaude.mjs:195:      description: "Bridge this TUI session live to a local openclaude-web server (toggles open the dialog when already active)",
x/package/scripts/patch-openclaude.mjs:216:              onDone("Web disconnected.", display:"system") → the runner
x/package/scripts/patch-openclaude.mjs:218:              disconnected." with the suppress flag silencing the cleanup's
x/package/scripts/patch-openclaude.mjs:240:          const _ocwUrl = "http://" + _ocwHost + (_ocwSid ? ("/?session=" + _ocwSid) : "/");
x/package/scripts/patch-openclaude.mjs:307:  const indicExplicitSig = `  if (!explicit && status2.label !== "Remote Control reconnecting") {
x/package/scripts/patch-openclaude.mjs:322:    [`label: "Remote Control reconnecting"`,  `label: "Web reconnecting"`],
x/package/scripts/patch-openclaude.mjs:382:          _ocwTrace('STORE CHANGE: __ocw=' + s.__ocwBridgeOn + ' enabled=' + s.replBridgeEnabled + ' explicit=' + s.replBridgeExplicit + ' connected=' + s.replBridgeConnected + ' sessionActive=' + s.replBridgeSessionActive);
x/package/scripts/patch-openclaude.mjs:396:      /*OCWPATCH-v40: the disconnect-message push moved into the cleanup
x/package/scripts/patch-openclaude.mjs:424:                _ocwTrace('  store snapshot post-set: enabled=' + _s.replBridgeEnabled + ' connected=' + _s.replBridgeConnected + ' sessionActive=' + _s.replBridgeSessionActive + ' reconnecting=' + _s.replBridgeReconnecting + ' explicit=' + _s.replBridgeExplicit);
x/package/scripts/patch-openclaude.mjs:476:      /*OCWPATCH-v46: cleanup no longer pushes "⎿ Web disconnected." into
x/package/scripts/patch-openclaude.mjs:478:        indicator-Enter "d to disconnect" path is silent, only the slash
x/package/scripts/patch-openclaude.mjs:480:        menu-Disconnect option already calls onDone("Web disconnected.",
x/package/scripts/patch-openclaude.mjs:482:        "> /web-control\n⎿ Web disconnected." pair. The previous cleanup
x/package/scripts/patch-openclaude.mjs:484:        unwanted "⎿ Web disconnected." after indicator-disconnects. Reset
x/package/scripts/patch-openclaude.mjs:512:        const _ocwEnv = { type: "event", data: { type: _ocwT, uuid: _ocwMsg.uuid, session_id: _ocwH.sid, parent_tool_use_id: null, message: _ocwMsg.message } };
x/package/scripts/patch-openclaude.mjs:564:  // for our local web UI. Rebrand to "browser at" so the user knows where to
x/package/scripts/patch-openclaude.mjs:567:  const idleFootReplace = `Open in browser at `;
x/package/scripts/patch-openclaude.mjs:574:  const activeFootReplace = `Continue in browser at `;
x/package/scripts/patch-openclaude.mjs:583:  // renders the head item. To mirror those prompts in the browser (cloud
x/package/scripts/patch-openclaude.mjs:588:  // and pop the matching item by toolUseID after the browser answers.
x/package/scripts/patch-openclaude.mjs:614:  // Bash prompts queue silently and the browser never sees them. Also
x/package/scripts/patch-openclaude.mjs:724:            // OCWPATCH-v29: browser-answered permission prompts arrive as
x/package/scripts/patch-openclaude.mjs:767:  // there and ship a `_bridge_mode_changed` envelope so the browser can keep
x/package/scripts/patch-openclaude.mjs:770:  const subAnchor = `_ocwTrace('STORE CHANGE: __ocw=' + s.__ocwBridgeOn + ' enabled=' + s.replBridgeEnabled + ' explicit=' + s.replBridgeExplicit + ' connected=' + s.replBridgeConnected + ' sessionActive=' + s.replBridgeSessionActive);`;
x/package/scripts/patch-openclaude.mjs:771:  const subReplace = `_ocwTrace('STORE CHANGE: __ocw=' + s.__ocwBridgeOn + ' enabled=' + s.replBridgeEnabled + ' explicit=' + s.replBridgeExplicit + ' connected=' + s.replBridgeConnected + ' sessionActive=' + s.replBridgeSessionActive);
x/package/scripts/patch-openclaude.mjs:796:  // out the wire right after the bridge handshake so the browser knows the
x/package/scripts/patch-openclaude.mjs:802:            /*OCWPATCH-v30+v32: initial mode sync to browser (with trace)*/
x/package/scripts/patch-openclaude.mjs:829:              const _activeMsgUrl = "http://" + _activeMsgHost + (_activeMsgSid ? ("/?session=" + _activeMsgSid) : "/");
x/package/scripts/patch-openclaude.mjs:912:  // (c) replace t19 (the static "d to disconnect..." hint) with a Fragment
x/package/scripts/patch-openclaude.mjs:922:      children: "d to disconnect · space for QR code · Enter/Esc to close"
x/package/scripts/patch-openclaude.mjs:932:      slash-command-runner's onDone. Disconnect calls onDone("Web disconnected.",
x/package/scripts/patch-openclaude.mjs:934:      "> /web-control\\n⎿ Web disconnected." pair. Cleanup's push is
x/package/scripts/patch-openclaude.mjs:939:      "d to disconnect · space for QR code · Enter/Esc to close". */
x/package/scripts/patch-openclaude.mjs:946:            { label: "Disconnect this session", value: "ocw-disconnect" },
x/package/scripts/patch-openclaude.mjs:952:            if (v === "ocw-disconnect") {
x/package/scripts/patch-openclaude.mjs:955:              try { onDone("Web disconnected.", { display: "system" }); } catch {}
x/package/scripts/patch-openclaude.mjs:973:      children: "d to disconnect \\u00b7 space for QR code \\u00b7 Enter/Esc to close"
x/package/scripts/patch-openclaude.mjs:990:  // the original single-line "Continue in browser at <url>" / "Open in
x/package/scripts/patch-openclaude.mjs:991:  // browser at <url>" wording set by Patch 13 (v28). No additional change
x/package/scripts/patch-openclaude.mjs:1071:  // ── Patch 22 (v35): dialog disconnect also clears __ocwBridgeOn ───────
x/package/scripts/patch-openclaude.mjs:1072:  // The BridgeDialog's "d to disconnect" handler calls setAppState(_temp1210)
x/package/scripts/patch-openclaude.mjs:1087:  /*OCWPATCH-v35: also clear our __ocwBridgeOn + URL state on dialog disconnect*/
x/package/scripts/patch-openclaude.mjs:1109:    return { ok: false, msg: "couldn't find _temp1210 (BridgeDialog disconnect reducer) — openclaude version probably changed" };
x/package/scripts/patch-openclaude.mjs:1115:  // fire the same handler when the browser's stop button is pressed during
x/package/scripts/patch-openclaude.mjs:1116:  // a bridged session — without that, the stop button just clears the local
x/package/scripts/patch-openclaude.mjs:1144:  // wrap, so when the user accepts/rejects in the TUI the browser never
x/package/bin/openclaude-web.mjs:42:  console.log(`openclaude-web — local browser UI for openclaude / claude
x/package/bin/openclaude-web.mjs:53:Live sync: with openclaude-web running, plain \`openclaude\` / \`openclaude --resume\`
x/package/bin/openclaude-web.mjs:54:will auto-bridge to this server, so any browser tab on the same session shows
x/package/bin/openclaude-web.mjs:63:Then open http://localhost:<port> in your browser.
x/package/bin/openclaude-web.mjs:81:  const { spawnSync } = await import("node:child_process");
x/package/bin/openclaude-web.mjs:82:  const check = spawnSync(process.execPath, [patchScript, "--check"], { stdio: "ignore" });
x/package/bin/openclaude-web.mjs:84:    const r = spawnSync(process.execPath, [patchScript], { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" });
x/package/server/sessions.mjs:78:        // Skip empty/system-only sessions — they're filesystem droppings from
x/package/server/sessions.mjs:79:        // child spawns that never received a real user message. Listing them
x/package/server/sessions.mjs:94:  // Dedup by session id — same UUID can appear in both ~/.openclaude/projects/
x/package/server/sessions.mjs:123:// Per-session sidecars that survive a plain .jsonl unlink — these are why
x/package/server/sessions.mjs:124:// `--resume <deleted-id>` could ghost a session back. Wipe them all.
x/package/server/sessions.mjs:129://   <config>/session-env/<id>/                    — session env dir
x/package/server/sessions.mjs:169:      const needle = `"sessionId":"${id}"`;
x/package/server/sessions.mjs:183:    // 4. session-env/<id>/
x/package/server/sessions.mjs:184:    const sessEnv = join(configDir, "session-env", id);
x/package/server/sessions.mjs:194:  // 5. openclaude-web's own path-mode upload dir for this session.
x/package/server/runner.mjs:1:import { spawn } from "node:child_process";
x/package/server/runner.mjs:22: * @param {"openclaude"|"claude"} [opts.runtime]  Which CLI to spawn (default: openclaude).
x/package/server/runner.mjs:23: * @param {string} [opts.sessionId]   Resume an existing session (preferred).
x/package/server/runner.mjs:24: * @param {string} [opts.newSessionId] Use a specific UUID for a new session.
x/package/server/runner.mjs:28:export function spawnOpenclaude(opts = {}) {
x/package/server/runner.mjs:47:  if (opts.sessionId) args.push("--resume", opts.sessionId);
x/package/server/runner.mjs:48:  else if (opts.newSessionId) args.push("--session-id", opts.newSessionId);
x/package/server/runner.mjs:60:  // syncs live with browser tabs). But subprocesses we spawn ourselves are
x/package/server/runner.mjs:63:  // any process we spawn.
x/package/server/runner.mjs:67:  console.log("[runner] spawn:", runtime.bin, args.join(" "), "cwd=" + cwd);
x/package/server/runner.mjs:68:  const child = spawn(runtime.bin, args, {
x/package/server/index.mjs:2:import websocket from "@fastify/websocket";
x/package/server/index.mjs:5:import { listSessions, findSessionFile, readSessionEvents, cleanupSessionSidecars } from "./sessions.mjs";
x/package/server/index.mjs:23:await fastify.register(websocket);
x/package/server/index.mjs:61:// List all sessions. Adds an `alive` flag per session telling the sidebar
x/package/server/index.mjs:62:// which ones currently have a live `claude --resume <id>` process in memory
x/package/server/index.mjs:64:fastify.get("/api/sessions", async () => {
x/package/server/index.mjs:65:  const sessions = await listSessions();
x/package/server/index.mjs:67:  return { sessions: sessions.map(s => ({ ...s, alive: live.has(s.id) })) };
x/package/server/index.mjs:92:// write to UPLOADS_DIR/<sessionId>/<timestamp>_<sanitized>.
x/package/server/index.mjs:100:  const sessionId = String(body.sessionId || "").trim();
x/package/server/index.mjs:104:  if (!sessionId) return reply.code(400).send({ error: "sessionId required" });
x/package/server/index.mjs:110:  if (buf.length === 0) return reply.code(400).send({ error: "empty file" });
x/package/server/index.mjs:115:  const dir = join(UPLOADS_DIR, sessionId);
x/package/server/index.mjs:142:    fastify.log.info({ sessionId, filename, size: buf.length, mime, pdfWrapped }, "upload saved");
x/package/server/index.mjs:150:// Close (SIGTERM) a single agent without touching the session JSONL. Click
x/package/server/index.mjs:151:// the same session later → fresh `claude --resume <id>` spawns and picks
x/package/server/index.mjs:152:// the conversation back up. Different from DELETE /api/sessions which
x/package/server/index.mjs:153:// also removes the on-disk session.
x/package/server/index.mjs:156:  if (!ok) return reply.code(404).send({ error: "no live agent for that session" });
x/package/server/index.mjs:160:// Read a session's full event history (for replay when user clicks it)
x/package/server/index.mjs:161:fastify.get("/api/sessions/:id/events", async (req, reply) => {
x/package/server/index.mjs:163:  if (!meta) return reply.code(404).send({ error: "session not found" });
x/package/server/index.mjs:168:// Delete a session — removes the underlying JSONL AND every per-session
x/package/server/index.mjs:169:// sidecar (tool-results dir, history.jsonl entry, debug log, session-env dir)
x/package/server/index.mjs:170:// so `--resume <deleted-id>` can't resurrect a ghost.
x/package/server/index.mjs:171:fastify.delete("/api/sessions/:id", async (req, reply) => {
x/package/server/index.mjs:173:  if (!meta) return reply.code(404).send({ error: "session not found" });
x/package/server/index.mjs:186:// Live chat WebSocket. Client connects with ?sessionId=<existing> to resume,
x/package/server/index.mjs:187:// or no param to start a new session.
x/package/server/index.mjs:189:  f.get("/ws/chat", { websocket: true }, async (socket, req) => {
x/package/server/index.mjs:191:    const sessionId = url.searchParams.get("sessionId") || null;
x/package/server/index.mjs:196:    // the TUI. Strict contract — the browser tab is a passive mirror of an
x/package/server/index.mjs:197:    // active TUI publisher. If no TUI is bridged for this session (or the TUI
x/package/server/index.mjs:198:    // disconnects later), reject and close instead of spawning a sidecar agent.
x/package/server/index.mjs:200:    const newSessionId = sessionId ? null : randomUUID();
x/package/server/index.mjs:208:    send({ type: "_meta", sessionId: sessionId || newSessionId, cwd, model, runtime, bridged: !!(sessionId && isBridged(sessionId)), mirror: mirrorOnly });

[Showing lines 109-611 of 611 (50.0KB limit). Full output: /tmp/pi-bash-13b74522d6f7fbf0.log]


**Researching Pi browser options**

I'm aiming for a concise recommendation for Path B options that focus on a browser client for Raspberry Pi. I should include tau-mirror, pi-tau-mux-server, and pi-remote. It's important to note that these options can mirror a running Pi process, so disconnecting doesn't kill the process if it's in tmux or running detached. For pi-remote, it likely allows a session to continue independently of the browser. I might also need an aggregator for sessions with tau mux and client extensions. Let's check the readme for pi-tau-mux or its existence.

bash

npm view pi-tau-mux name version description repository.url homepage keywords readme 2>/dev/null | sed -n '1,220p'

npm search --long --searchlimit=20 'pi-tau-mux' 2>/dev/null | sed -n '1,80p'


name = 'pi-tau-mux'
version = '1.0.9'
description = 'Web UI that mirrors your Pi terminal session in the browser - tmux session aware'
repository.url = 'git+https://github.com/dwainm/pi-tau-mux.git'
homepage = 'https://github.com/dwainm/pi-tau-mux#readme'
keywords = [ 'pi', 'pi-package', 'coding-agent', 'web-ui', 'mirror', 'tmux' ]
readme = '# pi-tau-mux\n' +
  '\n' +
  '**A lightweight client extension that connects Pi to [pi-tau-mux-server](https://github.com/dwainm/pi-tau-mux-server).**\n' +
  '\n' +
  'This extension turns your Pi session into a client that registers with the standalone Tau mux server, enabling real-time mirroring in the browser across multiple Pi instances.\n' +
  '\n' +
  '## Architecture\n' +
  '\n' +
  '```\n' +
  '┌─────────────┐                    ┌──────────────────────┐                    ┌─────────────┐\n' +
  '│  Pi TUI     │                    │  pi-tau-mux-server   │                    │  Browser    │\n' +
  '│  (terminal) │    WebSocket /pi    │  (standalone daemon) │    WebSocket /ws   │  (Tau UI)  │\n' +
  '│             │◄───────────────────►│                      │◄──────────────────►│             │\n' +
  '└─────────────┘                    │  Aggregates all      │                    └─────────────┘\n' +
  '                                   │  Pi instances        │\n' +
  '┌─────────────┐                    │                      │                    ┌─────────────┐\n' +
  '│  Pi TUI     │                    │  Serves web UI       │                    │  Phone      │\n' +
  '│  (another)  │◄───────────────────►│  Scans sessions      │◄──────────────────►│  (QR scan)  │\n' +
  '└─────────────┘                    └──────────────────────┘                    └─────────────┘\n' +
  '```\n' +
  '\n' +
  '**This extension** = lightweight client (registers, forwards events, unregisters)  \n' +
  '**[pi-tau-mux-server](https://github.com/dwainm/pi-tau-mux-server)** = standalone server (web UI, session browser, Tailscale support)\n' +
  '\n' +
  '## Install\n' +
  '\n' +
  '```bash\n' +
  'pi install git:github.com/dwainm/pi-tau-mux\n' +
  '```\n' +
  '\n' +
  '## Usage\n' +
  '\n' +
  '1. **Start the mux server** (one-time or auto-start):\n' +
  '   ```bash\n' +
  '   pi-tau-mux-server\n' +
  '   ```\n' +
  '   Or let the extension prompt you when Pi starts.\n' +
  '\n' +
  '2. **Start Pi normally** — the extension auto-connects to the mux server\n' +
  '\n' +
  '3. **Open the web UI** at the URL shown (e.g., `http://localhost:3001` or your Tailscale URL)\n' +
  '\n' +
  '## Commands\n' +
  '\n' +
  '| Command | Description |\n' +
  '|---------|-------------|\n' +
  '| `/tauconnect` | Connect to the mux server |\n' +
  '| `/taudisconnect` | Disconnect from the mux server |\n' +
  '\n' +
  '## Environment Variables\n' +
  '\n' +
  '| Variable | Default | Description |\n' +
  '|----------|---------|-------------|\n' +
  '| `TAU_HOST` | `localhost` | Mux server host |\n' +
  '| `TAU_PORT` | `3001` | Mux server port |\n' +
  '| `TAU_AUTO_CONNECT` | `1` | Set to `0` to disable auto-connect |\n' +
  '\n' +
  '## Features\n' +
  '\n' +
  '### Session Mirroring\n' +
  '- Real-time streaming of messages, tool calls, and thinking blocks\n' +
  '- Multiple Pi instances can connect to the same mux server\n' +
  '- Browser shows all active sessions across projects\n' +
  '\n' +
  '### Tailscale Support\n' +
  'The mux server auto-detects Tailscale and uses your Tailscale IP or MagicDNS hostname. Scan the QR code from any device on your tailnet.\n' +
  '\n' +
  '### Session Browser\n' +
  'View history from any past Pi session, grouped by project. Active sessions show a "LIVE" indicator.\n' +
  '\n' +
  '## Related\n' +
  '\n' +
  '- **[pi-tau-mux-server](https://github.com/dwainm/pi-tau-mux-server)** — The standalone server (install globally)\n' +
  '- **[pi-coding-agent](https://github.com/mariozechner/pi-coding-agent)** — The Pi coding agent\n' +
  '- **[Tau (original)](https://github.com/deflating/tau)** — This is a fork with tmux awareness and mux architecture\n' +
  '\n' +
  '## Why the Split?\n' +
  '\n' +
  'The original Tau ran an HTTP server inside each Pi process. This worked for single instances but caused issues with:\n' +
  '- Multiple Pi instances (port conflicts)\n' +
  '- Remote access (needed each port forwarded)\n' +
  '- Resource usage (server per Pi process)\n' +
  '\n' +
  'The mux architecture solves these:\n' +
  '- **One server** for all Pi instances\n' +
  '- **One port** to forward or expose via Tailscale\n' +
  '- **Session aggregation** across all running Pi instances\n' +
  '\n' +
  '## License\n' +
  '\n' +
  'MIT'


pi-tau-mux
Web UI that mirrors your Pi terminal session in the browser - tmux session aware
Version 1.0.9 published 2026-03-26 by dwainm
Maintainers: dwainm
Keywords: pi pi-package coding-agent web-ui mirror tmux
https://npm.im/pi-tau-mux

pi-tau-mux-server
Standalone Tau server - aggregates multiple pi coding agent instances into one web UI with Tailscale support
Version 1.0.8 published 2026-03-26 by dwainm
Maintainers: dwainm
Keywords: pi pi-coding-agent tau mux aggregator web-ui tailscale
https://npm.im/pi-tau-mux-server

@mux/mux-video
A custom mux video element for the browser that Just Works™
Version 0.31.0 published 2026-04-28 by mux-npmjs
Maintainers: jsanford8 dylanjha phil-mux mux-npmjs
Keywords: video mux player hls web-component
https://npm.im/@mux/mux-video

@mux/mux-player
An open source Mux player web component that Just Works™
Version 3.13.0 published 2026-04-28 by mux-npmjs
Maintainers: jsanford8 dylanjha phil-mux mux-npmjs
Keywords: video mux player hls web-component
https://npm.im/@mux/mux-player

@mux/playback-core
Core library for media playback in the browser shared by mux elements
Version 0.35.0 published 2026-04-28 by mux-npmjs
Maintainers: jsanford8 dylanjha phil-mux mux-npmjs
https://npm.im/@mux/playback-core

@mux/mux-player-react
An open source Mux player for React that Just Works™
Version 3.13.0 published 2026-04-28 by mux-npmjs
Maintainers: jsanford8 dylanjha phil-mux mux-npmjs
Keywords: video mux player hls react
https://npm.im/@mux/mux-player-react

mux-embed
Mux Javascript SDK and HTML5 Video element monitor
Version 5.18.1 published 2026-04-27 by GitHub Actions
Maintainers: jsanford8 dylanjha phil-mux mux-npmjs
https://npm.im/mux-embed

@mux/mux-data-google-ima
Mux Data SDK extension for monitoring Google IMA Ads with mux-embed
Version 0.3.17 published 2026-04-27 by GitHub Actions
Maintainers: jsanford8 dylanjha phil-mux mux-npmjs
Keywords: google-ima csai video-analytics mux
https://npm.im/@mux/mux-data-google-ima

tau-prolog
An open source Prolog interpreter in JavaScript
Version 0.3.4 published 2022-08-03 by jariazavalverde
Maintainers: jariazavalverde
Keywords: tau-prolog prolog-interpreter iso-prolog-standard javascript prolog
https://npm.im/tau-prolog

@mux/mux-node
The official TypeScript library for the Mux API
Version 14.0.1 published 2026-04-20 by GitHub Actions
Maintainers: jsanford8 dylanjha phil-mux mux-npmjs
https://npm.im/@mux/mux-node

@mux/upchunk
Dead simple chunked file uploads using Fetch
Version 3.5.0 published 2025-02-19 by mux-npmjs
Maintainers: jsanford8 dylanjha phil-mux mux-npmjs
Keywords: browser upload chunked upload file upload gcs upload google cloud storage upload large file upload resumable upload
https://npm.im/@mux/upchunk

@mux/mux-uploader
An uploader elements to be used with Mux Direct Uploads
Version 1.5.0 published 2026-04-24 by mux-npmjs
Maintainers: jsanford8 dylanjha phil-mux mux-npmjs
Keywords: video mux uploader gcs chunked-upload web-component upchunk
https://npm.im/@mux/mux-uploader


**Considering project setup**

I need to finalize options and make a recommendation. It might help to mention that there’s no parent orchestrator involved. I could host a tau mux server and run one Raspberry Pi per project in tmux for better durability. Additionally, using a browser UI for control makes sense. If someone wants a pure browser terminal with sessions that survive browser disconnections, pi-remote is an option. I should clearly distinguish between mirror and remote setups.


Updated #3 (pending → completed)

Find Pi extension search capability

Check whether local tools can search Pi extensions/packages, and if not, manually locate such a tool/command in installed Pi code/docs, then use it to search for relevant extensions.

Compare Pi orchestration extensions for tmux-resilient workflow

Inspect top candidate Pi packages for multi-agent orchestration with tmux/server persistence, then rank best fit for a single orchestrator whose children keep running after SSH disconnect.

Explore web client path for server-hosted Pi sessions

Find candidate web UIs/clients that can host persistent Pi sessions on the server, evaluate survival across browser disconnects, and recommend best path B options.


Path B looks viable.

## Best candidates

### 1. `pi-tau-mux` + `pi-tau-mux-server`
Best fit for **one browser UI, many project sessions**.

What it is:
- `pi-tau-mux-server` = standalone web server
- `pi-tau-mux` = Pi extension each Pi session connects to

Why it fits:
- one web UI for **all active Pi sessions**
- session browser across projects
- tmux-aware
- Tailscale-friendly
- browser disconnect does **not** own the Pi process
- good match for:
  - one Pi session per project
  - hosted on server
  - reopen from laptop/phone later

Important nuance:
- this is mainly a **mirror/control UI for existing Pi sessions**
- you still need to run each Pi session somewhere durable
- best durable setup: **one tmux window per project session**
- then mux server gives you the nicer browser front-end

So this path becomes:

- server runs `pi-tau-mux-server`
- each project has a Pi session running in tmux
- each Pi session loads `pi-tau-mux`
- browser becomes control plane

This is probably the strongest Path B shape.

---

### 2. `tau-mirror`
Good for **single Pi instance**, weaker for your case.

What it is:
- web UI inside the Pi process itself
- mirrors one running Pi session in browser

Pros:
- simple
- session browser
- same browser UX family as Tau
- no extra daemon

Cons:
- one server per Pi process
- less good for many concurrent project sessions
- weaker than mux for “many projects in one place”

Use only if:
- you want one project at a time
- or want lowest setup cost

---

### 3. `@noahsaso/pi-remote`
Good if you want **browser terminal access** more than a reconstructed app UI.

What it is:
- wraps Pi in a PTY
- exposes browser terminal via WebSocket
- auto-reconnect + replay buffer
- Tailscale support
- discovery page for active remote sessions

Pros:
- strongest “remote terminal” model
- explicit reconnect behavior
- session survives browser disconnect better than plain SSH
- probably best if you want something closer to “real terminal on web”

Cons:
- UX is still basically terminal UX in browser
- solves SSH pain more than Pi TUI design pain
- not as polished for multi-session project management as Tau mux appears

Use only if:
- your main problem is SSH transport
- not Pi interaction model itself

---

## What I think is best

### Best Path B recommendation:
**`pi-tau-mux-server` + `pi-tau-mux`**

Because your current idea is:

- no parent orchestrator for now
- one session per project
- hosted on server
- better human UX

That is exactly what mux is for.

## Recommended architecture

### Server side
- run `pi-tau-mux-server` as persistent service
- run **one tmux session/window per project**
- each project Pi runs with `pi-tau-mux` installed/enabled

### Client side
- open single browser UI
- switch between project sessions there
- if browser dies, tmux-backed Pi sessions keep running
- if SSH dies, tmux-backed Pi sessions keep running
- if browser reconnects, UI should reconnect to mux server

## Why this beats `pi-remote` for you
Because your pain is not just transport.
It is also:
- multi-project visibility
- less miserable human control
- avoid one parent orchestrator for now

Mux gives that better than raw browser terminal.

## Why this still needs tmux
Important:
- Path B does **not** remove need for durable server-side process ownership
- it just removes “human must live in tmux”
- tmux still likely remains the right process host underneath

So:
- **humans use browser**
- **Pi processes live in tmux**

That’s probably the right split.

---

## Decision table

### Pick `pi-tau-mux` stack if you want:
- one browser UI
- multiple project sessions
- Tailscale access
- session browser/history
- less terminal-like supervision

### Pick `pi-remote` if you want:
- true browser terminal
- reconnect/replay
- mobile-friendly direct remote access
- less concern about multi-project overview

### Pick `tau-mirror` if you want:
- simplest single-session experiment
- fastest proof of concept
- no standalone server

---

## My ranking for Path B

1. **pi-tau-mux-server + pi-tau-mux**
2. **@noahsaso/pi-remote**
3. **tau-mirror**

---

## Practical next step
I’d test this first:

1. install `pi-tau-mux-server`
2. install `pi-tau-mux`
3. run 2 project Pi sessions in tmux
4. open web UI from another device
5. verify:
   - live session switching
   - browser reconnect
   - session history
   - behavior when SSH client dies
   - behavior when one Pi process exits

If that works, Path B is real.

If you want, I can do the next layer now:
- inspect `pi-tau-mux` deeper for rough edges
- or install/test it locally on this server and report exact setup steps/results.


can you try saying the same but shorter?


Best Path B: **`pi-tau-mux-server` + `pi-tau-mux`**.

Why:
- one **web UI** for many Pi sessions
- good for **one session per project**
- sessions can keep running on server
- browser is just control/view layer
- better fit than raw browser terminal

How to use it:
- run each project’s Pi in **tmux**
- enable `pi-tau-mux` in those Pi sessions
- run one `pi-tau-mux-server`
- access all sessions from browser

Why tmux still matters:
- browser disconnect should not own the process
- tmux keeps Pi alive
- web UI replaces human tmux usage, not server-side durability

Other options:
- `pi-remote`: browser terminal, more transport fix than UX fix
- `tau-mirror`: simpler, but weaker for many sessions

Reco:
- test `pi-tau-mux-server` first
- if good, use **browser for humans + tmux for persistence**

If you want, I can install/test this stack here next.


we tried tau but cant remember results. anything in memory?