# Chat with Pi - Flow Documentation

This document describes the full-stack architecture and data flow of the Chat app in Officer.dev, which uses **Pi** (an AI coding agent) as the backend.

---

## Architecture Overview

```
┌─────────────────────────────────────────────────────────────────┐
│                         FRONTEND                                │
│  ┌──────────────┐    ┌────────────────┐    ┌───────────────┐   │
│  │ InputArea    │───▶│ usePiChat      │───▶│ useChatWebSocket│ │
│  │ (user input) │    │ (hook)         │    │ (WS connection)│  │
│  └──────────────┘    └───────┬────────┘    └───────┬────────┘   │
│                              │                    │             │
│                              ▼                    ▼             │
│                     ┌────────────────┐    ┌───────────────┐     │
│                     │ MessageList    │◀───│ handleMessage │     │
│                     │ (UI updates)   │    │ (event parser)│     │
│                     └────────────────┘    └───────────────┘     │
└─────────────────────────────────────────────────────────────────┘
                                    │ WebSocket
                                    ▼
┌─────────────────────────────────────────────────────────────────┐
│                         BACKEND                                  │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                    server.tsx                             │   │
│  │  '/api/pi/chat/ws' → upgradeWs() → piWebsocket handler   │   │
│  └──────────────────────────────────────────────────────────┘   │
│                              │                                   │
│                              ▼                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │               websocket.ts (piWebsocket)                │   │
│  │                                                           │   │
│  │  message() ──┬──▶ handleChat() ──▶ SessionManager      │   │
│  │              │                    │                     │   │
│  │              │                    ▼                     │   │
│  │              │              pi-bridge.ts               │   │
│  │              │         (spawn Pi subprocess)            │   │
│  │              │                    │                     │   │
│  │              │                    ▼                     │   │
│  │              │         Bun.spawn(['pi', '--mode',      │   │
│  │              │               'rpc', ...])               │   │
│  │              │                    │                     │   │
│  │              │                    ▼                     │   │
│  │              │         Parse JSON from stdout          │   │
│  │              │                    │                     │   │
│  │              └────────────────────┘                     │   │
│  │                         │                               │   │
│  │                         ▼                               │   │
│  │  sendToClient() ──▶ WebSocket JSON                     │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                  storage.ts                               │   │
│  │  Save to: ~/.pi-sessions/<sessionId>/                    │   │
│  │  - meta.json (session metadata)                         │   │
│  │  - messages.json (message history)                       │   │
│  └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘
                                    ▲
                                    │
                            Pi subprocess
                            (AI Agent)
```

---

## Directory Structure

### Frontend

```
src/workspaces/officerdev/src/
├── apps/
│   ├── Chat/
│   │   ├── index.ts                  # Main entry, exports
│   │   ├── types.ts                  # TypeScript types
│   │   ├── ChatPanelWrapper.tsx       # Main wrapper component
│   │   ├── ChatList.tsx               # Session list sidebar
│   │   ├── ChatLauncher.tsx           # Launch chat
│   │   ├── EmbeddableChat.tsx         # Embeddable version
│   │   ├── components/
│   │   │   ├── InputArea.tsx          # User input
│   │   │   ├── MessageList.tsx        # Message display
│   │   │   ├── MessageBubble.tsx      # Individual message
│   │   │   ├── ToolActivity.tsx      # Tool execution UI
│   │   │   ├── QuestionActivity.tsx  # Question prompt UI
│   │   │   ├── ModelSelector.tsx     # Model dropdown
│   │   │   ├── AttachButton.tsx      # File attachment
│   │   │   ├── AttachmentList.tsx    # Attached files
│   │   │   └── WebpageDialog.tsx     # URL preview
│   │   ├── hooks/
│   │   │   └── useChat.ts            # Main chat hook
│   │   ├── useAttachments.ts         # File attachment handling
│   │   ├── useAudioRecording.ts     # Voice input
│   │   └── useSlashCommands.ts       # /commands handling
│   │
│   └── ChatHistory/                   # Session management UI
│
├── hooks/
│   ├── usePiChat.ts                   # Main chat hook
│   └── ...
│
└── state/
    └── useChatSessions.ts             # Session state management
```

### Backend

```
src/
├── server.tsx                         # Main server entry
│                                      # - WebSocket upgrade
│                                      # - Route registration
│                                      # - Pi installation check
│
└── servers/
    ├── hono.ts                        # Hono REST server
    │
    └── api/
        ├── pi/
        │   ├── types.ts               # TypeScript types
        │   ├── websocket.ts          # WebSocket handler
        │   ├── session-manager.ts    # Session lifecycle
        │   ├── pi-bridge.ts          # Pi subprocess spawner
        │   ├── storage.ts             # Disk persistence
        │   ├── rest.ts               # REST API endpoints
        │   ├── logger.ts             # Logging
        │   └── README.md             # Original Pi API docs
        │
        └── ... (other API routes)
```

---

## Type Definitions

### Client Messages (Frontend → Backend)

