25 KiB
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:
type ChatPanelProps = {
chat: ReturnType<typeof usePi>;
availableModels?: ModelOption[];
};
Dependencies:
usePihook (WebSocket connection)useChatSessionshook (session data)useSlashCommandshook (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 positionbottomRef: Auto-scroll targetuserScrolledRef: Track if user manually scrolledtextareaRef: Direct textarea access
Props:
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:
- Start MediaRecorder
- On stop: Convert to WAV format
- POST to Whisper:
http://macmini:8178/inference - Append transcribed text to input
Dependencies:
sonnertoast notificationslucide-reacticons- 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:
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:
{
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:
- No session selected: Empty state message
- Selected existing session: Load and display conversation
- 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:
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 userGET /pi/sessions/{sessionId}- Get session with messagesPUT /pi/sessions/{sessionId}/messages- Save messages (debounced)PATCH /pi/sessions/{sessionId}- Rename sessionDELETE /pi/sessions/{sessionId}- Delete sessionGET /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 userPOST /pi/groups- Create new groupPATCH /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 modelsuseVisiblePiModels()- 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:
{
id: string;
title: string;
model: string;
cwd: string;
createdAt: number;
updatedAt: number;
messageCount: number;
cost: MessageCost;
groupSlug?: string | null;
}
GroupEntry:
{
name: string;
slug: string;
description?: string;
createdAt: number;
updatedAt: number;
sessionCount: number;
}
ChatMessage (frontend message):
| { 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):
| { 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
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
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
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
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
-
OpenCodeModelPicker: Still imported in InputArea but Settings component handles model selection. Can remove if consolidation complete.
-
Whisper endpoint: Hardcoded to
http://macmini:8178. Should be configurable. -
ChatList placeholder:
Chat/ChatList/index.tsxis just a placeholder, contains no actual implementation. -
Legacy type aliases: In
types.ts- marked for removal after Phase 9 cleanup. -
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:
- Open: empty
- Message: handle ServerMessage
- Close: session stays in memory with idle timeout
- 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[] arrayPI_GROUPS: GroupEntry[] arrayPI_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
- Extract message handlers: Move ServerMessage handlers to separate utility file
- Consolidate model selection: Remove OpenCodeModelPicker, use only Settings
- Streaming abstraction: Create reusable streaming hook for other features
- Panel channel typing: Create typed version of usePanelChannel
- Attachment architecture: Unified attachment service (currently inline)
- Voice transcription: Move Whisper into dedicated service with configurable endpoint
- Message components: Extract common patterns from MessageBubble, ToolActivity, QuestionActivity
- Group UI refinement: Drag-and-drop reordering, nested group support
- Search refinement: Full-text search, filter by date/model/group
- Cost tracking: Display cumulative cost per session/group
Integration Status
✅ Filled Instances
All Chat components have been properly integrated throughout the officer-web application:
-
Chat/index.tsx - Now exports:
ChatPanel(main chat container)EmbeddableChat(reusable chat widget)AttachmenttypeInputArea(message input)Settings(model selector)usePi(WebSocket hook)ChatList(session list)
-
ChatList Component - Fully implemented:
- Displays all sessions
- Shows creation date/time
- Shows model name
- Links to individual sessions
-
ChatHistory/index.tsx - Exports:
SessionListPage(main page component)SessionList(session tree view)ChatHistory(sidebar widget)
-
App Registry Integration - Fully configured:
- ✅
chat- ChatPanel widget - ✅
chat-history- SessionHistory navigation - ✅
chat-launcher- Quick chat launcher - All registered in Workspaces/app-registry.tsx
- ✅
-
Dashboard Routes - Complete:
- ✅
/chat- SessionListPage root - ✅
/chat/new- New chat - ✅
/chat/:sessionId- Resume session - All wired to SessionListPage
- ✅
-
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.