Files
platform/CHAT_APPS.md
T

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:

  • 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:

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:

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:

  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:

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:

{
  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

  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.