# Chat Interface + Task Orchestration Research Report **Date:** 2026-03-01 **Focus:** Analysis of Mission Control and Clawtrol for Mosaic Stack feature development **Goal:** Extract actionable design patterns for chat, task dispatch, and live event feeds --- ## Executive Summary Both Mission Control and Clawtrol are OpenClaw-compatible dashboards with complementary strengths: | Feature | Mission Control | Clawtrol | Mosaic Stack Gap | |---------|----------------|----------|------------------| | Chat with agents | ❌ No direct chat | ✅ Full session chat + send | **HIGH** - Stub exists, not wired | | Task dispatch | ✅ AI planning + Kanban | ✅ Simple Kanban | Medium - Kanban exists | | Live events | ✅ SSE-based feed | ❌ Polling only | Medium - SSE polling exists | | Session viewer | ❌ No | ✅ Full transcript view | **HIGH** - Missing | | Agent management | ✅ Auto-create agents | ❌ Basic list | Medium | **Top 3 Quick Wins for Mosaic Stack:** 1. **Session chat interface** (< 4 hours) - Wire existing chat stub to OpenClaw API 2. **Session list view** (< 2 hours) - Read `sessions.json` + `.jsonl` transcripts 3. **Task card planning indicator** (< 1 hour) - Add purple pulse animation --- ## 1. Chat Interface Analysis ### Clawtrol Sessions Module (Best Reference) **File:** `src/components/modules/SessionsModule/index.tsx` **Key Architecture:** ```typescript // Session list fetched from OpenClaw const res = await fetch('/api/sessions'); const data = await res.json(); setSessions(data.sessions || []); // Session detail with message history const res = await fetch(`/api/sessions/${encodeURIComponent(session.key)}?limit=50`); const data = await res.json(); setChatMessages(data.messages || []); // Send message to session (via Telegram or direct) await fetch('/api/sessions/send', { method: 'POST', body: JSON.stringify({ sessionKey: selectedSession.key, message: msg }), }); ``` **UI Pattern - Two-Column Chat Layout:** ```tsx // Session list view
{sessions.map(session => (
openSessionChat(session)}> {/* Activity indicator */}
{/* Session metadata */} {session.messageCount} msgs · {session.totalTokens}k tokens ${session.estimatedCost.toFixed(2)} {/* Last message preview */}
{session.lastMessages[0]?.text?.slice(0, 100)}
))}
``` **Chat View Pattern:** ```tsx // Messages container with auto-scroll
{chatMessages.map(msg => (
{/* Role badge */} {msg.role === 'user' ? 'you' : 'assistant'} {/* Markdown content */}
{renderMarkdown(msg.text)}
))}
{/* Auto-scroll anchor */}
// Input with Enter to send e.key === 'Enter' && sendChatMessage()} /> ``` **Session API Pattern (`/api/sessions/route.ts`):** ```typescript // Priority: CLI > Index file > Direct file scan const SESSIONS_INDEX = join(os.homedir(), '.openclaw', 'agents', 'main', 'sessions', 'sessions.json'); const SESSIONS_DIR = join(os.homedir(), '.openclaw', 'agents', 'main', 'sessions'); // Read sessions from index const sessionsMap = JSON.parse(await readFile(SESSIONS_INDEX, 'utf-8')); // Enrich with message count and last messages for (const session of sessions) { const [msgs, count] = await Promise.all([ getLastMessages(sessionFile, 3), // Last 3 messages getMessageCount(sessionFile), // Total count ]); } // Parse JSONL for messages function getLastMessages(sessionFile: string, count: number) { const lines = data.trim().split('\n').filter(Boolean); for (let i = lines.length - 1; i >= 0 && messages.length < count; i--) { const parsed = JSON.parse(lines[i]); if (parsed.type === 'message' && parsed.message) { messages.unshift({ role: parsed.message.role, text: extractTextFromContent(parsed.message.content), timestamp: parsed.timestamp, }); } } } ``` **Message Send Pattern (`/api/sessions/send/route.ts`):** ```typescript // Parse session key to determine target function parseSessionKey(key: string): { chatId: string; topicId?: string } | null { // agent:main:main → DM to owner if (key === 'agent:main:main') { return { chatId: await getDefaultChatId() }; } // agent:main:telegram:group::topic: const topicMatch = key.match(/:group:(-?\d+):topic:(\d+)$/); if (topicMatch) { return { chatId: topicMatch[1], topicId: topicMatch[2] }; } } // Send via Telegram Bot API (or could use OpenClaw chat.send) const res = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, { method: 'POST', body: JSON.stringify({ chat_id: target.chatId, text: message }), }); ``` ### Key Takeaways for Mosaic Stack 1. **Session key format:** `agent:main:telegram:group::topic:` or `agent:main:main` 2. **JSONL parsing:** Read from `~/.openclaw/agents/main/sessions/.jsonl` 3. **Cost estimation:** ```typescript const isOpus = modelName.includes('opus'); const inputRate = isOpus ? 15 : 3; const outputRate = isOpus ? 75 : 15; const cost = (inputTokens / 1_000_000 * inputRate) + (outputTokens / 1_000_000 * outputRate); ``` 4. **Activity color logic:** ```typescript if (lastActivity > hourAgo) return 'green'; // Active if (lastActivity > dayAgo) return 'yellow'; // Recent return 'dim'; // Stale ``` --- ## 2. Task/Agent Dispatch Flow (Mission Control) ### AI Planning UX Pattern **The Flow:** ``` CREATE → PLAN (AI Q&A) → ASSIGN (Auto-agent) → EXECUTE → DELIVER ``` **Status Columns:** ``` PLANNING → INBOX → ASSIGNED → IN PROGRESS → TESTING → REVIEW → DONE ``` **PlanningTab.tsx - Core Pattern:** 1. **Start Planning Button:** ```tsx if (!state?.isStarted) { return ( ); } ``` 2. **Question/Answer Loop:** ```tsx // Current question display

