Files
platform/CHAT_APPS.md
T

883 lines
25 KiB
Markdown

# Chat & ChatHistory Apps Architecture
**Date**: February 20, 2026
**Status**: Complete & Running
**Scope**: Unified Pi harness integration for chat functionality
---
## Overview
The Chat and ChatHistory applications work together to provide a complete conversational interface with session management, grouping, and model selection. They consume the new Pi harness WebSocket API (`/api/pi/chat/ws`) and REST endpoints (`/api/pi/*`).
---
## Directory Structure
```
src/
├── apps/officer-web/
│ ├── Screens/Dashboard/
│ │ ├── Chat/ # Chat input/output UI components
│ │ │ ├── index.tsx (empty export)
│ │ │ ├── ChatPanel.tsx (main chat container)
│ │ │ ├── EmbeddableChat.tsx (reusable chat widget)
│ │ │ ├── InputArea.tsx (message input with attachments & voice)
│ │ │ ├── Settings.tsx (model/provider selector)
│ │ │ ├── ChatList/index.tsx (placeholder)
│ │ │ ├── OpenCodeModelPicker.tsx (legacy model picker, keep for now)
│ │ │ └── usePi.ts (WebSocket hook for Pi harness)
│ │ │
│ │ └── ChatHistory/ # Session & group management UI
│ │ ├── index.tsx (SessionListPage - main router)
│ │ ├── Screen.tsx (SessionList - tree view)
│ │ ├── ChatDetailPanel.tsx (session or new chat detail panel)
│ │ ├── Widget.tsx (sidebar widget showing recent sessions)
│ │ ├── CreateGroupDialog.tsx (new group form)
│ │ ├── GroupContextMenu.tsx (group actions: rename, delete)
│ │ └── SessionContextMenu.tsx (session actions: move, rename, delete)
│ │
│ └── state/ # State management hooks
│ ├── useChatSessions.ts (REST endpoints: list, get, update, delete, search)
│ ├── useChatGroups.ts (REST endpoints: create, list, update, delete, move)
│ ├── useModels.ts (unified Pi models API)
│ └── useRecentModels.ts (recently used models tracking)
└── workspaces/apps/ # Shared Chat workspace components
├── Chat/
│ ├── index.ts (exports: MessageList, MessageBubble, etc.)
│ ├── types.ts (type definitions for all chat types)
│ ├── MessageList.tsx (message history display)
│ ├── MessageBubble.tsx (individual message component)
│ ├── ToolActivity.tsx (tool execution display)
│ ├── QuestionActivity.tsx (question suggestions)
│ └── [other message components]
└── ChatHistory/
├── index.ts (exports: SessionListPage, SessionList)
└── SessionBar.tsx (session header bar with nav)
```
---
## Component Hierarchy
### Chat Flow
```
ChatPanel (main container)
├── SessionBar (header with back, delete, fullscreen)
└── EmbeddableChat (reusable chat interface)
├── MessageList (scrollable message history)
│ ├── MessageBubble[] (user/assistant messages)
│ ├── ToolActivity (tool execution results)
│ └── StreamingBubble (real-time assistant text)
└── InputArea (message input)
├── Attachment controls (image, webpage, file)
├── Textarea (message input with auto-resize)
├── Model picker (Settings component)
├── Send/Stop button
└── Voice recording (Mic button)
```
### Session Management Flow
```
SessionListPage (routing)
└── WorkspaceLayout (split pane layout)
├── SessionList (left panel: session tree)
│ ├── Ungrouped sessions
│ └── Grouped sections
│ ├── GroupContextMenu (rename, delete group)
│ └── SessionContextMenu (move, rename, delete session)
└── ChatDetailPanel (right panel: chat view)
├── SessionChat (existing session)
│ ├── DetailBar (session info, delete button)
│ └── EmbeddableChat (chat interface)
└── NewChat (new conversation)
├── DetailBar
└── EmbeddableChat
```
---
## Core Components
### 1. ChatPanel
**File**: `Chat/ChatPanel.tsx`
**Purpose**: Main chat container for the `/chat/:sessionId` route.
**Key Features**:
- Displays session title in header
- Handles location.state for pre-filled messages and initial sends
- Manages fullscreen toggle
- Passes chat state to EmbeddableChat
**Props**:
```typescript
type ChatPanelProps = {
chat: ReturnType<typeof usePi>;
availableModels?: ModelOption[];
};
```
**Dependencies**:
- `usePi` hook (WebSocket connection)
- `useChatSessions` hook (session data)
- `useSlashCommands` hook (command handling)
---
### 2. EmbeddableChat
**File**: `Chat/EmbeddableChat.tsx`
**Purpose**: Reusable, embeddable chat widget used in both ChatPanel and ChatDetailPanel.
**Key Features**:
- Message display with streaming text
- Input area with attachment handling
- Jump-to-bottom button when user scrolls up
- Auto-scroll to latest message (unless user scrolled)
- Textarea auto-resize
- Webpage scraping and image upload
- Before-send hook for slash commands
**Key Refs**:
- `scrollViewportRef`: Track scroll position
- `bottomRef`: Auto-scroll target
- `userScrolledRef`: Track if user manually scrolled
- `textareaRef`: Direct textarea access
**Props**:
```typescript
type EmbeddableChatProps = {
chat: ReturnType<typeof usePi>;
availableModels?: ModelOption[];
onBeforeSend?: (text: string) => boolean | Promise<boolean>;
commandFeedback?: string | null;
defaultInput?: string;
promptPrefix?: string;
className?: string;
cwd?: { root?: string; path: string };
autoSend?: boolean;
};
```
---
### 3. InputArea
**File**: `Chat/InputArea.tsx`
**Purpose**: Message input with advanced features (attachments, voice, model selection).
**Key Features**:
- Textarea with dynamic height
- Attachment dropdown (image, file, URL, PDF)
- Voice recording to WAV format
- Whisper transcription (hardcoded to `http://macmini:8178/inference`)
- Paste-to-attach images
- Model picker with provider selection
- Command feedback display
- Send/Stop button
**Voice Recording Pipeline**:
1. Start MediaRecorder
2. On stop: Convert to WAV format
3. POST to Whisper: `http://macmini:8178/inference`
4. Append transcribed text to input
**Dependencies**:
- `sonner` toast notifications
- `lucide-react` icons
- Settings component (model picker)
---
### 4. usePi Hook
**File**: `Chat/usePi.ts`
**Purpose**: Central WebSocket connection to Pi harness with message handling.
**Key Features**:
- Establishes WebSocket connection to `/api/pi/chat/ws`
- Manages message streaming with requestAnimationFrame batching
- Handles all ServerMessage types
- Session initialization and resume
- Debounced message saving (1s)
- Model and CWD tracking
**Message Type Handling**:
| Server Message | Handler | Action |
|----------------|---------|--------|
| `session:init` | Sets sessionId, model, cwd; updates URL |
| `assistant:delta` | Accumulates in streaming buffer |
| `assistant:text` | Commits streaming or adds as-is |
| `tool:start` | Creates tool execution message |
| `tool:result` | Updates tool message with output |
| `result` | Commits streaming, adds cost metadata, sets isGenerating=false |
| `sync:messages` | Restores full message history (resume) |
| `error` | Adds error message, sets isGenerating=false |
| `stopped` | Commits streaming, sets isGenerating=false |
**Streaming Optimization**:
- Uses refs (`streamingRef`) to avoid state thrashing
- RequestAnimationFrame batches updates
- Commits on message boundaries or generation end
**Options**:
```typescript
type UsePiOptions = {
replaceUrl?: boolean; // auto-update URL on session init
storage?: ResourceChatStorage; // custom storage provider
resourceChatDir?: string; // for resource/task chats
taskInfo?: TaskInfo; // task metadata
};
```
**Return Type**:
```typescript
{
messages: ChatMessage[];
streamingText: string;
isConnected: boolean;
isGenerating: boolean;
sessionId: string | null;
model: string | null;
selectedModel: string | null;
cwd: string | null;
setSelectedModel: (modelId: string) => void;
sendPrompt: (text, attachmentIds?, images?, cwd?, groupSlug?) => void;
stopGeneration: () => void;
}
```
---
### 5. Settings Component
**File**: `Chat/Settings.tsx`
**Purpose**: Model selection UI with provider filtering.
**Key Features**:
- Shows provider buttons before first message
- Switches to dropdown display after first message
- Groups models by provider
- Display names for well-known providers
**Display Names**:
```
anthropic → "Anthropic"
openai → "OpenAI"
opencode → "OpenCode Zen"
google → "Google"
groq → "Groq"
... (14 total providers defined)
```
---
### 6. SessionList (Screen.tsx)
**File**: `ChatHistory/Screen.tsx`
**Purpose**: Tree view showing all sessions organized by groups.
**Key Features**:
- Ungrouped sessions at top
- Collapsible group sections
- Session count badges
- Jump to selected session on render
- Create group and new chat buttons
**Structure**:
```
Sessions (Header)
├─ New Chat (button)
└─ New Group (button)
Ungrouped Sessions
├─ Session 1
│ ├─ Title + ID (truncated)
│ ├─ Created date/time
│ ├─ Model name
│ └─ Context menu
Groups (sorted by updatedAt desc)
├─ 📁 Group Name (5 sessions)
│ ├─ Collapse/expand chevron
│ ├─ Group context menu
│ └─ Sessions (nested, indented)
│ ├─ Session A
│ ├─ Session B
│ └─ Context menu per session
```
**Styling**:
- Selected session: `border-duck-teal/30 bg-duck-teal/5`
- Unselected: `border-duck-dark/10 bg-background/80`
---
### 7. ChatDetailPanel
**File**: `ChatHistory/ChatDetailPanel.tsx`
**Purpose**: Right panel showing either existing session or new chat.
**Key Features**:
- Session detail bar (title, delete, status indicators)
- Switches between SessionChat and NewChat components
- Handles location.state for initial message/prefill
- Syncs selected session via panel channel
**Panel States**:
1. No session selected: Empty state message
2. Selected existing session: Load and display conversation
3. Selected 'new': New chat form with optional pre-fill
**Status Indicators**:
- Red dot: Disconnected
- Orange pulsing dot: Generating
- Green dot: Ready
---
### 8. Group & Session Context Menus
**GroupContextMenu** (`ChatHistory/GroupContextMenu.tsx`):
- Rename group (inline edit)
- Delete group (confirm dialog)
- Shows sessions will be ungrouped on delete
**SessionContextMenu** (`ChatHistory/SessionContextMenu.tsx`):
- Move to group (submenu)
- Rename session (inline edit)
- Delete session (direct action)
- Move submenu shows ungrouped + all groups
---
### 9. CreateGroupDialog
**File**: `ChatHistory/CreateGroupDialog.tsx`
**Purpose**: Modal for creating new session groups.
**Features**:
- Name input (required)
- Description input (optional)
- Auto-generated slug (lowercase, kebab-case)
- Form validation
- Error display
- Loading state
**Slug Generation**:
```javascript
name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
```
---
## State Management
### useChatSessions Hook
**File**: `state/useChatSessions.ts`
**REST Endpoints**:
- `POST /pi/sessions` - List all sessions for user
- `GET /pi/sessions/{sessionId}` - Get session with messages
- `PUT /pi/sessions/{sessionId}/messages` - Save messages (debounced)
- `PATCH /pi/sessions/{sessionId}` - Rename session
- `DELETE /pi/sessions/{sessionId}` - Delete session
- `GET /pi/sessions/search?q={query}` - Search sessions
**Query Keys**:
- `['PI_SESSIONS']` - Invalidated on any session change
---
### useChatGroups Hook
**File**: `state/useChatGroups.ts`
**REST Endpoints**:
- `GET /pi/groups` - List all groups for user
- `POST /pi/groups` - Create new group
- `PATCH /pi/groups/{slug}` - Update group (name, description)
- `DELETE /pi/groups/{slug}` - Delete group (moves sessions to root)
- `POST /pi/sessions/{sessionId}/move` - Move session to/from group
**Query Keys**:
- `['PI_GROUPS']` - Invalidated on group changes
- Invalidates `['PI_SESSIONS']` on move/create/delete
---
### useModels Hook
**File**: `state/useModels.ts`
**Functions**:
- `usePiModels()` - Get all available models
- `useVisiblePiModels()` - Filter by enabled models in settings
**REST Endpoint**:
- `GET /pi/models` - List all models
**Query Key**: `['PI_MODELS']` (5-minute stale time)
---
## Type System
### Core Types
**SessionEntry**:
```typescript
{
id: string;
title: string;
model: string;
cwd: string;
createdAt: number;
updatedAt: number;
messageCount: number;
cost: MessageCost;
groupSlug?: string | null;
}
```
**GroupEntry**:
```typescript
{
name: string;
slug: string;
description?: string;
createdAt: number;
updatedAt: number;
sessionCount: number;
}
```
**ChatMessage** (frontend message):
```typescript
| { role: 'user'; text: string; images?: [...] }
| { role: 'assistant'; text: string }
| { role: 'system'; text: string }
| {
role: 'tool';
toolName: string;
toolInput: Record<string, unknown>;
toolCallId: string;
output?: string;
isError?: boolean;
}
| { role: 'result'; cost: MessageCost }
| { role: 'error'; text: string }
```
**ServerMessage** (WebSocket protocol):
```typescript
| { type: 'session:init'; sessionId: string; model: string; cwd: string }
| { type: 'assistant:text'; text: string }
| { type: 'assistant:delta'; text: string }
| { type: 'tool:start'; toolCallId: string; toolName: string; toolInput: ... }
| { 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' }
```
---
## Data Flow
### New Chat
```
User clicks "New Chat"
SessionListPage sets selected.id = 'new'
ChatDetailPanel renders NewChat component
NewChat creates usePi() hook (no sessionId)
usePi connects to WebSocket, sends first prompt
Backend spawns Pi process, sends 'session:init'
usePi updates sessionId, URL changes to /chat/{sessionId}
SessionListPage re-renders, session appears in list
Chat continues normally
```
### Resume Existing Session
```
User clicks session in SessionList
SessionListPage.useEffect sets selected.id = sessionId
ChatDetailPanel renders SessionChat with sessionId
SessionChat calls usePi(sessionId) hook
usePi.useEffect calls getSession(sessionId)
Loads messages from server, populates local state
User sends first new message
usePi sends prompt with sessionId to WebSocket
Backend loads session from disk, injects history in system prompt
Pi process continues with context
```
### Model Switching
```
User selects different model in Settings
onChange handler calls setSelectedModel(modelId)
Next sendPrompt() includes { model: selectedModel }
WebSocket sends to backend with model ID
New Pi process spawned with specified model
session:init message returns new model
```
### Attachment Upload
```
User clicks image/URL in InputArea
EmbeddableChat.handleAttach{Image|Webpage}()
POST /scrape or /upload with sessionId/provider
Attachment added to state with loading=true
Response includes attachmentId
Attachment loading=false, displays thumbnail
User sends message
Attachment prepended to prompt text
Server relocates file to session directory
```
---
## Key Patterns
### 1. Streaming Text Optimization
```typescript
const streamingRef = useRef('');
const rafRef = useRef<number | null>(null);
function flushStreaming() {
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
rafRef.current = requestAnimationFrame(() => {
setStreamingText(streamingRef.current);
rafRef.current = null;
});
}
// On delta: accumulate + flush
streamingRef.current += msg.text;
flushStreaming();
// On message end: commit to messages
function commitStreaming() {
if (!streamingRef.current) return;
setMessages((prev) => [...prev, { role: 'assistant', text: streamingRef.current }]);
streamingRef.current = '';
setStreamingText('');
}
```
**Benefit**: Avoids rendering on every character; batches updates with RAF
### 2. Auto-Scroll Detection
```typescript
const userScrolledRef = useRef(false);
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = viewport;
const atBottom = scrollHeight - scrollTop - clientHeight < 60;
userScrolledRef.current = !atBottom;
setShowJumpToBottom(!atBottom);
};
// Auto-scroll only if user hasn't scrolled up
useEffect(() => {
if (!userScrolledRef.current) {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}
}, [messages, streamingText]);
```
**Benefit**: Respects user's scroll position; auto-scrolls only when at bottom
### 3. Textarea Auto-Resize
```typescript
useEffect(() => {
const textarea = textareaRef.current;
if (!textarea) return;
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
}, [input]);
```
**Behavior**: Grows up to 200px, then scrolls internally
### 4. Debounced Save
```typescript
useEffect(() => {
if (!sessionIdRef.current || messages.length === 0) return;
if (saveTimerRef.current !== null) clearTimeout(saveTimerRef.current);
const sid = sessionIdRef.current;
const snapshot = messages;
saveTimerRef.current = window.setTimeout(() => {
saveMessages(sid, snapshot);
saveTimerRef.current = null;
}, SAVE_DEBOUNCE_MS);
return () => {
if (saveTimerRef.current !== null) clearTimeout(saveTimerRef.current);
};
}, [messages]);
```
**Benefit**: Saves after 1s of no message changes; avoids spam
---
## Hardcoded Configuration
### Whisper Transcription
**Location**: `InputArea.tsx:blobToWav()` and `handleMicClick()`
**Endpoint**: `http://macmini:8178/inference`
**Status**: ⚠️ Hardcoded - needs parameterization
### Session Idle Timeout
**Location**: Backend (see PI_HARNESS_REBUILD.md)
**Value**: 1 hour
---
## Known Issues & TODOs
1. **OpenCodeModelPicker**: Still imported in InputArea but Settings component handles model selection. Can remove if consolidation complete.
2. **Whisper endpoint**: Hardcoded to `http://macmini:8178`. Should be configurable.
3. **ChatList placeholder**: `Chat/ChatList/index.tsx` is just a placeholder, contains no actual implementation.
4. **Legacy type aliases**: In `types.ts` - marked for removal after Phase 9 cleanup.
5. **Panel channel**: Uses string key `'chat:selected-session'` for panel synchronization. Could be more type-safe.
---
## Integration Points
### WebSocket (`usePi.ts`)
**URL**: `/api/pi/chat/ws?token={token}`
**Protocol**: See `ServerMessage` types above
**Lifecycle**:
1. Open: empty
2. Message: handle ServerMessage
3. Close: session stays in memory with idle timeout
4. Error: display error message
### REST API
**Base**: `/api/pi/`
**Endpoints**:
- Sessions: `/sessions` (list), `/sessions/{id}` (get/update/delete), `/sessions/search` (search), `/sessions/{id}/move` (move)
- Groups: `/groups` (list/create), `/groups/{slug}` (update/delete)
- Models: `/models` (list)
**Auth**: Bearer token in Authorization header
### Storage
**Cache Keys**:
- `PI_SESSIONS`: SessionEntry[] array
- `PI_GROUPS`: GroupEntry[] array
- `PI_MODELS`: ModelOption[] array
**Persistence**: TanStack Query with localStorage (default)
---
## Testing Checklist
- [ ] New chat creates session on backend
- [ ] Resume chat loads messages from server
- [ ] Model switching sends to backend
- [ ] Streaming text appears correctly
- [ ] Tool execution tracked properly
- [ ] Session rename works
- [ ] Session delete removes from list
- [ ] Create group works
- [ ] Move session to group updates counts
- [ ] Delete group moves sessions to root
- [ ] Collapse/expand groups works
- [ ] Fullscreen toggle works
- [ ] Voice recording transcribes to input
- [ ] Image paste/upload works
- [ ] Webpage scrape/attach works
- [ ] Jump-to-bottom button appears when scrolled
- [ ] Auto-scroll respects user scroll
- [ ] Messages debounce save correctly
- [ ] Panel sync between components works
- [ ] URL updates on session init
---
## Future Refactoring Opportunities
1. **Extract message handlers**: Move ServerMessage handlers to separate utility file
2. **Consolidate model selection**: Remove OpenCodeModelPicker, use only Settings
3. **Streaming abstraction**: Create reusable streaming hook for other features
4. **Panel channel typing**: Create typed version of usePanelChannel
5. **Attachment architecture**: Unified attachment service (currently inline)
6. **Voice transcription**: Move Whisper into dedicated service with configurable endpoint
7. **Message components**: Extract common patterns from MessageBubble, ToolActivity, QuestionActivity
8. **Group UI refinement**: Drag-and-drop reordering, nested group support
9. **Search refinement**: Full-text search, filter by date/model/group
10. **Cost tracking**: Display cumulative cost per session/group
---
## Integration Status
### ✅ Filled Instances
All Chat components have been properly integrated throughout the officer-web application:
1. **Chat/index.tsx** - Now exports:
- `ChatPanel` (main chat container)
- `EmbeddableChat` (reusable chat widget)
- `Attachment` type
- `InputArea` (message input)
- `Settings` (model selector)
- `usePi` (WebSocket hook)
- `ChatList` (session list)
2. **ChatList Component** - Fully implemented:
- Displays all sessions
- Shows creation date/time
- Shows model name
- Links to individual sessions
3. **ChatHistory/index.tsx** - Exports:
- `SessionListPage` (main page component)
- `SessionList` (session tree view)
- `ChatHistory` (sidebar widget)
4. **App Registry Integration** - Fully configured:
-`chat` - ChatPanel widget
-`chat-history` - SessionHistory navigation
-`chat-launcher` - Quick chat launcher
- All registered in Workspaces/app-registry.tsx
5. **Dashboard Routes** - Complete:
-`/chat` - SessionListPage root
-`/chat/new` - New chat
-`/chat/:sessionId` - Resume session
- All wired to SessionListPage
6. **Integration Points**:
- ✅ ChatPanel (ChatHistory/ChatDetailPanel.tsx)
- ✅ EmbeddableChat (Files/TaskRunnerModal.tsx)
- ✅ usePi hook (CapabilityPage.tsx)
- ✅ Model selection (Settings.tsx)
- ✅ Task runner modal (Files/Screen/TaskRunnerModal.tsx)
- ✅ Automation edit chat (Automation/AutomationEditChat.tsx)
### Type Exports
All Chat-related types properly exported from `workspaces/apps/Chat/types.ts`:
-`ChatMessage` (union type for all message variants)
-`ServerMessage` (WebSocket protocol types)
-`Message` (backend message format)
-`SessionEntry` (session metadata)
-`GroupEntry` (group metadata)
-`MessageCost` (cost tracking)
-`ModelOption` (model definition)
-`TaskInfo` (task metadata)
### State Management
All state hooks properly integrated:
-`useChatSessions` - REST endpoints for sessions
-`useChatGroups` - REST endpoints for groups
-`usePiModels` - Model listing
-`useVisiblePiModels` - Filtered models by settings
### Compilation Status
✅ No Chat-related TypeScript errors
✅ All imports resolve correctly
✅ Type definitions align with WebSocket protocol
---
## Summary
The Chat & ChatHistory apps form a cohesive system for conversational AI interaction:
- **Chat** components handle real-time messaging and user input
- **ChatHistory** components manage session persistence and organization
- **usePi** hook bridges frontend and Pi harness backend
- **State hooks** provide clean REST API abstraction
- **Type system** ensures message protocol consistency
- **Streaming optimization** prevents UI thrashing
- **Grouping feature** organizes conversations logically
- **All instances filled** and properly integrated across the dashboard
The architecture is clean, modular, and fully integrated throughout the application. Ready for the major refactor.