```typescript
// Start new chat
type ClientMessage =
  | { type: 'chat'; prompt: string; sessionId?: string; model?: string; cwd?: string; groupSlug?: string; attachmentIds?: string[] }
  | { type: 'resume'; sessionId: string }  // Resume existing session
  | { type: 'stop' };                      // Stop generation
```

### Server Messages (Backend → Frontend)

```typescript
type ServerMessage =
  | { type: 'session:init'; sessionId: string; model: string; cwd: string }
  | { type: 'assistant:delta'; text: string }        // Streaming token
  | { type: 'assistant:text'; text: string }        // Complete message
  | { type: 'tool:start'; toolCallId: string; toolName: string; toolInput: Record<string, unknown> }
  | { type: 'tool:result'; toolCallId: string; output: string; isError: boolean }
  | { type: 'result'; sessionId: string; cost: MessageCost }
  | { type: 'sync:messages'; sessionId: string; messages: Message[]; isGenerating: boolean; streamingText: string }
  | { type: 'error'; message: string; errorCode?: string }
  | { type: 'stopped' };
```

### Internal Types

```typescript
type Message = {
  id: string;
  timestamp: number;
  role: 'user' | 'assistant' | 'tool';
  text?: string;
  model?: string;
  cost?: MessageCost;
  toolCallId?: string;
  toolName?: string;
  toolInput?: Record<string, unknown>;
  output?: string;
  isError?: boolean;
};

type SessionMeta = {
  id: string;
  title: string;
  model: string;
  cwd: string;
  createdAt: number;
  updatedAt: number;
  messageCount: number;
  cost: MessageCost;
  groupSlug?: string | null;
};

type MessageCost = {
  inputTokens: number;
  outputTokens: number;
  totalUSD: number;
};
```

---

## REST API Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/pi/models` | List available AI models |
| POST | `/api/pi/sessions` | List all sessions |
| GET | `/api/pi/sessions/:id` | Get session with messages |
| PATCH | `/api/pi/sessions/:id` | Update session (rename) |
| DELETE | `/api/pi/sessions/:id` | Delete session |
| GET | `/api/pi/sessions/search?q=` | Search sessions |
| POST | `/api/pi/groups` | Create group |
| GET | `/api/pi/groups` | List groups |
| PATCH | `/api/pi/groups/:slug` | Update group |
| DELETE | `/api/pi/groups/:slug` | Delete group |
| POST | `/api/pi/sessions/:id/move` | Move session between groups |

---

## Data Flow Details

### 1. Starting a New Chat

```
User enters message
       ↓
usePiChat.sendPrompt()
       ↓
useChatWebSocket.send({ type: 'chat', prompt, ... })
       ↓
WebSocket connection to /api/pi/chat/ws
       ↓
server.tsx: upgradeWs() - JWT verification
       ↓
websocket.ts: handleChat()
       ↓
SessionManager.getOrCreate(sessionId, email, cwd, model)
       ↓
pi-bridge.ts: spawnPi(cwd, model, onEvent)
       ↓
Bun.spawn(['pi', '--mode', 'rpc', '--no-extensions', ...])
       ↓
send prompt to Pi via stdin
       ↓
Pi process streams JSON events to stdout
       ↓
pi-bridge parses events → onEvent(PiEvent)
       ↓
websocket.ts createEventHandler() → sendToClient()
       ↓
useChatWebSocket.onMessage() → usePiChat.handleMessage()
       ↓
React state update → UI renders
```

### 2. Resuming a Session

```
User navigates to /chat/:sessionId
       ↓
usePiChat mounts (with initialSessionId)
       ↓
useEffect triggers: getSession(initialSessionId)
       ↓
REST API: GET /api/pi/sessions/:sessionId
       ↓
storage.ts: loadSession() reads from disk
       ↓
Messages loaded into state
       ↓
WebSocket connects (for new messages)
       ↓
websocket.ts: handleResume()
       ↓
Spawns fresh Pi process
       ↓
Sends 'sync:messages' to client
```

### 3. Pi Event Parsing

```
Pi stdout JSON line:
{
  "type": "message_update",
  "assistantMessageEvent": {
    "type": "text_delta",
    "delta": "Hello"
  }
}
       ↓
pi-bridge.ts: parsePiEvent()
       ↓
Converted to PiEvent:
{ type: 'delta', text: 'Hello' }
       ↓
Event handler:
- 'delta' → append to streamBuffer, send 'assistant:delta'
- 'text' → commit streamBuffer, send 'assistant:text'
- 'tool:start' → send 'tool:start', add tool message
- 'tool:result' → send 'tool:result', update tool message
- 'result' → save session, send 'result'
```

---

## Session Storage

### Directory Structure

```
~/.pi-sessions/
├── <sessionId>/
│   ├── meta.json       # Session metadata
│   └── messages.json   # Message history
│
└── @<groupSlug>/       # Grouped sessions
    ├── .group-meta.json
    ├── <sessionId1>/
    │   ├── meta.json
    │   └── messages.json
    └── <sessionId2>/
```

### meta.json

```json
{
  "id": "session-uuid",
  "title": "First prompt...",
  "model": "opencode/big-pickle",
  "cwd": "/home/user",
  "createdAt": 1700000000000,
  "updatedAt": 1700000000000,
  "messageCount": 10,
  "cost": {
    "inputTokens": 5000,
    "outputTokens": 2000,
    "totalUSD": 0.05
  },
  "groupSlug": null
}
```