{state.currentQuestion.question}

// Multiple choice options {state.currentQuestion.options.map(option => ( ))} // "Other" option with text input {isOther && isSelected && ( )} ``` 3. **Polling for AI Response:** ```typescript // Poll every 2 seconds for next question pollingIntervalRef.current = setInterval(() => { pollForUpdates(); }, 2000); // 90-second timeout pollingTimeoutRef.current = setTimeout(() => { setError('Taking too long to respond...'); }, 90000); ``` 4. **Planning Complete - Spec Display:** ```tsx if (state?.isComplete && state?.spec) { return (
Planning Complete
{/* Generated spec */}

{state.spec.title}

{state.spec.summary}

    {state.spec.deliverables.map(d =>
  • {d}
  • )}
    {state.spec.success_criteria.map(c =>
  • {c}
  • )}
{/* Auto-created agents */} {state.agents.map(agent => (
{agent.avatar_emoji}

{agent.name}

{agent.role}

))}
); } ``` ### Planning API Pattern **POST `/api/tasks/[id]/planning` - Start Planning:** ```typescript // Create session key const sessionKey = `agent:main:planning:${taskId}`; // Build planning prompt const planningPrompt = ` PLANNING REQUEST Task Title: ${task.title} Task Description: ${task.description} Generate your FIRST question. Respond with ONLY valid JSON: { "question": "Your question here?", "options": [ {"id": "A", "label": "First option"}, {"id": "B", "label": "Second option"}, {"id": "other", "label": "Other"} ] } `; // Send to OpenClaw await client.call('chat.send', { sessionKey, message: planningPrompt, }); // Store in DB UPDATE tasks SET planning_session_key = ?, planning_messages = ?, status = 'planning' ``` **Key Insight:** The AI doesn't just plan - it asks **multiple-choice questions** to clarify requirements. This is the "AI clarification before dispatch" pattern. ### Kanban Card with Planning Indicator ```tsx // TaskCard.tsx const isPlanning = task.status === 'planning';
{isPlanning && (
Continue planning
)}
``` ### Auto-Dispatch Pattern ```typescript // When task moves from PLANNING → INBOX (planning complete) if (shouldTriggerAutoDispatch(oldStatus, newStatus, agentId)) { await triggerAutoDispatch({ taskId, taskTitle, agentId, agentName, workspaceId, }); } ``` --- ## 3. Live Event Feed ### Mission Control SSE Pattern **`src/lib/events.ts`:** ```typescript // In-memory client registry const clients = new Set(); export function registerClient(controller) { clients.add(controller); } export function broadcast(event: SSEEvent) { const data = `data: ${JSON.stringify(event)}\n\n`; const encoded = new TextEncoder().encode(data); for (const client of Array.from(clients)) { try { client.enqueue(encoded); } catch { clients.delete(client); } } } ``` **LiveFeed Component:** ```tsx // Filter tabs
{['all', 'tasks', 'agents'].map(tab => ( ))}
// Event list with icons {filteredEvents.map(event => (
{getEventIcon(event.type)}

{event.message}

