# 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// │ │ │ │ - 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 } | { 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; 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/ ├── / │ ├── meta.json # Session metadata │ └── messages.json # Message history │ └── @/ # Grouped sessions ├── .group-meta.json ├── / │ ├── meta.json │ └── messages.json └── / ``` ### 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=` 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(); private userSessions = new Map(); // 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*