### messages.json

```json
{
  "messages": [
    { "id": "...", "timestamp": 1700000000000, "role": "user", "text": "Hello" },
    { "id": "...", "timestamp": 1700000000001, "role": "assistant", "text": "Hi!" },
    { "id": "...", "timestamp": 1700000000002, "role": "tool", "toolName": "Bash", "toolInput": {...}, "output": "..." }
  ]
}
```

---

## Authentication

### WebSocket Authentication

1. Client connects: `ws://host/api/pi/chat/ws?token=<JWT>`
2. `server.tsx:upgradeWs()` extracts token from URL
3. `jwt.ts:verify(token)` validates JWT
4. Check token blacklist (logout/revocation)
5. Extract user info: `{ userId, email, role }`
6. Attach to WebSocket data: `ws.data = { userId, email, provider: 'pi', ... }`

### REST Authentication

- Bearer token in `Authorization` header
- `hono.ts` uses `userMiddleware` to verify
- User object attached to request context: `ctx.get('user')`

---

## Session Management

### SessionManager (in-memory)

```typescript
class SessionManager {
  private sessions = new Map<string, UserSession>();
  private userSessions = new Map<string, string[]>();  // email → sessionIds

  getOrCreate(sessionId, email, cwd, model, groupSlug): UserSession
  getSession(sessionId): UserSession | null
  attachWs(sessionId, ws): void
  detachWs(sessionId): void
  setIdleTimeout(sessionId, timeoutMs): void   // Auto-cleanup after 1 hour
  deleteSession(sessionId): void
}
```

### UserSession Object

```typescript
type UserSession = {
  sessionId: string;
  email: string;
  cwd: string;
  model: string;
  piProcess: Subprocess | null;     // Bun subprocess
  ws: WebSocket | null;            // Connected client
  lastActivity: number;
  idleTimer: Timer | null;
  streamBuffer: string;             // Uncommitted streaming text
  isGenerating: boolean;
  systemContextSent: boolean;
  messages: Message[];
  meta: SessionMeta;
};
```

---

## Pi Subprocess

### Spawn Command

```typescript
const args = [
  'pi',
  '--mode', 'rpc',
  '--no-extensions',
  '--no-skills',
  '--no-prompt-templates',
  '--no-themes',
  '--model', model  // e.g., 'opencode/big-pickle'
];

Bun.spawn(args, {
  cwd,
  stdin: 'pipe',
  stdout: 'pipe',  // JSON event stream
  stderr: 'pipe',  // Debug logs
});
```

### RPC Protocol

```
// Send prompt (stdin):
{ "type": "prompt", "id": "request-uuid", "message": "Hello" }

{ "type": "abort", "id": "request-uuid" }

// Receive events (stdout):
{ "type": "message_update", "assistantMessageEvent": { "type": "text_delta", "delta": "..." } }
{ "type": "message_end" }
{ "type": "tool_execution_start", "toolCallId": "...", "toolName": "Bash", "args": {...} }
{ "type": "tool_execution_end", "toolCallId": "...", "result": "...", "isError": false }
{ "type": "agent_end", "cost": {...} }
```

---

## Key Files Reference

| File | Purpose |
|------|---------|
| `server.tsx` | Main server, WebSocket upgrade, route setup |
| `servers/api/pi/websocket.ts` | WebSocket message handling |
| `servers/api/pi/session-manager.ts` | Session lifecycle in memory |
| `servers/api/pi/pi-bridge.ts` | Spawn and communicate with Pi |
| `servers/api/pi/storage.ts` | Disk persistence |
| `servers/api/pi/rest.ts` | REST API endpoints |
| `servers/api/pi/types.ts` | Shared types |
| `workspaces/officerdev/src/hooks/usePiChat.ts` | Main frontend hook |
| `workspaces/hooks/src/useChatWebSocket.ts` | WebSocket connection |
| `workspaces/officerdev/src/apps/Chat/types.ts` | Frontend types |

---

## Configuration

### Environment Variables

- `PORT` - Server port (default: 5000)
- `JWT_SECRET` - JWT signing secret

### Constants

```typescript
// websocket.ts
const IDLE_TIMEOUT_MS = 60 * 60 * 1000; // 1 hour

// storage.ts
const PI_SESSIONS_DIR = '.pi-sessions';
const GROUP_PREFIX = '@';
```

---

## Error Handling

- WebSocket errors → Logged and sent as `{ type: 'error', message: ... }`
- Pi process errors → `{ type: 'error', message: errorMsg }`
- Session not found → HTTP 404 or `SESSION_NOT_FOUND` error code
- Token blacklist check → 401 Unauthorized

---

## Dependencies

### Backend
- `bun` - Runtime
- `hono` - HTTP framework
- `drizzle-orm` - Database (for user auth)

### Frontend
- `react` - UI framework
- `@tanstack/react-query` - Data fetching
- `lucide-react` - Icons

---

*Last updated: 2024*