{formatDistanceToNow(event.created_at)}
))} // Event icons function getEventIcon(type: string) { switch (type) { case 'task_created': return '📋'; case 'task_assigned': return '👤'; case 'task_completed': return '✅'; case 'message_sent': return '💬'; case 'agent_joined': return '🎉'; } } ``` ### SSE vs WebSocket Trade-off | Aspect | SSE (Mission Control) | WebSocket (Clawtrol) | |--------|----------------------|---------------------| | Direction | Server → Client only | Bidirectional | | Reconnect | Automatic browser handling | Manual implementation | | Overhead | HTTP-based, lighter | Full TCP connection | | Use case | Event feeds, notifications | Real-time terminal, chat | **Recommendation:** Use SSE for event feeds (simpler), WebSocket for interactive terminals. --- ## 4. Session Viewer Pattern ### Clawtrol Session List ```tsx // Session card with activity indicator
openSessionChat(session)}> {/* Activity dot */}
{/* Session info */}

{session.label}

{session.messageCount} msgs · {session.totalTokens}k tokens {session.estimatedCost > 0 && · ${session.estimatedCost.toFixed(2)}} {session.model && · {session.model}}
{/* Last message preview */} {session.lastMessages?.length > 0 && (
{session.lastMessages[0]?.role === 'user' ? 'you: ' : 'assistant: '} {session.lastMessages[0]?.text?.slice(0, 100)}
))}
``` ### Session Label Mapping ```typescript const TOPIC_NAMES: Record = { '1369': '🔖 Bookmarks', '13': '🌴 Bali Trip', '14': '💰 Expenses', // ... user-defined topic labels }; function getSessionLabel(key: string): string { if (key === 'agent:main:main') return 'Main Session (DM)'; if (key.includes(':subagent:')) return `Subagent ${uuid.slice(0, 8)}`; // Telegram topic const topicMatch = key.match(/:topic:(\d+)$/); if (topicMatch) { return TOPIC_NAMES[topicMatch[1]] || `Topic ${topicMatch[1]}`; } return key.split(':').pop() || key; } ``` --- ## 5. OpenClaw Client Integration ### WebSocket Client Pattern **`src/lib/openclaw/client.ts`:** ```typescript export class OpenClawClient extends EventEmitter { private ws: WebSocket | null = null; private pendingRequests = new Map(); private connected = false; private authenticated = false; async connect(): Promise { // Add token to URL for auth const wsUrl = new URL(this.url); wsUrl.searchParams.set('token', this.token); this.ws = new WebSocket(wsUrl.toString()); this.ws.onmessage = (event) => { const data = JSON.parse(event.data); // Handle challenge-response auth if (data.type === 'event' && data.event === 'connect.challenge') { const response = { type: 'req', id: crypto.randomUUID(), method: 'connect', params: { auth: { token: this.token }, role: 'operator', scopes: ['operator.admin'], } }; this.ws.send(JSON.stringify(response)); return; } // Handle RPC responses if (data.type === 'res') { const pending = this.pendingRequests.get(data.id); if (pending) { data.ok ? pending.resolve(data.payload) : pending.reject(data.error); } } }; } async call(method: string, params?: object): Promise { const id = crypto.randomUUID(); const message = { type: 'req', id, method, params }; return new Promise((resolve, reject) => { this.pendingRequests.set(id, { resolve, reject }); this.ws.send(JSON.stringify(message)); // 30s timeout setTimeout(() => { if (this.pendingRequests.has(id)) { this.pendingRequests.delete(id); reject(new Error(`Timeout: ${method}`)); } }, 30000); }); } // Convenience methods async listSessions() { return this.call('sessions.list'); } async sendMessage(sessionId: string, content: string) { return this.call('sessions.send', { session_id: sessionId, content }); } async listAgents() { return this.call('agents.list'); } } ``` ### Event Deduplication Pattern ```typescript // Global dedup cache (survives Next.js hot reload) const GLOBAL_EVENT_CACHE_KEY = '__openclaw_processed_events__'; const globalProcessedEvents = globalThis[GLOBAL_EVENT_CACHE_KEY] || new Map(); // Content-based event ID function generateEventId(data: any): string { const canonical = JSON.stringify({ type: data.type, seq: data.seq, runId: data.payload?.runId, payloadHash: createHash('sha256').update(JSON.stringify(data.payload)).digest('hex').slice(0, 16), }); return createHash('sha256').update(canonical).digest('hex').slice(0, 32); } // Skip duplicates if (globalProcessedEvents.has(eventId)) return; globalProcessedEvents.set(eventId, Date.now()); // LRU cleanup if (globalProcessedEvents.size > MAX_EVENTS) { // Remove oldest entries } ``` --- ## 6. Feature Recommendations for Mosaic Stack ### Quick Wins (< 4 hours each) | Feature | Effort | Impact | Source | |---------|--------|--------|--------| | **Session list page** | 2h | HIGH | Clawtrol | | **Session chat interface** | 4h | HIGH | Clawtrol | | **Planning indicator on task cards** | 1h | MEDIUM | Mission Control | | **Activity dots (green/yellow/dim)** | 30m | MEDIUM | Clawtrol | | **Token/cost display per session** | 1h | MEDIUM | Clawtrol | | **Event feed filter tabs** | 1h | LOW | Mission Control | ### Medium Effort (4-16 hours) | Feature | Effort | Impact | Description | |---------|--------|--------|-------------| | **AI planning flow** | 8h | HIGH | Multi-choice Q&A before dispatch | | **OpenClaw WebSocket client** | 4h | HIGH | Real-time event streaming | | **Session transcript viewer** | 4h | MEDIUM | JSONL parsing + display | | **Auto-agent creation** | 8h | MEDIUM | Generate agents from planning spec | ### Architecture Recommendations 1. **Keep SSE for event feed** - Simpler than WebSocket for one-way updates 2. **Use OpenClaw `chat.send` for messages** - Don't implement Telegram API directly 3. **Store session metadata in PostgreSQL** - Mirror `sessions.json` for joins 4. **Implement planning as a state machine** - Clear states: idle → started → questioning → complete --- ## 7. Code Snippets to Reuse ### Session API Route (Clawtrol-style) ```typescript // app/api/sessions/route.ts import { readFile, readdir } from 'fs/promises'; import { join } from 'path'; import os from 'os'; const SESSIONS_DIR = join(os.homedir(), '.openclaw', 'agents', 'main', 'sessions'); export async function GET() { // Try CLI first try { const { stdout } = await execAsync('openclaw sessions --json'); return NextResponse.json({ sessions: JSON.parse(stdout).sessions, source: 'cli' }); } catch {} // Fallback to file const index = await readFile(join(SESSIONS_DIR, 'sessions.json'), 'utf-8'); const sessionsMap = JSON.parse(index); const sessions = await Promise.all( Object.entries(sessionsMap).map(async ([key, data]) => ({ key, label: getSessionLabel(key), kind: getSessionKind(key), lastActivity: new Date(data.updatedAt).toISOString(), messageCount: await getMessageCount(key), totalTokens: data.totalTokens || 0, estimatedCost: calculateCost(data), })) ); return NextResponse.json({ sessions, source: 'file' }); } ``` ### Activity Indicator Component ```tsx // components/ActivityIndicator.tsx export function ActivityIndicator({ lastActivity }: { lastActivity: Date }) { const now = Date.now(); const hourAgo = now - 60 * 60 * 1000; const dayAgo = now - 24 * 60 * 60 * 1000; const color = lastActivity.getTime() > hourAgo ? 'bg-green-500' : lastActivity.getTime() > dayAgo ? 'bg-yellow-500' : 'bg-gray-500'; const glow = lastActivity.getTime() > hourAgo ? 'shadow-[0_0_6px_rgba(34,197,94,0.5)]' : ''; return (
); } ``` ### Cost Estimation Utility ```typescript // lib/cost-estimation.ts const RATES = { opus: { input: 15, output: 75 }, sonnet: { input: 3, output: 15 }, haiku: { input: 0.25, output: 1.25 }, }; export function estimateCost(model: string, inputTokens: number, outputTokens: number): number { const tier = model.includes('opus') ? 'opus' : model.includes('sonnet') ? 'sonnet' : 'haiku'; const rates = RATES[tier]; return (inputTokens / 1_000_000 * rates.input) + (outputTokens / 1_000_000 * rates.output); } ``` --- ## 8. Summary **Best patterns to steal:** 1. **Clawtrol's session chat** - Clean two-panel layout with activity dots 2. **Mission Control's planning flow** - Multi-choice Q&A with polling 3. **Clawtrol's JSONL parsing** - Efficient reverse-iteration for last N messages 4. **Mission Control's SSE events** - Simple broadcast pattern with client registry 5. **Activity color logic** - Hour = green, day = yellow, older = dim **Don't copy:** 1. Telegram Bot API integration - Use OpenClaw `chat.send` instead 2. File-based session index - Mosaic Stack has PostgreSQL 3. PM2 daemon management - Use Docker/systemd **Next steps:** 1. Create `/app/(dashboard)/sessions` page with session list 2. Add chat view at `/app/(dashboard)/sessions/[key]` 3. Wire `/api/sessions` route to OpenClaw CLI or sessions.json 4. Add `ActivityIndicator` component to session cards 5. Add "Start Planning" button to task cards in Kanban